JVM(一)---- 總結與專題目錄
JVM(二)----Java運行時數據區域
JVM(三)----垃圾收集算法及Safe Point介紹
JVM(四)----HotSpot的垃圾收集器與內存分配回收策略
JVM(五)----虛擬機類加載機制
本文的內容如下:
- 如何判斷對象是否存活
- 強軟弱虛引用
- 垃圾收集算法
- HotSpot的算法實現
- safe point 和safe region介紹
一、判斷對象是否存活(Which?)
垃圾收集器在對堆進行回收之前,第一件事情就是要確定這些對象之中哪些還存活著,哪些已經死去。
1.1.引用計數算法
每個對象有一個引用計數屬性,新增一個引用時計數加1,引用釋放時計數減1,計數為0時可以回收。此方法簡單,無法解決對象相互循環引用的問題。
1.2 可達性分析算法
此算法可以解決循環引用問題。從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的,是不可達對象。
在Java語言中,GC Roots包括:
虛擬機棧中引用的對象。
方法區中類靜態屬性實體引用的對象。
方法區中常量引用的對象。
本地方法棧中JNI引用的對象。
1.3 強、軟、弱、虛引用
在JDK1.2以前的版本中,當一個對象不被任何變量引用,那么程序就無法再使用這個對象。也就是說,只有對象處于可觸及狀態,程序才能使用它。這就像在日常生活中,從商店購買了某樣物品后,如果有用,就一直保留它,否則就把它扔到垃圾箱,由清潔工人收走。一般說來,如果物品已經被扔到垃圾箱,想再把它撿回來使用就不可能了。
但有時候情況并不這么簡單,你可能會遇到類似雞肋一樣的物品,食之無味,棄之可惜。這種物品現在已經無用了,保留它會占空間,但是立刻扔掉它也不劃算,因 為也許將來還會派用場。對于這樣的可有可無的物品,一種折衷的處理辦法是:如果家里空間足夠,就先把它保留在家里,如果家里空間不夠,即使把家里所有的垃圾清除,還是無法容納那些必不可少的生活用品,那么再扔掉這些可有可無的物品。
從JDK1.2版本開始,把對象的引用分為四種級別,從而使程序能更加靈活的控制對象的生命周期。這四種級別由高到低依次為:強引用、軟引用、弱引用和虛引用。
強引用:
平時我們編程的時候例如:Object object=new Object();那object就是一個強引用了。如果一個對象具有強引用,那就類似于必不可少的生活用品,垃圾回收器絕不會回收它。當內存空間不足,Java虛擬機寧愿拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具有強引用的對象來解決內存不足問題。
軟引用:
如果一個對象只具有軟引用,那就類似于可有可無的生活用品。如果內存空間足夠,垃圾回收器就不會回收它,如果內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就可以被程序使用。軟引用可用來實現內存敏感的高速緩存。 軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被垃圾回收,Java虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。
弱引用:
如果一個對象只具有弱引用,那就類似于可有可無的生活用品。弱引用與軟引用的區別在于:只具有弱引用的對象擁有更短暫的生命周期。在垃圾回收器線程掃描它 所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。不過,由于垃圾回收器是一個優先級很低的線程, 因此不一定會很快發現那些只具有弱引用的對象。 弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。
虛引用:
"虛引用"顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用并不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它就和沒有任何引用一樣,在任何時候都可能被垃圾回收。 虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在于:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象的內存之前,把這個虛引用加入到與之關聯的引用隊列中。程序可以通過判斷引用隊列中是否已經加入了虛引用,來了解被引用的對象是否將要被垃圾回收。程序如果發現某個虛引用已經被加入到引用隊列,那么就可以在所引用的對象的內存被回收之前采取必要的行動。
二、垃圾收集算法(How?)
知道了要收集那些垃圾對象后,怎么收集呢?這就需要一些垃圾收集算法了。
2.1 標記清除算法
最基本的收集算法“標記-清除”(Mark-Sweep)算法,算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成后統一回收所有被標記的對象,之所以說它是最基本的收集算法,是因為后續的收集算法都是基于這種思路并對其不足進行改進而得到的。
它的主要不足有兩個:一是效率問題,標記和清除效率都不高,二是空間問題,標記清除后會產生大量不連續的內存碎片,空間碎片太多可能會導致以后程序在運行過程中需要分配較大對象時,無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。執行過程如下:
2.2 復制算法
為了解決效率問題,一種稱為“復制”(Copying)的收集算法出現了,他將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這塊的內存用完了,就將還存活這的對象復制到另外一塊上面,然后再把已使用過的內存空間一次清理掉。這樣使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等復雜情況,只要移動堆頂指針,按順序分配內存即可,實現簡單,運行高效。只是這種算法的代價是將內存縮小為了原來的一半,未免太高了一點。現代的商業虛擬機都采用這種算法進行垃圾回收,但是不是分為大小相等的兩塊,而是分為一塊較大的Eden和兩塊較小的Survivor區域,每次使用一塊Eden和Survivor區域,把存活的復制到另外一塊Survivor區域,然后清理剛才使用的Eden和Survivor。HotSpot虛擬機默認Eden和Survivor的的大小比例是8:1.
2.3 標記整理算法
復制收集算法在對象存活率較高時就要進行較多的復制操作,效率將會變低。更關鍵的是如果不想浪費50%的空間就要使用額外的空間進行分配擔保(Handle Promotion當空間不夠時,需要依賴其他內存),以應對被使用的內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
對于“標記-整理”算法,標記過程仍與“標記-清除”算法一樣,但是后續步驟不是直接對可回收對象進行清理,而是讓所有的存活對象都向一端移動,然后直接清理掉端邊界以外的內存,”標記-整理“算法示意圖如下:
2.4 分代收集算法
當前的商業虛擬機的垃圾收集都是采用“分代收集”(Generational Collection)算法,這種算法并沒有什么新的思想,只是根據對象存活周期的不同將內存劃分為幾塊。一般是把堆劃分為新生代和老年代,這樣就可以根據各個年代的特點采用最適合的收集算法。在新生代中,每次垃圾收集時都發現有大批對象死去,只有少量存活,那就采用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。而老年代中因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用“標記-清理”或者“標記-整理”算法來進行回收。
三、HotSpot的算法實現
3.1 枚舉根節點
通過前面的介紹,我們知道,在分析一個對象是否是存活的時候有兩種方法,一個是引用計數法,引用計數法雖然實現簡單并且效率較高,但是很難解決循環引用。所以目前主流的虛擬機都是使用的是:可達性分析法。在可達性分析法中對象能被回收的條件是沒有引用來引用它,要做到這點就需要得到所有的GC Roots節點,來從GC Root來遍歷。可作為GC Root的主要是全局性引用(例如常量和靜態變量),與執行上下文(棧幀中的本地變量表)中。那么如何在這么多的全局變量和棧中的局部變量表中找到棧上的根節點呢?
在棧中只有一部分數據是Reference(引用)類型,那些非Reference的類型的數據對于找到根節點沒有什么用處,如果我們對棧全部掃描一遍這是相當浪費時間和資源的事情。
那怎么做可以減少回收時間呢?我們很自然的想到可以用空間來換取時間,我們可以在某個位置把棧上代表引用的位置記錄下來,這樣在gc發生的時候就不用全部掃描了,在HotSpot中使用的是一種叫做OopMap的數據結構來記錄的。對于OopMap可以簡單的理解成:它記錄著對象內什么偏移量上是什么類型的數據。
3.2 安全點(Safe point)
在OopMap的協助下,HotSpot可以快速且準確地完成GC Roots枚舉,但我們也不能為每一條指令都生成OopMap,那樣一方面會需要更多的空間來存放這些對象,另一方面效率也會低。所以,只會在特定的位置記錄這些信息,這些特定位置稱為安全點(Safe point),即程序執行時并非在所有地方都能停頓下來GC,只有在到達安全點時才能暫停。
從線程角度看,safepoint可以理解成是在代碼執行過程中的一些特殊位置,當線程執行到這些位置的時候,說明虛擬機當前的狀態是安全的,如果有需要,可以在這個位置暫停,比如發生GC時,需要暫停暫停所以活動線程,但是線程在這個時刻,還沒有執行到一個安全點,所以該線程應該繼續執行,到達下一個安全點的時候暫停,等待GC結束。
什么地方可以放safepoint?
- 循環的末尾 (防止大循環的時候一直不進入safepoint,而其他線程在等待它進入safepoint)
- 方法返回前
- 調用方法的call之后
- 拋出異常的位置
之所以選擇這些位置作為safepoint的插入點,主要的考慮是“避免程序長時間運行而不進入safepoint”,比如GC的時候必須要等到Java線程都進入到safepoint的時候VMThread才能開始執行GC,如果程序長時間運行而沒有進入safepoint,那么GC也無法開始,JVM可能進入到Freezen假死狀態。
知道了safe point的概念后,怎么使線程都“跑”到最近的安全點上停下來呢。這里有兩種方式:搶先式中斷和主動式中斷。
搶先式中斷
在GC發生時先中斷所有線程,如果線程不在安全點上,則啟動該線程使其執行到安全點后掛起。幾乎已沒有虛擬機只用此種方式主動式中斷
不需要直接對線程進行操作,僅僅簡單設置一個標識,在線程執行時主動輪詢這個標識,若中斷標識為真,線程自己中斷掛起。這個標識和安全點是重合的。
3.3安全區域(safe region)
上面的安全點檢查仿佛完全解決了如何進入GC的問題,但只有安全點還是不夠的,安全點只解決了那些在運行的程序,保證了他們可以運行到安全點并掛起,但如果有些線程此時并未執行,例如處于sleep或blocked狀態的線程,就無法響應JVM的中斷請求,這是就用到了安全區域。
定義:
安全區域是指在此區域內,對象的引用關系不會發生變化(即不會影響枚舉根節點)
原理:
當線程運行到安全區域時會將自己標識,在JVM準備進行GC時將視這些線程為安全的,不影響GC,當線程運行完畢要離開安全區域時,線程會檢查JVM是否在枚舉根節點,若是,則等待完成后再離開安全區域繼續執行。
補充:引自占小狼的文章
Safe Point對JVM性能有什么影響?
通過設置JVM參數 -XX:+PrintGCApplicationStoppedTime, 可以打出系統停止的時間,大概如下:
Total time for which application threads were stopped: 0.0051000 seconds
Total time for which application threads were stopped: 0.0041930 seconds
Total time for which application threads were stopped: 0.0051210 seconds
Total time for which application threads were stopped: 0.0050940 seconds
Total time for which application threads were stopped: 0.0058720 seconds
Total time for which application threads were stopped: 5.1298200 seconds
Total time for which application threads were stopped: 0.0197290 seconds
Total time for which application threads were stopped: 0.0087590 seconds
從上面數據可以發現,有一次暫停時間特別長,達到了5秒多,這在線上環境肯定是無法忍受的,那么是什么原因導致的呢?
一個大概率的原因是當發生GC時,有線程遲遲進入不到safepoint進行阻塞,導致其他已經停止的線程也一直等待,VM Thread也在等待所有的Java線程掛起才能開始GC,這里需要分析業務代碼中是否存在有界的大循環邏輯,可能在JIT優化時,這些循環操作沒有插入safepoint檢查。