前言
作為一名Java編程者,想要往高級(jí)進(jìn)階,內(nèi)存管理往往是避不開(kāi)的環(huán)節(jié),而垃圾回收 以下簡(jiǎn)稱GC(Garbage Collection)機(jī)制作為內(nèi)存管理最重要的一個(gè)部分,是我們必須要掌握的。今天就分享下我對(duì) 垃圾回收機(jī)制 與 分代回收策略 的理解.
目錄
1. 背景
-
2. 兩種回收機(jī)制
- 2.1. 引用計(jì)數(shù)
- 2.2. 可達(dá)性分析
-
3. 回收算法
- 3.1. 標(biāo)記清除算法
- 3.2. 復(fù)制算法
- 3.3. 標(biāo)記壓縮算法
-
4. 分代回收策略
- 4.1. 新生代
- 4.2. 老生代
5. 四大引用
1. 背景
一般來(lái)講,在我們編程的過(guò)程中是會(huì)不斷的往內(nèi)存中寫(xiě)入數(shù)據(jù)的,而這些數(shù)據(jù)用完了要及時(shí)從內(nèi)存中清理,否則會(huì)引發(fā)OutOfMemory(內(nèi)存溢出) ,所以每個(gè)編程者都必須遵從這一原則。聽(tīng)說(shuō)(我也不懂C語(yǔ)言~)在C語(yǔ)言階段,垃圾是需要編程者自己手動(dòng)回收的,而我們Javaer相對(duì)來(lái)說(shuō)就要幸福多了,因?yàn)镴VM存在GC機(jī)制,也就是說(shuō)JVM會(huì)幫我們自動(dòng)清理垃圾,但幸福也是有代價(jià)的,因?yàn)榭偸菚?huì)有些垃圾對(duì)象陰差陽(yáng)錯(cuò)的避開(kāi)GC算法,這一現(xiàn)象也稱之為內(nèi)存泄漏,所以只有掌握了GC機(jī)制才能避免寫(xiě)出內(nèi)存泄漏的程序。
2. 兩種回收機(jī)制
2.1 引用計(jì)數(shù)
什么是引用計(jì)數(shù)呢?打個(gè)比方A a = new A(),代碼中 A 對(duì)象被引用 a 所持有,此時(shí)引用計(jì)數(shù)就會(huì) +1 ,如果 a 將引用置為 null 即a = null此時(shí)對(duì)象 A 的引用計(jì)數(shù)就會(huì)變?yōu)?0 ,GC算法檢測(cè)到 A 對(duì)象引用計(jì)數(shù)為 0 就會(huì)將其回收。很簡(jiǎn)單,但引用計(jì)數(shù)存在一定弊端
場(chǎng)景如下:
A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;
執(zhí)行完上述代碼后 A 和 B 對(duì)象會(huì)被回收嗎?看似引用都已經(jīng)置為 null ,但實(shí)際上 a 和 b 的 next 分別持有對(duì)方引用,形成了一種相互持有引用的局面,導(dǎo)致A 和 B 即使成了垃圾對(duì)象且不能被回收。有些同學(xué)可能會(huì)說(shuō),內(nèi)存泄漏太容易看出來(lái)了, a 和 b 置空前將各自的 next 置為空不就完了。嗯,這樣說(shuō)沒(méi)錯(cuò),但是在實(shí)際業(yè)務(wù)中面對(duì)龐大的業(yè)務(wù)邏輯內(nèi)存泄漏是很難一眼看出的。所以JVM在后來(lái)摒棄了引用計(jì)數(shù),采用了可達(dá)性分析。
2.2 可達(dá)性分析
可達(dá)性分析其實(shí)是數(shù)學(xué)中的一個(gè)概念,在JVM中,會(huì)將一些特殊的引用作為 GcRoot ,如果通過(guò) GcRoot 可以訪達(dá)的對(duì)象不會(huì)被當(dāng)作垃圾對(duì)象。換種方式說(shuō)就是,一個(gè)對(duì)象被 GcRoot 直接 或 間接持有,那么該對(duì)象就不會(huì)被當(dāng)作垃圾對(duì)象。用一張圖表示大概就是這個(gè)樣子:
圖中A、B、C、D可以被 GcRoot 訪達(dá),所以不會(huì)被回收。E、F不能被 GcRoot 訪達(dá),所以會(huì)被標(biāo)記為垃圾對(duì)象。最典型的是G、H,雖說(shuō)相互引用,但不能被 GcRoot 訪達(dá),所以也會(huì)被標(biāo)記為垃圾對(duì)象。綜上所述: 可達(dá)性分析 可以解決 引用計(jì)數(shù) 中 對(duì)象相互引用 不能被回收的問(wèn)題。
什么類(lèi)型的引用可作為 GcRoot 呢。 大概有如下四種:
- 棧中局部變量
- 方法區(qū)中靜態(tài)變量
- 方法區(qū)中常量
- 本地方法棧JNI的引用對(duì)象
注意點(diǎn)
千萬(wàn)不要把引用和對(duì)象兩個(gè)概念混淆,對(duì)象是實(shí)實(shí)在在存在于內(nèi)存中的,而引用只是一個(gè)變量/常量并持有對(duì)象在內(nèi)存中的地址指。
下面我來(lái)通過(guò)一些代碼來(lái)驗(yàn)證幾種 GcRoot
局部變量
筆者是用Android代碼進(jìn)行調(diào)試,不懂Android的同學(xué)把onCreate視為main方法即可。
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
method();
}
private void method(){
Log.i("test","method start");
A a = new A();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.i("test","method end");
}
class A{
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize A");
}
}
}
提示
- 在java中一個(gè)對(duì)象被回收會(huì)調(diào)用其
finalize方法- JVM中垃圾回收是在一個(gè)單獨(dú)線程進(jìn)行。為了更好的驗(yàn)證效果,在此加2000毫秒延時(shí)
打印結(jié)果如下:
17:58:57.526 method start
17:58:59.526 method end
17:58:59.591 finalize A
method 方法執(zhí)行時(shí)間是2000毫秒,對(duì)象A 在 method 方法結(jié)束立即被回收。所以可以認(rèn)定棧中局部變量可作為 GcRoot
本地方法區(qū)靜態(tài)變量
public class MyApp extends Application {
private static A a;
@Override
public void onCreate() {
super.onCreate();
Log.i("test","onCreate");
a = new A();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
a = null;
Log.i("test","a = null");
}
}
打印結(jié)果如下:
18:12:35.988 a = new A()
18:12:38.028 a = null
18:12:38.096 finalize A
創(chuàng)建一個(gè) A 對(duì)象賦值給靜態(tài)變量 a , 2000毫秒后將靜態(tài)變量 a 置為空。通過(guò)日志可以看出對(duì)象 A 在靜態(tài)變量 a 置空后被立即回收。所以可以認(rèn)定靜態(tài)變量可作為 GcRoot
方法區(qū)常量與靜態(tài)變量驗(yàn)證過(guò)程完全一致,關(guān)于native 驗(yàn)證過(guò)程比較復(fù)雜,感興趣的同學(xué)可自行驗(yàn)證。
驗(yàn)證成員變量是否可作為 GcRoot
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
A a = new A();
B b = new B();
a.b = b;
a = null;
}
class A{
B b;
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize A");
}
}
class B{
@Override
protected void finalize() throws Throwable {
Log.i("test","finalize B");
}
}
}
打印結(jié)果如下:
13:14:58.999 finalize A
13:14:58.999 finalize B
通過(guò)日志可以看出,A、B 兩個(gè)對(duì)象都被回收。雖然 B 對(duì)象被 A 對(duì)象中的 b 引用所持有,但成員變量不能被作為 GcRoot, 所以B 對(duì)象不可達(dá),進(jìn)而會(huì)被當(dāng)作垃圾。
3. 回收算法
上一小結(jié)描述了 GC 機(jī)制,但具體實(shí)現(xiàn)還是要靠算法,下面我簡(jiǎn)單描述一下幾種常見(jiàn)的 GC算法。
3.1. 標(biāo)記清除算法
獲取所有的 GcRoot 遍歷內(nèi)存中所有的對(duì)象,如果可以被 GcRoot 就加個(gè)標(biāo)記,剩下所有的對(duì)象都將視為垃圾被清除。
- 優(yōu)點(diǎn):實(shí)現(xiàn)簡(jiǎn)單,執(zhí)行效率高
- 缺點(diǎn):容易產(chǎn)生 內(nèi)存碎片(可用內(nèi)存分布比較分散),如果需要申請(qǐng)大塊連續(xù)內(nèi)存可能會(huì)頻繁觸發(fā) GC
3.2. 復(fù)制算法
將內(nèi)存分為兩塊,每次只是用其中一塊。首先遍歷所有對(duì)象,將可用對(duì)象復(fù)制到另一塊內(nèi)存中,此時(shí)上一塊內(nèi)存可視為全是垃圾,清理后將新內(nèi)存塊置為當(dāng)前可用。如此反復(fù)進(jìn)行
- 優(yōu)點(diǎn):解決了內(nèi)存碎片的問(wèn)題
- 缺點(diǎn):需要按順序分配內(nèi)存,可用內(nèi)存變?yōu)樵瓉?lái)的一半。
3.3. 標(biāo)記壓縮算法
獲取所有的 GcRoot , GcRoot 開(kāi)始從遍歷內(nèi)存中所有的對(duì)象,將可用對(duì)象壓縮到另一端,再將垃圾對(duì)象清除。實(shí)則是犧牲時(shí)間復(fù)雜度來(lái)降低空間復(fù)雜度
- 優(yōu)點(diǎn):解決了標(biāo)記清除的 內(nèi)存碎片 ,也不需要復(fù)制算法中的 內(nèi)存分塊
- 缺點(diǎn):仍需要將對(duì)象進(jìn)行移動(dòng),執(zhí)行效率略低。
4. 分代回收策略
在JVM中 垃圾回收器 是很繁忙的,如果一個(gè)對(duì)象存活時(shí)間較長(zhǎng),避免重復(fù) 創(chuàng)建/回收 給 垃圾回收器 進(jìn)一步造成負(fù)擔(dān),能不能犧牲點(diǎn)內(nèi)存把它緩存起來(lái)? 答案是肯定的。JVM制定了 分代回收策略 為每個(gè)對(duì)象設(shè)置生命周期
,堆內(nèi)存會(huì)劃分不同的區(qū)域,來(lái)存儲(chǔ)各生命周期的對(duì)象。一般情況下對(duì)象的生命周期有 新生代、老年代、永久代(java 8已廢棄)。
4.1. 新生代
首先來(lái)看新生代內(nèi)存結(jié)構(gòu)示意圖:
按照8:1:1將新生代內(nèi)存分為 Eden、SurvivorA、SurvivorB
新生代內(nèi)存工作流程:
- 當(dāng)一個(gè)對(duì)象剛被創(chuàng)建時(shí)會(huì)放到 Eden 區(qū)域,當(dāng) Eden 區(qū)域即將存滿時(shí)做一次垃圾回收,將當(dāng)前存活的對(duì)象復(fù)制到 SurvivorA ,隨后將 Eden 清空
- 當(dāng)Eden 下一次存滿時(shí),再做一次垃圾回收,先將存活對(duì)象復(fù)制到 SurvivorB ,再把 Eden 和 SurvivorA 所有對(duì)象進(jìn)行回收,
- 當(dāng)Eden 再一次存滿時(shí),再做一次垃圾回收,將存活對(duì)象復(fù)制到 SurvivorA,再把 Eden 和 SurvivorB對(duì)象進(jìn)行回收。如此反復(fù)進(jìn)行大概 15 次,將最終依舊存活的對(duì)象放入到老年代區(qū)域。
新生代工作流程與 復(fù)制算法 應(yīng)用場(chǎng)景較為吻合,都是以復(fù)制為核心,所以會(huì)采用復(fù)制算法。
4.2. 老年代
根據(jù)對(duì) 上一小節(jié) 我們可以得知 當(dāng)一個(gè)對(duì)象存活時(shí)間較久會(huì)被存入到 老年代 區(qū)域。 老年代 區(qū)即將被存滿時(shí)會(huì)做一次垃圾回收,
所以 老年代 區(qū)域特點(diǎn)是存活對(duì)象多、垃圾對(duì)象少,采用標(biāo)記壓縮 算法時(shí)移動(dòng)少、也不會(huì)產(chǎn)生內(nèi)存碎片。所以老年代 區(qū)域可以選用 標(biāo)記壓縮 算法進(jìn)一步提升效率。
5. 四大引用
在我們開(kāi)發(fā)程序的過(guò)程中,避免不了會(huì)創(chuàng)建一些比較大的對(duì)象,比如Android中用于承載像素信息的Bitmap,使用稍有不當(dāng)就會(huì)造成內(nèi)存泄漏,如果存在大量類(lèi)似對(duì)象對(duì)內(nèi)存影響還是蠻大的。
為了盡可能避免上述情況的出現(xiàn),JVM為我們提供了四種對(duì)象引用方式:強(qiáng)引用、軟引用、弱引用、虛引用 供我們選擇,下面我用一張表格來(lái)做一下類(lèi)比
- 假設(shè)以下所述對(duì)象可被 GcRoot 訪達(dá)
| 引用類(lèi)型 | 回收時(shí)機(jī) |
|---|---|
| 強(qiáng)引用 | 絕不會(huì)被回收(默認(rèn)) |
| 軟引用 | 內(nèi)存不足時(shí)回收 |
| 弱引用 | 第一次觸發(fā)GC時(shí)就會(huì)被回收 |
| 虛引用 | 隨時(shí)都會(huì)被回收,不存在實(shí)際意義 |
參考文獻(xiàn):《Android 工程師進(jìn)階 34 講》 第二講
結(jié)語(yǔ)
文章從五個(gè)方面描述了 GC 機(jī)制。
- GC 機(jī)制的誕生是為了提升開(kāi)發(fā)者的效率
- 可達(dá)性分析 解決的 引用計(jì)數(shù) 相互引用的問(wèn)題
- 不同場(chǎng)景 運(yùn)用不同 GC 算法可以提升效率
- 分代回收策略 進(jìn)一步提升 GC 效率
- 巧妙運(yùn)用 四大引用 可以一定程度解決 內(nèi)存泄漏