垃圾回收器算法
GC
垃圾回收概述
- 哪些內(nèi)存需要回收?
- 什么時候回收?
- 如何回收?
大廠面試題
螞蟻金服
百度
天貓
滴滴
京東
阿里
字節(jié)跳動
什么是垃圾?
為什么需要GC?
想要學(xué)習(xí)GC,首先需要理解為什么需要GC?
早期垃圾回收
Java 垃圾回收機(jī)制
自動內(nèi)存管理
官網(wǎng)介紹:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/toc.html
自動內(nèi)存管理的優(yōu)點(diǎn)
關(guān)于自動內(nèi)存管理的擔(dān)憂
應(yīng)該關(guān)心哪些區(qū)域的回收?

垃圾回收相關(guān)算法
標(biāo)記階段:引用計數(shù)算法
標(biāo)記階段的目的
垃圾標(biāo)記階段:主要是為了判斷對象是否存活
引用計數(shù)算法
循環(huán)引用

當(dāng)p的指針斷開的時候,內(nèi)部的引用形成一個循環(huán),計數(shù)器都還算1,無法被回收,這就是循環(huán)引用,從而造成內(nèi)存泄漏
證明:java使用的不是引用計數(shù)算法
/*** -XX:+PrintGCDetails* 證明:java使用的不是引用計數(shù)算法*/ public class RefCountGC {//這個成員屬性唯一的作用就是占用一點(diǎn)內(nèi)存private byte[] bigSize = new byte[5 * 1024 * 1024];//5MBObject reference = null;public static void main(String[] args) {RefCountGC obj1 = new RefCountGC();RefCountGC obj2 = new RefCountGC();obj1.reference = obj2;obj2.reference = obj1;obj1 = null;obj2 = null;//顯式的執(zhí)行垃圾回收行為//這里發(fā)生GC,obj1和obj2能否被回收?System.gc();} }
- 如果不小心直接把obj1.reference和obj2.reference置為null。則在Java堆中的兩塊內(nèi)存依然保持著互相引用,無法被回收
沒有進(jìn)行GC時
把下面的幾行代碼注釋掉,讓它來不及
System.gc();//把這行代碼注釋掉 HeapPSYoungGen total 38400K, used 14234K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)eden space 33280K, 42% used [0x00000000d5f80000,0x00000000d6d66be8,0x00000000d8000000)from space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)to space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)ParOldGen total 87552K, used 0K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)object space 87552K, 0% used [0x0000000081e00000,0x0000000081e00000,0x0000000087380000)Metaspace used 3496K, capacity 4498K, committed 4864K, reserved 1056768Kclass space used 387K, capacity 390K, committed 512K, reserved 1048576KProcess finished with exit code 0進(jìn)行GC
打開那行代碼的注釋
[GC (System.gc()) [PSYoungGen: 13569K->808K(38400K)] 13569K->816K(125952K), 0.0012717 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] [Full GC (System.gc()) [PSYoungGen: 808K->0K(38400K)] [ParOldGen: 8K->670K(87552K)] 816K->670K(125952K), [Metaspace: 3491K->3491K(1056768K)], 0.0051769 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] HeapPSYoungGen total 38400K, used 333K [0x00000000d5f80000, 0x00000000d8a00000, 0x0000000100000000)eden space 33280K, 1% used [0x00000000d5f80000,0x00000000d5fd34a8,0x00000000d8000000)from space 5120K, 0% used [0x00000000d8000000,0x00000000d8000000,0x00000000d8500000)to space 5120K, 0% used [0x00000000d8500000,0x00000000d8500000,0x00000000d8a00000)ParOldGen total 87552K, used 670K [0x0000000081e00000, 0x0000000087380000, 0x00000000d5f80000)object space 87552K, 0% used [0x0000000081e00000,0x0000000081ea7990,0x0000000087380000)Metaspace used 3498K, capacity 4498K, committed 4864K, reserved 1056768Kclass space used 387K, capacity 390K, committed 512K, reserved 1048576KProcess finished with exit code 01、從打印日志就可以明顯看出來,已經(jīng)進(jìn)行了GC
2、如果使用引用計數(shù)算法,那么這兩個對象將會無法回收。而現(xiàn)在兩個對象被回收了,說明Java使用的不是引用計數(shù)算法來進(jìn)行標(biāo)記的。
小結(jié)
- 手動解除:很好理解,就是在合適的時機(jī),解除引用關(guān)系。
- 使用弱引用weakref,weakref是Python提供的標(biāo)準(zhǔn)庫,旨在解決循環(huán)引用。
標(biāo)記階段:可達(dá)性分析算法
可達(dá)性分析算法:也可以稱為根搜索算法、追蹤性垃圾收集
可達(dá)性分析實(shí)現(xiàn)思路
- 所謂"GCRoots”根集合就是一組必須活躍的引用
- 其基本思路如下:

GC Roots可以是哪些元素?
- 比如:各個線程被調(diào)用的方法中使用到的參數(shù)、局部變量等。
- 比如:Java類的引用類型靜態(tài)變量
- 比如:字符串常量池(StringTable)里的引用
- 基本數(shù)據(jù)類型對應(yīng)的Class對象,一些常駐的異常對象(如:NullPointerException、OutofMemoryError),系統(tǒng)類加載器。

- 如果只針對Java堆中的某一塊區(qū)域進(jìn)行垃圾回收(比如:典型的只針對新生代),必須考慮到內(nèi)存區(qū)域是虛擬機(jī)自己的實(shí)現(xiàn)細(xì)節(jié),更不是孤立封閉的,這個區(qū)域的對象完全有可能被其他區(qū)域的對象所引用,這時候就需要一并將關(guān)聯(lián)的區(qū)域?qū)ο笠布尤隚C Roots集合中去考慮,才能保證可達(dá)性分析的準(zhǔn)確性。
小技巧
由于Root采用棧方式存放變量和指針,所以如果一個指針,它保存了堆內(nèi)存里面的對象,但是自己又不存放在堆內(nèi)存里面,那它就是一個Root。
注意
對象的 finalization 機(jī)制
finalize() 方法機(jī)制
對象銷毀前的回調(diào)函數(shù):finalize()
Object 類中 finalize() 源碼
// 等待被重寫 protected void finalize() throws Throwable { }生存還是死亡?
由于finalize()方法的存在,虛擬機(jī)中的對象一般處于三種可能的狀態(tài)。
具體過程
判定一個對象objA是否可回收,至少要經(jīng)歷兩次標(biāo)記過程:
通過 JVisual VM 查看 Finalizer 線程

