前言
作為一名Java編程者资铡,想要往高級進階又沾,內(nèi)存管理往往是避不開的環(huán)節(jié)励幼,而垃圾回收 以下簡稱GC(Garbage Collection)
機制作為內(nèi)存管理最重要的一個部分灌曙,是我們必須要掌握的耗跛。今天就分享下我對 垃圾回收機制 與 分代回收策略 的理解.
目錄
1. 背景
-
2. 兩種回收機制
- 2.1. 引用計數(shù)
- 2.2. 可達性分析
-
3. 回收算法
- 3.1. 標記清除算法
- 3.2. 復(fù)制算法
- 3.3. 標記壓縮算法
-
4. 分代回收策略
- 4.1. 新生代
- 4.2. 老生代
5. 四大引用
1. 背景
一般來講裕照,在我們編程的過程中是會不斷的往內(nèi)存中寫入數(shù)據(jù)的,而這些數(shù)據(jù)用完了要及時從內(nèi)存中清理调塌,否則會引發(fā)OutOfMemory(內(nèi)存溢出)
晋南,所以每個編程者都必須遵從這一原則。聽說(我也不懂C語言~)在C語言階段羔砾,
垃圾是需要編程者自己手動回收的负间,而我們Javaer相對來說就要幸福多了,因為JVM存在GC機制
姜凄,也就是說JVM會幫我們自動清理垃圾政溃,但幸福也是有代價的,因為總是會有些垃圾對象陰差陽錯的避開GC算法
态秧,這一現(xiàn)象也稱之為內(nèi)存泄漏
董虱,所以只有掌握了GC機制才能避免寫出內(nèi)存泄漏的程序。
2. 兩種回收機制
2.1 引用計數(shù)
什么是引用計數(shù)呢申鱼?打個比方A a = new A()
愤诱,代碼中 A 對象被引用 a 所持有,此時引用計數(shù)就會 +1 捐友,如果 a 將引用置為 null 即a = null
此時對象 A 的引用計數(shù)就會變?yōu)?0 淫半,GC算法檢測到 A 對象引用計數(shù)為 0 就會將其回收。很簡單匣砖,但引用計數(shù)存在一定弊端
場景如下:
A a = new A();
B b = new B();
a.next = b;
b.next = a;
a = null;
b = null;
執(zhí)行完上述代碼后 A 和 B 對象會被回收嗎科吭?看似引用都已經(jīng)置為 null ,但實際上 a 和 b 的 next 分別持有對方引用脆粥,形成了一種相互持有引用的局面砌溺,導(dǎo)致A 和 B 即使成了垃圾對象且不能被回收。有些同學(xué)可能會說变隔,內(nèi)存泄漏太容易看出來了, a 和 b 置空前將各自的 next 置為空不就完了蟹倾。嗯匣缘,這樣說沒錯猖闪,但是在實際業(yè)務(wù)中面對龐大的業(yè)務(wù)邏輯內(nèi)存泄漏是很難一眼看出的。所以JVM在后來摒棄了引用計數(shù)肌厨,采用了可達性分析培慌。
2.2 可達性分析
可達性分析其實是數(shù)學(xué)中的一個概念,在JVM中柑爸,會將一些特殊的引用作為 GcRoot 吵护,如果通過 GcRoot 可以訪達的對象不會被當(dāng)作垃圾對象。換種方式說就是表鳍,一個對象被 GcRoot 直接 或 間接持有馅而,那么該對象就不會被當(dāng)作垃圾對象。用一張圖表示大概就是這個樣子:
圖中A譬圣、B、C、D可
以被 GcRoot 訪達砖瞧,所以不會被回收嘀趟。E、F
不能被 GcRoot 訪達绳姨,所以會被標記為垃圾對象登澜。最典型的是G、H
飘庄,雖說相互引用脑蠕,但不能被 GcRoot 訪達,所以也會被標記為垃圾對象竭宰。綜上所述: 可達性分析 可以解決 引用計數(shù) 中 對象相互引用 不能被回收的問題空郊。
什么類型的引用可作為 GcRoot 呢。 大概有如下四種:
- 棧中局部變量
- 方法區(qū)中靜態(tài)變量
- 方法區(qū)中常量
- 本地方法棧JNI的引用對象
注意點
千萬不要把引用和對象兩個概念混淆切揭,對象是實實在在存在于內(nèi)存中的狞甚,而引用只是一個變量/常量并持有對象在內(nèi)存中的地址指。
下面我來通過一些代碼來驗證幾種 GcRoot
局部變量
筆者是用Android代碼進行調(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中一個對象被回收會調(diào)用其
finalize
方法- JVM中垃圾回收是在一個單獨線程進行。為了更好的驗證效果孕豹,在此加2000毫秒延時
打印結(jié)果如下:
17:58:57.526 method start
17:58:59.526 method end
17:58:59.591 finalize A
method 方法執(zhí)行時間是2000毫秒涩盾,對象A 在 method 方法結(jié)束立即被回收。所以可以認定棧中局部變量可作為 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)建一個 A 對象賦值給靜態(tài)變量 a 励背, 2000毫秒后將靜態(tài)變量 a 置為空春霍。通過日志可以看出對象 A 在靜態(tài)變量 a 置空后被立即回收。所以可以認定靜態(tài)變量可作為 GcRoot
方法區(qū)常量與靜態(tài)變量驗證過程完全一致叶眉,關(guān)于native 驗證過程比較復(fù)雜址儒,感興趣的同學(xué)可自行驗證芹枷。
驗證成員變量是否可作為 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
通過日志可以看出,A莲趣、B 兩個對象都被回收鸳慈。雖然 B 對象被 A 對象中的 b 引用所持有,但成員變量不能被作為 GcRoot喧伞, 所以B 對象不可達走芋,進而會被當(dāng)作垃圾。
3. 回收算法
上一小結(jié)描述了 GC 機制潘鲫,但具體實現(xiàn)還是要靠算法翁逞,下面我簡單描述一下幾種常見的 GC算法。
3.1. 標記清除算法
獲取所有的 GcRoot 遍歷內(nèi)存中所有的對象次舌,如果可以被 GcRoot 就加個標記熄攘,剩下所有的對象都將視為垃圾被清除。
- 優(yōu)點:實現(xiàn)簡單彼念,執(zhí)行效率高
- 缺點:容易產(chǎn)生 內(nèi)存碎片(可用內(nèi)存分布比較分散)挪圾,如果需要申請大塊連續(xù)內(nèi)存可能會頻繁觸發(fā) GC
3.2. 復(fù)制算法
將內(nèi)存分為兩塊,每次只是用其中一塊逐沙。首先遍歷所有對象哲思,將可用對象復(fù)制到另一塊內(nèi)存中,此時上一塊內(nèi)存可視為全是垃圾吩案,清理后將新內(nèi)存塊置為當(dāng)前可用棚赔。如此反復(fù)進行
- 優(yōu)點:解決了內(nèi)存碎片的問題
- 缺點:需要按順序分配內(nèi)存,可用內(nèi)存變?yōu)樵瓉淼囊话搿?/li>
3.3. 標記壓縮算法
獲取所有的 GcRoot 徘郭, GcRoot 開始從遍歷內(nèi)存中所有的對象靠益,將可用對象壓縮到另一端,再將垃圾對象清除残揉。實則是犧牲時間復(fù)雜度來降低空間復(fù)雜度
- 優(yōu)點:解決了標記清除的 內(nèi)存碎片 胧后,也不需要復(fù)制算法中的 內(nèi)存分塊
- 缺點:仍需要將對象進行移動,執(zhí)行效率略低抱环。
4. 分代回收策略
在JVM中 垃圾回收器 是很繁忙的壳快,如果一個對象存活時間較長,避免重復(fù) 創(chuàng)建/回收 給 垃圾回收器 進一步造成負擔(dān)镇草,能不能犧牲點內(nèi)存把它緩存起來? 答案是肯定的眶痰。JVM制定了 分代回收策略 為每個對象設(shè)置生命周期
,堆內(nèi)存會劃分不同的區(qū)域竖伯,來存儲各生命周期的對象。一般情況下對象的生命周期有 新生代黔夭、老年代羽嫡、永久代(java 8已廢棄)本姥。
4.1. 新生代
首先來看新生代內(nèi)存結(jié)構(gòu)示意圖:
按照8:1:1將新生代內(nèi)存分為 Eden、SurvivorA杭棵、SurvivorB
新生代內(nèi)存工作流程:
- 當(dāng)一個對象剛被創(chuàng)建時會放到 Eden 區(qū)域,當(dāng) Eden 區(qū)域即將存滿時做一次垃圾回收先舷,將當(dāng)前存活的對象復(fù)制到 SurvivorA ,隨后將 Eden 清空
- 當(dāng)Eden 下一次存滿時滓侍,再做一次垃圾回收蒋川,先將存活對象復(fù)制到 SurvivorB ,再把 Eden 和 SurvivorA 所有對象進行回收撩笆,
- 當(dāng)Eden 再一次存滿時,再做一次垃圾回收夕冲,將存活對象復(fù)制到 SurvivorA,再把 Eden 和 SurvivorB對象進行回收泣栈。如此反復(fù)進行大概 15 次弥姻,將最終依舊存活的對象放入到老年代區(qū)域。
新生代工作流程與 復(fù)制算法 應(yīng)用場景較為吻合庭敦,都是以復(fù)制為核心,所以會采用復(fù)制算法颠悬。
4.2. 老年代
根據(jù)對 上一小節(jié) 我們可以得知 當(dāng)一個對象存活時間較久會被存入到 老年代 區(qū)域定血。 老年代 區(qū)即將被存滿時會做一次垃圾回收,
所以 老年代 區(qū)域特點是存活對象多灾票、垃圾對象少茫虽,采用標記壓縮 算法時移動少既们、也不會產(chǎn)生內(nèi)存碎片正什。所以老年代 區(qū)域可以選用 標記壓縮 算法進一步提升效率。
5. 四大引用
在我們開發(fā)程序的過程中婴氮,避免不了會創(chuàng)建一些比較大的對象,比如Android中用于承載像素信息的Bitmap
荣暮,使用稍有不當(dāng)就會造成內(nèi)存泄漏罩驻,如果存在大量類似對象對內(nèi)存影響還是蠻大的。
為了盡可能避免上述情況的出現(xiàn)砾跃,JVM為我們提供了四種對象引用方式:強引用爽哎、軟引用、弱引用课锌、虛引用 供我們選擇,下面我用一張表格來做一下類比
- 假設(shè)以下所述對象可被 GcRoot 訪達
引用類型 | 回收時機 |
---|---|
強引用 | 絕不會被回收(默認) |
軟引用 | 內(nèi)存不足時回收 |
弱引用 | 第一次觸發(fā)GC時就會被回收 |
虛引用 | 隨時都會被回收雏胃,不存在實際意義 |
參考文獻:《Android 工程師進階 34 講》 第二講
結(jié)語
文章從五個方面描述了 GC 機制志鞍。
- GC 機制的誕生是為了提升開發(fā)者的效率
- 可達性分析 解決的 引用計數(shù) 相互引用的問題
- 不同場景 運用不同 GC 算法可以提升效率
- 分代回收策略 進一步提升 GC 效率
- 巧妙運用 四大引用 可以一定程度解決 內(nèi)存泄漏