垃圾回收
在 JVM 中需要對沒有被引用的對象,也就是垃圾對象進行垃圾回收
對象存活判斷算法
判斷對象存活有兩種方式:引用計數法、可達性分析算法
引用計數法
引用計數法通過記錄每個對象被引用的次數,例如對象 A 被引用 1 次,就將 A 的引用計數器加 1,當其他對象對 A 的引用失效了,就將 A 的引用計數器減 1
- 優點:
- 實現簡單,判定效率高
- 缺點:
- 需要單獨的字段存儲計數器,增加存儲空間開銷
- 每次賦值都要更新計數器,增加時間開銷
- 無法處理循環引用的情況,致命問題!即 A 引用 B,B 引用 A,那么他們兩個的引用計數器永遠都為 1
可達性分析算法
可達性分析算法可以有效解決循環引用的問題,Java 選擇了這種算法
可達性分析算法以根對象集合(GC Roots)
為起使點,按照從上至下的方式搜索被根對象集合所連接的目標對象是否可達
,通過可達性分析算法分析后,內存中的存活對象都會被根對象集合直接或間接連接著,搜索過程所走過的路徑稱為引用鏈
,如果目標對象沒有任何引用鏈
相連,則是不可達的,就可以標記為垃圾對象
GC Roots 主要包含以下幾類元素:
-
虛擬機棧中引用的對象
如:各個線程被調用的方法中所使用的參數、局部變量等
-
本地方法棧內的本地方法引用的對象
-
方法區中引用類型的靜態變量
-
方法區中常量引用的對象
如:字符串常量池里的引用
-
所有被
synchronized
持有的對象 -
Java 虛擬機內部的引用
如:基本數據類型對應的 Class 對象、異常對象(如 NullPointerException、OutOfMemoryError)、系統類加載器
垃圾回收過程
在 Java 中對垃圾對象進行回收需要至少經歷兩次標記過程:
- 第一次標記:如果經過可達性分析后,發現沒有任何引用鏈相連,則會第一次被標記
- 第二次標記:判斷第一次標記的對象是否有必要執行
finalize()
方法,如果在finalize()
方法中沒有重新與引用鏈建立關聯,則會被第二次標記
第二次被標記成功的對象會進行回收;否則,將繼續存活
對象的 finalization 機制:
Java 提供了 finalization
機制來允許開發人員 自定義對象被銷毀之前的處理邏輯
,即在垃圾回收一個對象之前,會先調用這個對象的 finalize()
方法,該方法允許在子類中被重寫,用于在對象被回收時進行資源釋放的工作
對象引用
在 JDK1.2 之后,Java 對引用的概念進行了擴張,將引用分為強引用(StrongReference)、軟引用(SoftReference)、弱引用(WeakReference)、虛引用(PhantomReference)四種,這四種引用強度依次逐漸減弱
-
強引用-不回收:強引用是最普遍的對象引用,也是默認的引用類型,強引用的對象是可觸及的,垃圾回收器永遠不會回收被引用的對象,因此
強引用是造成Java內存泄漏的主要原因之一
。- 當使用new操作創建一個新對象時,并且將其賦值給一個變量時,這個變量就成為該對象的一個
強引用
- 當使用new操作創建一個新對象時,并且將其賦值給一個變量時,這個變量就成為該對象的一個
-
軟引用-內存不足回收:在即將發生內存溢出時,會將這些對象列入回收范圍進行第二次回收,如果回收之后仍然沒有足夠的內存,則會拋出
內存溢出異常
-
軟引用通常用來實現內存敏感的緩存,例如
高速緩存
使用了軟引用,如果內存足夠就暫時保留緩存;如果內存不足,就清理緩存// 創建弱引用 SoftReference<User> softReference = new SoftReference<>(user); // 從軟引用中獲取強引用對象 System.out.println(softReference.get());
-
-
弱引用-發現即回收:被弱引用關聯的對象只能存活在下一次垃圾回收之前,在垃圾回收時,無論空間是否足夠,都會會受掉被弱引用關聯的對象
-
弱引用常用于監控對象是否已經被垃圾回收器標記為即將回收的垃圾,可以通過弱引用的
isEnQueued
方法判斷對象是否被垃圾回收器標記Object obj = new Object(); WeakReference<Object> wf = new WeakReference<Object>(obj); obj = null; // System.gc(); // 有時候會返回null Object o = wf.get(); // 返回是否被垃圾回收器標記為即將回收的垃圾 boolean enqueued = wf.isEnqueued(); System.out.println("o = " + o); System.out.println("enqueued = " + enqueued);
-
-
虛引用:垃圾回收時,直接回收,無法通過虛引用獲取對象實例
-
為一個對象設置虛引用關聯的唯一目的就是能在這個對象被垃圾回收時收到一個系統通知
Object obj = new Object(); PhantomReference<Object> pf = new PhantomReference<Object>(obj, new ReferenceQueue<>()); obj=null; // 永遠返回null Object o = pf.get(); // 返回是否從內存中已經刪除 boolean enqueued = pf.isEnqueued(); System.out.println("o = " + o); System.out.println("enqueued = " + enqueued);
-
垃圾清除算法
GC最基礎的算法有三種: 標記 -清除算法、復制算法、標記-壓縮算法,我們常用的垃圾回收器一般都采用分代收集算法。
-
標記-清除算法
:在標記階段,從 GC Roots 開始遍歷,標記所有被引用的對象,標記為可達對象,再對堆內存從頭到尾遍歷,回收沒有標記為可達對象的對象(標記清除算法可以標記存活對象也可以標記待回收對象)- 這里并不是真正清除,而是將清除對象的地址放在空閑的地址列表中
- 缺點
- 效率不高
- GC 時需要停止整個應用進程,用戶體驗不好
- 會產生內存碎片
-
復制算法
:它將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活
著的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉現在商用的 Java 虛擬機大多都優先采用這種收集算法去回收新生代
,如果將內存區域劃分為容量相同的兩部分太占用空間,因此將復制算法進行了優化
,優化后將新生代分為了 Eden 區、Survivor From 區、Survivor To 區,Eden 和 Survivor 的大小比例為8:1:1
,每次分配內存時只使用 Eden 和其中的一塊 Survivor 區,在進行垃圾回收時,將 Eden 和已經使用過的 Survivor 區的存活對象轉移到另一塊 Survivor 區中,再清理 Eden 和已經使用過的 Survivor 區域,當 Survivor 區域的空間不足以容納一次 Minor GC 之后存活的對象時,就需要依賴老年代進行分配擔保(通過分配擔保機制,將存活的對象放入老年代即可)- 優點
- 實現簡單,運行高效
- 復制之后,保證空間的連續性,不會出現“內存碎片”
- 缺點
- 存在空間浪費
- 應用場景
- 在新生代,常規的垃圾回收,一次可以回收大部分內存空間,
剩余存活對象不多
,因此現在的商業虛擬機都是用這種收集算法回收新生代
- 在新生代,常規的垃圾回收,一次可以回收大部分內存空間,
- 優點
-
標記-壓縮算法
:標記過程仍然與“標記-清除”算法一樣,之后將所有的存活對象壓到內存的一端,按順序排放,之后,清理邊界外的內存- 優點
- 解決了標記-清除算法出現內存碎片的問題
- 解決了復制算法中空間浪費的問題
- 缺點
- 效率上低于復制算法
- 移動對象時,如果對象被其他對象引用,則還需要調整引用的地址
- 移動過程中,需要暫停用戶應用程序。即 STW
- 優點
-
分代收集算法
:把 Java 堆分為新生代和老年代,這樣就可以對不同生命周期的對象采取不同的收集方式,以提高回收效率當前商業虛擬機都采用這種算法
- 新生代中的對象生命周期短,存活率低,因此適合使用
復制算法
(存活對象越少,復制算法效率越高) - 老年代中對象生命周期長,存活率高,回收沒有新生代頻繁,一般使用
標記-清除
或者是標記-壓縮
- 新生代中的對象生命周期短,存活率低,因此適合使用