深入理解JVM虚拟机读书笔记——垃圾回收算法
注:本文參考自周志明老師的著作《深入理解Java虛擬機(第3版)》,相關電子書可以關注WX公眾號,回復 001 獲取。
1. 如何判斷對象已死?
JVM 中判斷對象是否已經死亡的算法主要有 2 種:引用計數法、可達性分析法。
1.1 引用計數法
- 如果一個對象被其他變量所引用,則讓該對象的引用計數+1,如果該對象被引用2次則其引用計數為2,依次類推。
- 某個變量不再引用該對象,則讓該對象的引用計數-1,當該對象的引用計數變為0時,則表示該對象沒用被其他變量所引用,這時候該對象就可以被作為垃圾進行回收。
引用計數法弊端:循環引用時,兩個對象的引用計數都為1,導致兩個對象都無法被釋放回收。最終就會造成內存泄漏!
1.2 可達性分析算法
可達性分析算法就是JVM中判斷對象是否是垃圾的算法:該算法首先要確定GC Root(根對象,就是肯定不會被當成垃圾回收的對象)。
在垃圾回收之前,JVM會先對堆中的所有對象進行掃描,判斷每一個對象是否能被GC Root直接或者間接的引用,如果能被根對象直接或間接引用則表示該對象不能被垃圾回收,反之則表示該對象可以被回收:
- JVM中的垃圾回收器通過可達性分析來探索所有存活的對象。
- 掃描堆中的對象,看能否沿著GC Root為起點的引用鏈找到該對象,如果找不到,則表示可以回收,否則就可以回收。
- **在Java技術體系里面,固定可作為GC Roots的對象包括以下幾種 **:
- 虛擬機棧(棧幀中的本地變量表)中引用的對象,譬如各個線程被調用的方法堆棧中使用到的參數、局部變量、臨時變量等。
- 在方法區中類靜態屬性引用的對象,譬如Java類的引用類型靜態變量。
- 在方法區中常量引用的對象,譬如字符串常量池(String Table)里的引用。
- 在本地方法棧中JNI(即通常所說的Native方法)引用的對象。
- 所有被同步鎖(synchronized關鍵字)持有的對象。
- Java虛擬機內部的引用,如基本數據類型對應的Class對象,一些常駐的異常對象(比如 NullPointExcepiton、OutOfMemoryError)等,還有系統類加載器。
1.3 Java中的五種引用
強引用
上圖實心線表示強引用:比如,new 一個對象M,將對象M通過=(賦值運算符),賦值給某個變量m,則變量m就強引用了對象M。
強引用的特點:只要沿著GC Root的引用鏈能夠找到該對象,就不會被垃圾回收;只有當GC Root都不引用該對象時,才會回收強引用對象。
- 如上圖B、C對象都不引用A1對象時,A1對象才會被回收。
軟引用
上圖中寬虛線所表示的就是軟引用:
軟引用的特點:當GC Root指向軟引用對象時,若內存不足,則會回收軟引用所引用的對象。
- 如上圖如果B對象不再引用A2對象且內存不足時,軟引用所引用的A2對象就會被回收。
軟引用的使用:
public class Demo1 {public static void main(String[] args) {final int _4M = 4*1024*1024;// 軟引用對象內部包裝new byte[_4M]對象SoftReference<byte[]> ref= new SoftReference<>(new byte[_4M]);// 這時List 跟 SoftReference之間是強引用,SoftReference 跟 byte[] 之間是軟引用List 跟 <SoftReference<byte[]>> list = new ArrayList<>();} }如果在垃圾回收時發現內存不足,在回收軟引用所指向的對象時,軟引用本身不會被清理。
如果想要清理軟引用,需要使用引用隊列:
public class Demo04 {final static int _4M = 4 * 1024 * 1024;public static void main(String[] args) {// List和SoftReference是強引用,而SoftReference和byte數組則是軟引用List<SoftReference<byte[]>> list = new ArrayList<>();// 引用隊列,用于移除引用為空的軟引用對象ReferenceQueue<byte[]> queue = new ReferenceQueue<>();for (int i = 0; i < 5; i++) {// 關聯引用隊列,當軟引用所關聯的 byte[]被回收時,軟引用自己會假如到queue中去SoftReference<byte[]> ref = new SoftReference<>(new byte[_4M], queue);System.out.println(ref.get());list.add(ref);System.out.println(list.size());}// 遍歷,從引用隊列中獲取無用的軟引用對象,并移除Reference<? extends byte[]> poll = queue.poll();while (poll != null) {// 引用隊列不為空,則從集合中移除該元素list.remove(poll);// 移動到引用隊列中的下一個元素poll = queue.poll();}System.out.println("==========================");for (SoftReference<byte[]> reference : list) {System.out.println(reference.get());}} }**大概思路為:**查看引用隊列中有無軟引用,如果有,則將該軟引用從存放它的集合中移除(這里為一個list集合)。
弱引用
只有當弱引用引用該對象,在垃圾回收時,無論內存是否充足,都會回收弱引用所引用的對象。
- 如上圖如果B對象不再引用A3對象,則A3對象會被回收。
弱引用的使用和軟引用類似,只是將 SoftReference 換為了 WeakReference。
虛引用
當引用的對象ByteBuffer被垃圾回收以后,虛引用對象Cleaner就會被放入引用隊列中:
然后調用Cleaner的clean方法(Unsafe.freeMemory())來釋放直接內存:
- 虛引用的一個體現是釋放直接內存所分配的內存,當引用的對象ByteBuffer被垃圾回收以后,虛引用對象Cleaner就會被放入引用隊列中,然后調用Cleaner的clean方法來釋放直接內存。
- 如上圖,B對象不再引用ByteBuffer對象,ByteBuffer就會被回收。但是直接內存中的內存還未被回收。這時需要將虛引用對象Cleaner放入引用隊列中,然后調用它的clean方法來釋放直接內存。
終結器引用
所有的類都繼承自Object類,Object類有一個finalize()方法。當某個對象不再被其他的對象所引用時,會先將終結器引用對象放入引用隊列中,然后根據終結器引用對象找到它所引用的對象,然后調用該對象的finalize()方法。調用以后,該對象就可以被垃圾回收了。
- 如上圖,B對象不再引用A4對象。這是終結器對象就會被放入引用隊列中,引用隊列會根據它,找到它所引用的對象。然后調用被引用對象的finalize()方法。調用以后,該對象就可以被垃圾回收了。
引用隊列
- 軟引用和弱引用可以配合引用隊列(也可以不配合):
- 在弱引用和虛引用所引用的對象被回收以后,會將這些引用放入引用隊列中,方便一起回收這些軟/弱引用對象。
- 虛引用和終結器引用必須配合引用隊列:
- 虛引用和終結器引用在使用時會關聯一個引用隊列。
1.4 回收方法區
方法區的垃圾收集主要回收兩部分內容:廢棄的常量和不再使用的類型。舉個常量池中字面量回收的例子,假如一個字符串“java”曾經進入常量池中,但是當前系統又沒有任何一個字符串對象的值是“java”,換句話說,已經沒有任何字符串對象引用常量池中的“java”常量,且虛擬機中也沒有其他地方引用這個字面量。如果在這時發生內存回收,而且垃圾收集器判斷確有必要的話,這個“java”常量就將會被系統清理出常量池。常量池中其他類(接口)、方法、字段的符號引用也與此類似。
判定一個常量是否“廢棄”需要同時滿足下面三個條件:
- 該類所有的實例都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實例。
- 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如 OSGi、JSP 的重加載等,否則通常是很難達成的。
- 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。
2. 垃圾回收算法
在Java堆劃分出不同的區域之后,垃圾收集器才可以每次只回收其中某一個或者某些部分的區域——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收類型的劃分;也才能夠針對不同的區域安排與里面存儲對象存亡特征相匹配的垃圾收集算法——因而發展出了“標記-復制算法”“標記-清除算法”“標記-整理算法”等針對性的垃圾收集算法。
四種GC概念的介紹:
■ 新生代收集(Minor GC/Young GC):指目標只是新生代的垃圾收集。
■ 老年代收集(Major GC/Old GC):指目標只是老年代的垃圾收集。目前只有CMS收集器會有單獨收集老年代的行為。
■ 混合收集(Mixed GC):指目標是收集整個新生代以及部分老年代的垃圾收集。目前只有G1收集器會有這種行為。
■ 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集。
下面逐個介紹下4種回收算法:
2.1 標記-清除
定義:標記清除算法顧名思義,是指在虛擬機執行垃圾回收的過程中,先采用標記算法確定可回收對象,然后垃圾收集器根據標識,清除相應的內容,給堆內存騰出相應的空間。
- 這里的騰出內存空間并不是將內存空間的字節清 0,而是記錄下這段內存的起始結束地址,下次分配內存的時候,會直接覆蓋這段內存。
缺點:(容易產生大量的內存碎片,可能無法滿足大對象的內存分配,一旦導致無法分配對象,那就會導致jvm啟動gc)。
-
第一個是執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過
程的執行效率都隨對象數量增長而降低;
-
第二個是內存空間的碎片化問題,標記、清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致當以后在程序運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
標記-清除算法的執行過程(書中配圖):
2.2 標記-整理
標記-整理:會將不被GC Root引用的對象回收,清除其占用的內存空間。然后整理剩余的對象,可以有效避免因內存碎片而導致的問題,但是牽扯到對象的整理移動,需要消耗一定的時間,所以效率較低。
標記-整理算法的執行過程(書中配圖):
2.3 標記-復制
當需要回收對象時,先將GC Root直接引用的的對象(不需要回收)放入TO中:
然后清除FROM中的需要回收的對象:
最后 交換 FROM 和 TO 的位置:(FROM換成TO,TO換成FROM)
復制算法:將內存分為等大小的兩個區域,FROM和TO(TO中為空)。先將被GC Root引用的對象從FROM放入TO中,再回收不被GC Root引用的對象。然后交換FROM和TO。這樣也可以避免內存碎片的問題,但是會占用雙倍的內存空間。
標記-復制算法的執行過程(書中配圖):
2.4 分代回收
把分代收集理論具體放到現在的商用Java虛擬機里,設計者一般至少會把Java堆劃分為新生代(Young Generation)和老年代(Old Generation)兩個區域,顧名思義,在新生代中,每次垃圾收集時都發現有大批對象死去,而每次回收后存活的少量對象,將會逐步晉升到老年代中存放。
長時間使用的對象放在老年代中(長時間回收一次,回收花費時間久),用完即可丟棄的對象放在新生代中(頻繁需要回收,回收速度相對較快),如下圖所示:
回收流程
新創建的對象都被放在了新生代的伊甸園中:
當伊甸園中的內存不足時,就會進行一次垃圾回收,這時的回收叫做 Minor GC:
Minor GC 會將伊甸園和幸存區FROM仍需要存活的對象先復制到 幸存區 TO中, 并讓其壽命加1,再交換FROM和TO。
伊甸園中不需要存活的對象清除:
交換FROM和TO:
同理,繼續向伊甸園新增對象,如果滿了,則進行第二次Minor GC:
流程相同,仍需要存活的對象壽命+1:(下圖中FROM中壽命為1的對象是新從伊甸園復制過來的,而不是原來幸存區FROM中的壽命為1的對象,這里只是靜態圖片不好展示,只能用文字描述了)
再次創建對象,若新生代的伊甸園又滿了,則會再次觸發 Minor GC(會觸發 stop the world, 暫停其他用戶線程,只讓垃圾回收線程工作),這時不僅會回收伊甸園中的垃圾,還會回收幸存區中的垃圾,再將活躍對象復制到幸存區TO中。回收以后會交換兩個幸存區,并讓幸存區中的對象壽命加1!
如果幸存區中的對象的壽命超過某個閾值(最大為15,4bit),就會被放入老年代中:
如果新生代老年代中的內存都滿了,就會先觸發Minor Gc,再觸發Full GC,掃描新生代和老年代中所有不再使用的對象并回收:
分代回收小結:
- 新創建的對象首先會被分配在伊甸園區域。
- 新生代空間不足時,觸發Minor GC,伊甸園和 FROM幸存區需要存活的對象會被COPY到TO幸存區中,存活的對象壽命+1,并且交換FROM和TO。
- Minor GC會引發 Stop The World:暫停其他用戶的線程,等待垃圾回收結束后,用戶線程才可以恢復執行。
- 當對象壽命超過閾值15時,會晉升至老年代。
- 如果新生代、老年代中的內存都滿了,就會先觸發Minor GC,再觸發Full GC,掃描新生代和老年代中所有不再使用的對象并回收。
后續會陸續更新,這本書的筆記記的差不多了,排版和格式需要花時間整理,文章都會同步到公眾號上,也歡迎大家通過公眾號加入我的交流qun互相討論jvm這塊的知識內容!
總結
以上是生活随笔為你收集整理的深入理解JVM虚拟机读书笔记——垃圾回收算法的全部內容,希望文章能夠幫你解決所遇到的問題。