代碼演示 finalize() 方法可復(fù)活對象
我們重寫 CanReliveObj 類的 finalize()方法,在調(diào)用其 finalize()方法時,將 obj 指向當(dāng)前類對象 this
/*** 測試Object類中finalize()方法,即對象的finalization機(jī)制。**/ public class CanReliveObj {public static CanReliveObj obj;//類變量,屬于 GC Root//此方法只能被調(diào)用一次@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調(diào)用當(dāng)前類重寫的finalize()方法");obj = this;//當(dāng)前待回收的對象在finalize()方法中與引用鏈上的一個對象obj建立了聯(lián)系}public static void main(String[] args) {try {obj = new CanReliveObj();// 對象第一次成功拯救自己obj = null;System.gc();//調(diào)用垃圾回收器System.out.println("第1次 gc");// 因?yàn)镕inalizer線程優(yōu)先級很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}System.out.println("第2次 gc");// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了obj = null;System.gc();// 因?yàn)镕inalizer線程優(yōu)先級很低,暫停2秒,以等待它Thread.sleep(2000);if (obj == null) {System.out.println("obj is dead");} else {System.out.println("obj is still alive");}} catch (InterruptedException e) {e.printStackTrace();}} }如果注釋掉finalize()方法
//此方法只能被調(diào)用一次@Overrideprotected void finalize() throws Throwable {super.finalize();System.out.println("調(diào)用當(dāng)前類重寫的finalize()方法");obj = this;//當(dāng)前待回收的對象在finalize()方法中與引用鏈上的一個對象obj建立了聯(lián)系}輸出結(jié)果:
第1次 gc obj is dead 第2次 gc obj is dead放開finalize()方法
輸出結(jié)果:
第1次 gc 調(diào)用當(dāng)前類重寫的finalize()方法 obj is still alive 第2次 gc obj is dead第一次自救成功,但由于 finalize() 方法只會執(zhí)行一次,所以第二次自救失敗
MAT與JProfiler的GC Roots溯源
MAT 介紹
1、雖然Jvisualvm很強(qiáng)大,但是在內(nèi)存分析方面,還是MAT更好用一些
2、此小節(jié)主要是為了實(shí)時分析GC Roots是哪些東西,中間需要用到一個dump的文件
獲取 dump 文件方式
方式一:命令行使用 jmap

方式二:使用JVisualVM
捕捉 dump 示例
使用JVisualVM捕捉 heap dump
代碼:
- numList 和 birth 在第一次捕捉內(nèi)存快照的時候,為 GC Roots
- 之后 numList 和 birth 置為 null ,對應(yīng)的引用對象被回收,在第二次捕捉內(nèi)存快照的時候,就不再是 GC Roots
如何捕捉堆內(nèi)存快照
1、先執(zhí)行第一步,然后停下來,去生成此步驟dump文件

2、 點(diǎn)擊【堆 Dump】

3、右鍵 --> 另存為即可

4、輸入命令,繼續(xù)執(zhí)行程序

5、我們接著捕獲第二張堆內(nèi)存快照

使用 MAT 查看堆內(nèi)存快照
1、打開 MAT ,選擇File --> Open File,打開剛剛的兩個dump文件,我們先打開第一個dump文件
點(diǎn)擊Open Heap Dump也行

2、選擇Java Basics --> GC Roots

3、第一次捕捉堆內(nèi)存快照時,GC Roots 中包含我們定義的兩個局部變量,類型分別為 ArrayList 和 Date,Total:21

4、打開第二個dump文件,第二次捕獲內(nèi)存快照時,由于兩個局部變量引用的對象被釋放,所以這兩個局部變量不再作為 GC Roots ,從 Total Entries = 19 也可以看出(少了兩個 GC Roots)

JProfiler GC Roots 溯源
1、在實(shí)際開發(fā)中,我們很少會查看所有的GC Roots。一般都是查看某一個或幾個對象的GC Root是哪個,這個過程叫GC Roots 溯源
2、下面我們使用使用 JProfiler 進(jìn)行 GC Roots 溯源演示
依然用下面這個代碼
public class GCRootsTest {public static void main(String[] args) {List<Object> numList = new ArrayList<>();Date birth = new Date();for (int i = 0; i < 100; i++) {numList.add(String.valueOf(i));try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("數(shù)據(jù)添加完畢,請操作:");new Scanner(System.in).next();numList = null;birth = null;System.out.println("numList、birth已置空,請操作:");new Scanner(System.in).next();System.out.println("結(jié)束");} }1、

2、


可以發(fā)現(xiàn)顏色變綠了,可以動態(tài)的看變化
3、右擊對象,選擇 Show Selection In Heap Walker,單獨(dú)的查看某個對象


4、選擇Incoming References,表示追尋 GC Roots 的源頭
點(diǎn)擊Show Paths To GC Roots,在彈出界面中選擇默認(rèn)設(shè)置即可



JProfiler 分析 OOM
這里是簡單的講一下,后面篇章會詳解
/*** -Xms8m -Xmx8m * -XX:+HeapDumpOnOutOfMemoryError 這個參數(shù)的意思是當(dāng)程序出現(xiàn)OOM的時候就會在當(dāng)前工程目錄生成一個dump文件*/ public class HeapOOM {byte[] buffer = new byte[1 * 1024 * 1024];//1MBpublic static void main(String[] args) {ArrayList<HeapOOM> list = new ArrayList<>();int count = 0;try{while(true){list.add(new HeapOOM());count++;}}catch (Throwable e){System.out.println("count = " + count);e.printStackTrace();}} }程序輸出日志
com.atguigu.java.HeapOOM java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid14608.hprof ... java.lang.OutOfMemoryError: Java heap spaceat com.atguigu.java.HeapOOM.<init>(HeapOOM.java:12)at com.atguigu.java.HeapOOM.main(HeapOOM.java:20) Heap dump file created [7797849 bytes in 0.010 secs] count = 6打開這個dump文件
1、看這個超大對象

2、揪出 main() 線程中出問題的代碼

清除階段:標(biāo)記-清除算法
垃圾清除階段
- 當(dāng)成功區(qū)分出內(nèi)存中存活對象和死亡對象后,GC接下來的任務(wù)就是執(zhí)行垃圾回收,釋放掉無用對象所占用的內(nèi)存空間,以便有足夠的可用內(nèi)存空間為新對象分配內(nèi)存。目前在JVM中比較常見的三種垃圾收集算法是
背景
標(biāo)記-清除算法(Mark-Sweep)是一種非常基礎(chǔ)和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出并并應(yīng)用于Lisp語言。
執(zhí)行過程
當(dāng)堆中的有效內(nèi)存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進(jìn)行兩項(xiàng)工作,第一項(xiàng)則是標(biāo)記,第二項(xiàng)則是清除
- 注意:標(biāo)記的是被引用的對象,也就是可達(dá)對象,并非標(biāo)記的是即將被清除的垃圾對象

標(biāo)記-清除算法的缺點(diǎn)
注意:何為清除?
這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放(也就是覆蓋原有的地址)。
關(guān)于空閑列表是在為對象分配內(nèi)存的時候提過:
- 采用指針碰撞的方式進(jìn)行內(nèi)存分配
- 虛擬機(jī)需要維護(hù)一個空閑列表
- 采用空閑列表分配內(nèi)存
清除階段:復(fù)制算法
背景
核心思想
將活著的內(nèi)存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內(nèi)存中的存活對象復(fù)制到未被使用的內(nèi)存塊中,之后清除正在使用的內(nèi)存塊中的所有對象,交換兩個內(nèi)存的角色,最后完成垃圾回收

新生代里面就用到了復(fù)制算法,Eden區(qū)和S0區(qū)存活對象整體復(fù)制到S1區(qū)
復(fù)制算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
缺點(diǎn)
復(fù)制算法的應(yīng)用場景

清除階段:標(biāo)記-壓縮算法
標(biāo)記-壓縮(或標(biāo)記-整理、Mark - Compact)算法
背景
執(zhí)行過程

標(biāo)記-壓縮算法與標(biāo)記-清除算法的比較
標(biāo)記-壓縮算法的優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
缺點(diǎn)
垃圾回收算法小結(jié)
對比三種清除階段的算法
| | 標(biāo)記清除 | 標(biāo)記整理 | 復(fù)制 |
| — | — | — | — |
| 速率 | 中等 | 最慢 | 最快 |
| 空間開銷 | 少(但會堆積碎片) | 少(不堆積碎片) | 通常需要活對象的2倍空間(不堆積碎片) |
| 移動對象 | 否 | 是 | 是 |
分代收集算法
Q:難道就沒有一種最優(yōu)的算法嗎?
A:無,沒有最好的算法,只有最合適的算法
為什么要使用分代收集算法
- 比如Http請求中的Session對象、線程、Socket連接,這類對象跟業(yè)務(wù)直接掛鉤,因此生命周期比較長。
- 但是還有一些對象,主要是程序運(yùn)行過程中生成的臨時變量,這些對象生命周期會比較短,比如:String對象,由于其不變類的特性,系統(tǒng)會產(chǎn)生大量的這些對象,有些對象甚至只用一次即可回收。
目前幾乎所有的GC都采用分代手機(jī)算法執(zhí)行垃圾回收的
在HotSpot中,基于分代的概念,GC所使用的內(nèi)存回收算法必須結(jié)合年輕代和老年代各自的特點(diǎn)。
- 年輕代特點(diǎn):區(qū)域相對老年代較小,對象生命周期短、存活率低,回收頻繁。
- 這種情況復(fù)制算法的回收整理,速度是最快的。復(fù)制算法的效率只和當(dāng)前存活對象大小有關(guān),因此很適用于年輕代的回收。而復(fù)制算法內(nèi)存利用率不高的問題,通過hotspot中的兩個survivor的設(shè)計得到緩解。
- 老年代特點(diǎn):區(qū)域較大,對象生命周期長、存活率高,回收不及年輕代頻繁。
- 這種情況存在大量存活率高的對象,復(fù)制算法明顯變得不合適。一般是由標(biāo)記-清除或者是標(biāo)記-清除與標(biāo)記-整理的混合實(shí)現(xiàn)。
- Mark階段的開銷與存活對象的數(shù)量成正比。
- Sweep階段的開銷與所管理區(qū)域的大小成正相關(guān)。
- Compact階段的開銷與存活對象的數(shù)據(jù)成正比。
增量收集算法和分區(qū)算法
增量收集算法
上述現(xiàn)有的算法,在垃圾回收過程中,應(yīng)用軟件將處于一種Stop the World的狀態(tài)。在Stop the World狀態(tài)下,應(yīng)用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應(yīng)用程序會被掛起很久,將嚴(yán)重影響用戶體驗(yàn)或者系統(tǒng)的穩(wěn)定性。為了解決這個問題,即對實(shí)時垃圾收集算法的研究直接導(dǎo)致了增量收集(Incremental Collecting)算法的誕生。
增量收集算法基本思想
增量收集算法的缺點(diǎn)
使用這種方式,由于在垃圾回收過程中,間斷性地還執(zhí)行了應(yīng)用程序代碼,所以能減少系統(tǒng)的停頓時間。但是,因?yàn)榫€程切換和上下文轉(zhuǎn)換的消耗,會使得垃圾回收的總體成本上升,造成系統(tǒng)吞吐量的下降。
分區(qū)算法
主要針對G1收集器來說的

寫在最后
注意,這些只是基本的算法思路,實(shí)際GC實(shí)現(xiàn)過程要復(fù)雜的多,目前還在發(fā)展中的前沿GC都是復(fù)合算法,并且并行和并發(fā)兼?zhèn)洹?/p>
總結(jié)
- 上一篇: python重复输入上面指令_stdin
- 下一篇: expressjs路由和Nodejs服务