1. 程式人生 > >【深入理解JVM】:垃圾收集演算法

【深入理解JVM】:垃圾收集演算法

垃圾收集演算法主要有以下幾種:標記-清除演算法(mark-sweep)、複製演算法(copying)和標記-整理演算法(mark-compact)。
標記-清除演算法:
演算法的執行過程與名字一樣,先標記所有需要回收的物件,在標記完成後統一回收所有被標記的物件。該演算法有兩個問題:
標記和清除過程效率不高。主要由於垃圾收集器需要從GC Roots根物件中遍歷所有可達的物件(個人理解可達物件就是存在被引用的物件),並給這些物件加上一個標記,表明此物件在清除的時候被跳過,然後在清除階段,垃圾收集器會從Java堆中從頭到尾進行遍歷,如果有物件沒有被打上標記,那麼這個物件就會被清除。顯然遍歷的效率是很低的
會產生很多不連續的空間碎片,所以可能會導致程式執行過程中需要分配較大的物件的時候,無法找到足夠的記憶體而不得不提前出發一次垃圾回收。
複製演算法:


複製演算法是為了解決標記-清除演算法的效率問題的,其思想如下:將可用記憶體的容量分為大小相等的兩塊,每次只使用其中的一塊,當這一塊記憶體使用完了,就把存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間清理掉。
優點:每次都是對整個半區進行記憶體回收,記憶體分配時也就不用考慮記憶體碎片等複雜情況,只要移動堆頂指標,按順序分配記憶體即可,實現簡單,執行高效。
缺點:演算法的代價是將記憶體縮小為了原來的一半,未免太高了一點。
現在的商業虛擬機器都採用這種收集演算法來回收新生代,新生代中的物件98%是“朝生夕死”的,所以並不需要按照1∶1的比例來劃分記憶體空間,而是將記憶體分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden和其中一塊Survivor。
當回收時,將Eden和Survivor中還存活著的物件一次性地複製到另外一塊Survivor空間上,最後清理掉Eden和剛才用過的Survivor空間。HotSpot虛擬機器預設Eden和Survivor的大小比例是8∶1,也就是每次新生代中可用記憶體空間為整個新生代容量的90%,只有10%的記憶體會被“浪費”。
當然,90%的物件可回收只是一般場景下的資料,我們沒有辦法保證每次回收都只有不多於10%的物件存活,當Survivor空間不夠用時(例如,存活的物件需要的空間大於剩餘一塊Survivor的空間),需要依賴其他記憶體(這裡指老年代)進行分配擔保(Handle Promotion)。
標記-整理演算法:

複製收集演算法在物件存活率較高時就要進行較多的複製操作,效率將會變低。更關鍵的是,如果不想浪費50%的空間,就需要有額外的空間進行分配擔保,以應對被使用的記憶體中所有物件都100%存活的極端情況,所以在老年代一般不能直接選用這種演算法。
與標記-清除演算法過程一樣,只不過在標記後不是對未標記的記憶體區域進行清理,而是讓所有的存活物件都向一端移動,然後清理掉邊界外的記憶體。該方法主要用於老年代。
分代收集演算法:
目前商用虛擬機器都使用“分代收集演算法”,所謂分代就是根據物件的生命週期把記憶體分為幾塊,一般把Java堆中分為新生代和老年代,這樣就可以根據物件的“年齡”選擇合適的垃圾回收演算法。
新生代:“朝生夕死”,存活率低,使用複製演算法。
老年代:存活率較高,使用“標記-清除”演算法或者“標記-整理”演算法。
Hotspot的演算法實現:

GC Roots與GC停頓
我們回到標記-清除演算法,在清除階段,為了列舉未被標記的物件,所以需要從根節點(GC Roots)開始查詢引用鏈,這個過程會導致GC停頓,意思就是在GC的時候Java的執行執行緒都被停頓,好像被凍結在某一個時間點,也叫“Stop the world”。然而目前主流的Java虛擬機器都是用準確式GC(所謂準確式GC,即使虛擬機器知道記憶體中的某個位置的資料是什麼型別),當“Stop the world”的時候並不需要檢查所有的引用位置,虛擬機器通過使用OopMap這個資料結構知道哪些地方存放著物件的引用。
類載入完成後,HotSpot將物件內什麼偏移量上是什麼型別的資料計算出來;在JIT編譯過程中,在特定的位置記錄下棧和暫存器(程式計數器)中哪些位置是引用。
安全點
使用OopMap,虛擬機器已經知道哪些位置存放著物件,從而GC Roots可以迅速的列舉可達物件的引用鏈。但是問題來了:是不是需要對所有的指令都使用OopMap呢?答案是否定的。實際上,虛擬機器只在“特定的位置”記錄了物件的引用資訊,比如我們使用方法呼叫或者迴圈的時候,就會設定這樣的位置,如果越過這個位置的繼續執行指令,然而程式是不允許因為指令流長度太長而執行過長時間,所以這個“特定位置“就成為了程式是否具有長時間執行的分界點。這個”特定的位置“也稱為安全點(SafePoint)。
主動式中斷執行緒
現在虛擬機器有了安全點,於是只會到安全點尋找物件的引用資訊,並且在安全點暫停Java執行執行緒,然而還有一個問題如何在GC發生時讓所有執行緒(不包括JNI呼叫的執行緒)都“跑”到最近的安全點上再停頓下來?
在Hotspot使用主動式中斷來中斷執行執行緒,其思想如下:當GC的時候不需要直接對執行緒操作以中斷執行緒,僅僅是設定一個標誌,然後讓執行執行緒去輪詢這個標誌,發現中斷標誌為真的時候就自己中斷執行緒。需要注意的是,輪詢標誌的地方與安全點的位置是重合的,另外再加上建立物件需要分配的地方。
安全區域
現在通過GC Roots和安全點,程式能夠在不太長的時間就可以到達安全點,並暫停執行執行緒。那麼如果程式在阻塞(Blocked)或者睡眠(Sleep)的狀態的時候,執行執行緒如何中斷呢?
JVM設定了安全區域(Safe Region)。安全區域可以看成是被擴充套件了的安全點,是指在這個區域內,物件的引用關係不會發生改變,在這個區域內的任何地方開始GC都是安全的。
參考
1、周志明,深入理解Java虛擬機器:JVM高階特性與最佳實踐,機械工業出版社


作者:smile4lee
來源:CSDN
原文:https://blog.csdn.net/u011080472/article/details/51324103
版權宣告:本文為博主原創文章,轉載請附上博文連結!