2 回收無效對象的過程
當經可達性算法篩選出失效的對象之後,并不是立即清除,而是再給對象一次重生的機會
- 判斷是否覆寫finalize()
-
- 未覆寫該或已調用過該方法,直接釋放對象記憶體
- 已覆寫該方法且還未被執行,則将finalize()扔到F-Queue隊列中
-
執行F-Queue中的finalize()
虛拟機會以較低的優先級執行這些finalize(),不會確定所有的finalize()都會執行結束
如果finalize()中出現耗時操作,虛拟機就直接停止執行,将該對象清除
- 對象重生或死亡
- 如果在執行finalize()方法時,将this賦給了某一個引用,則該對象重生
- 如果沒有,那麼就會被垃圾收集器清除
注意:強烈不建議使用finalize()進行任何操作!
如果需要釋放資源,請用try-finally或者其他方式都能做得更好.
因為finalize()不确定性大,開銷大,無法保證各個對象的調用順序.
以下代碼示例看到:一個對象的finalize被執行,但依然可以存活
/**
* 示範兩點:
* 1.對象可以在被GC時自救
* 2.這種自救機會隻有一次,因為一個對象的finalize()最多隻能被系統自動調用一次,是以第二次自救失敗
* @author sss
* @since 17-9-17 下午12:02
*
*/
public class FinalizeEscapeGC {
private static FinalizeEscapeGC SAVE_HOOK = null;
private void isAlive() {
System.out.println("yes,I am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize methodd executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 對象第一次成功自救
SAVE_HOOK = null;
System.gc();
// 因為finalize方法優先級很低,是以暫停0.5s以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,I am dead :(");
}
// 自救失敗
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,I am dead :(");
}
}
}
運作結果
finalize methodd executed!
yes,I am still alive :)
no,I am dead :(
3 方法區的記憶體回收
使用
複制算法
實作堆的記憶體回收,堆被分為
新生代
和
老年代
- 新生代中的對象"朝生夕死",每次垃圾回收都會清除掉大量對象
- 老年代中的對象生命較長,每次垃圾回收隻有少量的對象被清除
由于方法區中存放生命周期較長的類資訊、常量、靜态變量.
是以方法區就像堆的老年代,每次GC隻有少量垃圾被清除.
方法區中主要清除兩種垃圾
- 廢棄常量
- 無用類
3.1 回收廢棄常量
回收廢棄常量和回收對象類似,隻要常量池中的常量不被任何變量或對象引用,那麼這些常量就會被清除.
3.2 回收無用類
判定無用類的條件則較為苛刻
-
該類所有執行個體都已被回收
即Java堆不存在該類的任何執行個體
- 加載該類的ClassLoader已被回收
-
該類的java.lang.Class對象沒有被任何對象或變量引用,無法通過反射通路該類的方法
隻要一個類被虛拟機加載進方法區,那麼在堆中就會有一個代表該類的對象:java.lang.Class.這個對象在類被加載進方法區的時候建立,在方法區中該類被删除時清除.
4 垃圾收集算法
4.1 清除(Sweep)
最基礎
的收集算法,後續算法也都是基于此并改進其不足而得.
該算法會從每個GC Roots出發,依次标記有引用關系的對象,最後将沒有被标記的對象清除
把死亡對象所占據的記憶體标記為空閑記憶體,并記錄在一個空閑清單(free list)之中
當需要建立對象時,記憶體管理子產品便會從該空閑清單中尋找空閑記憶體,并劃分給建立的對象。

不足
清除這種回收方式的原理及其簡單,但是有兩個缺點
記憶體碎片
由于Java虛拟機的堆中對象必須是連續分布的,是以可能出現總空閑記憶體足夠,但是無法配置設定的極端情況。
配置設定效率較低
如果是一塊連續的記憶體空間,那麼我們可以通過指針加法(pointer bumping)來做配置設定
而對于空閑清單,Java虛拟機則需要逐個通路清單中的項,來查找能夠放入建立對象的空閑記憶體。
第二種是壓縮(compact),即把存活的對象聚集到記憶體區域的起始位置,進而留下一段連續的記憶體空間。這種做法能夠解決記憶體碎片化的問題,但代價是壓縮算法的性能開銷。
這種算法會帶來大量的空間碎片,導緻需要配置設定一個較大連續空間時容易觸發FullGC,降低了空間使用率.
為了解決這個問題,又提出了“标記-整理算法”,該算法類似計算機的磁盤整理,首先會從GC Roots出發标記存活的對象,然後将存活對象整理到記憶體空間的一端,形成連續的已使用空間,最後把已使用空間之外的部分全部清理掉,這樣就不會産生空間碎片的問題
4.2 複制算法(Copy)
把記憶體區域分為兩等分,分别用兩個指針from和to來維護,并且隻是用from指針指向的記憶體區域來配置設定記憶體。
當發生垃圾回收時,便把存活的對象複制到to指針指向的記憶體區域中,并且交換from指針和to指針的内容。複制這種回收方式同樣能夠解決記憶體碎片化的問題,但是它的缺點也極其明顯,即堆空間的使用效率極其低下。
将記憶體分成大小相等兩份,隻将資料存儲在其中一塊上
- 當需要回收時,首先标記廢棄資料
- 然後将有用資料複制到另一塊記憶體
- 最後将第一塊記憶體空間全部清除
4.2.1 分析
- 這種算法避免了空間碎片,但記憶體縮小了一半.
- 每次都需将有用資料全部複制到另一片記憶體,效率不高
4.2.2 解決空間使用率問題
堆記憶體空間分為較大的Eden和兩塊較小的Survivor,每次隻使用Eden和Survivor區的一塊。這種情形下的“ Mark-Copy"減少了記憶體空間的浪費。“Mark-Copy”現作為主流的YGC算法進行新生代的垃圾回收。
在新生代中,由于大量對象都是"朝生夕死",也就是一次垃圾收集後隻有少量對象存活
是以我們可以将記憶體劃分成三塊
-
Eden、Survior1、Survior2
- 記憶體大小分别是8:1:1
配置設定記憶體時,隻使用Eden和一塊Survior1.
- 當發現Eden+Survior1的記憶體即将滿時,JVM會發起一次
,清除掉廢棄的對象,Minor GC
- 并将所有存活下來的對象複制到另一塊Survior2中.
- 接下來就使用Survior2+Eden進行記憶體配置設定
通過這種方式,隻需要浪費10%的記憶體空間即可實作帶有壓縮功能的垃圾收集方法,避免了記憶體碎片的問題.
4.2.3 配置設定擔保
準備為一個對象配置設定記憶體時,發現此時Eden+Survior中空閑的區域無法裝下該對象
就會觸發
MinorGC
(新生代 GC 算法),對該區域的廢棄對象進行回收.
但如果MinorGC過後隻有少量對象被回收,仍然無法裝下新對象
- 那麼此時需要将Eden+Survior中的
都所有對象
中,然後再将新對象存入Eden區.這個過程就是"配置設定擔保".轉移到老年代
在發生 minor gc 前,虛拟機會檢測
老年代最大可用連續空間是否大于新生代所有對象總空間
若成立,minor gc 可確定安全
若不成立,JVM會檢視
HandlePromotionFailure
是否允許擔保失敗
-
若允許
那麼會繼續檢測老年代最大可用的連續空間是否 > 曆次晉升到老年代對象的平均大小
-
若大于
則将嘗試進行一次 minor gc,盡管這次 minor gc 是有風險的
-
若小于或 HandlePromotionFailure 設定不允許冒險
改為進行一次 full gc (老年代GC)
-
4.3 壓縮算法(Compact)
在回收前,标記過程仍與"清除"一樣
但後續不是直接清理可回收對象,而是
- 将所有存活對象移到一端
- 直接清掉端邊界之外記憶體
深入了解Java虛拟機(JVM) --- 垃圾收集算法(中)2 回收無效對象的過程3 方法區的記憶體回收4 垃圾收集算法5 Java中引用的種類總結參考
分析
這是一種老年代垃圾收集算法.
老年代中對象一般壽命較長,每次垃圾回收會有大量對象存活
是以如果選用"複制"算法,每次需要較多的複制操作,效率低
而且,在新生代中使用"複制"算法
當 Eden+Survior 都裝不下某個對象時,可使用老年代記憶體進行"配置設定擔保"
而如果在老年代使用該算法,那麼在老年代中如果出現 Eden+Survior 裝不下某個對象時,沒有其他區域給他作配置設定擔保
是以,老年代中一般使用"壓縮"算法
4.4 分代收集算法(Generational Collection)
目前商業虛拟機都采用此算法.
根據對象存活周期的不同将Java堆劃分為老年代和新生代,根據各個年代的特點使用最佳的收集算法.
- 老年代中對象存活率高,無額外空間對其配置設定擔保,必須使用"标記-清除"或"标記-壓縮"算法
- 新生代中存放"朝生夕死"的對象,用複制算法,隻需要付出少量存活對象的複制成本,就可完成收集
5 Java中引用的種類
Java中根據生命周期的長短,将引用分為4類
-
強引用
我們平時所使用的引用就是強引用
類似
A a = new A();
即通過關鍵字new建立的對象所關聯的引用就是強引用
隻要強引用還存在,該對象永遠不會被回收
-
軟引用
一些還有用但并非必需的對象
隻有當堆即将發生OOM異常時,JVM才會回收軟引用所指向的對象.
軟引用通過SoftReference類實作
軟引用的生命周期比強引用短一些
-
弱引用
也是描述非必需對象,比軟引用更弱
所關聯的對象隻能存活到下一次GC發生前.
隻要垃圾收集器工作,無論記憶體是否足夠,弱引用所關聯的對象都會被回收.
弱引用通過WeakReference類實作.
-
虛引用
也叫幽靈(幻影)引用,最弱的引用關系.
它和沒有引用沒有差別,無法通過虛引用取得對象執行個體.
設定虛引用唯一的作用就是在該對象被回收之前收到一條系統通知.
虛引用通過PhantomReference類來實作.
總結
Java虛拟機中的垃圾回收器采用可達性分析來探索所有存活的對象。它從一系列GC Roots出發,邊标記邊探索所有被引用的對象。
為了防止在标記過程中堆棧的狀态發生改變,Java虛拟機采取安全點機制來實作Stop-the-world操作,暫停其他非垃圾回收線程。
回收死亡對象的記憶體共有三種方式,分别為:會造成記憶體碎片的清除、性能開銷較大的壓縮、以及堆使用效率較低的複制。
今天的實踐環節,你可以體驗一下無安全點檢測的計數循環帶來的長暫停。你可以分别測單獨跑foo方法或者bar方法的時間,然後與合起來跑的時間比較一下。
// time java SafepointTestp
// 還可以使用如下幾個選項
// -XX:+PrintGC
// -XX:+PrintGCApplicationStoppedTime
// -XX:+PrintSafepointStatistics
// -XX:+UseCountedLoopSafepoints
public class SafepointTest {
static double sum = 0;
public static void foo() {
for (int i = 0; i < 0x77777777; i++) {
sum += Math.sqrt(i);
}
}
public static void bar() {
for (int i = 0; i < 50_000_000; i++) {
new Object().hashCode();
}
}
public static void main(String[] args) {
new Thread(SafepointTest::foo).start();
new Thread(SafepointTest::bar).start();
}
}
參考
- java的gc為什麼要分代?
- 深入了解Java虛拟機(第2版)
- 深入拆解Java虛拟機