12 JVM 垃圾回收(下)
Java 虛擬機器的堆劃分
Java 虛擬機器將堆劃分為新生代和老年代。其中新生代又被劃分為 Eden 區,以及兩個大小相同的 Survivor 區。
預設情況下,Java 虛擬機器採取一種動態分配的策略,根據物件生成的速率,以及 Survivor 區的使用情況動態調整 Eden 區和 Survivor 區的比例。也可以通過引數 -XX:SurvivorRatio 來固定這個比例。需要注意的是,其中一個 Survivor 區會一直為空,因此比例越低浪費的空間越高。
當呼叫 new 指令時,它會在 Eden 區中劃出一塊作為儲存物件的記憶體。由於堆記憶體是執行緒共享的,因此直接在這裡劃分空間是需要進行同步的。否則會出現兩個物件公用一段記憶體的事故。
Java 虛擬機器的解決方法是:每個執行緒可以向 Java 虛擬機器申請一段連續的記憶體,比如 2048 位元組,作為執行緒私有的 TLAB。這個操作需要加鎖,執行緒需要維護兩個重要的指標,一個指向 TLAB 中空餘記憶體的起始位置,一個則指向 TLAB 末尾。
然後通過 new 指令,便可以直接通過指標加法來實現,即把指向空餘記憶體位置的指標加上所請求的位元組數。如果加法後空餘記憶體指標的值扔小於或等於指向末尾的指標,則代表分配成功。否則,TLAB 以及沒有足夠的空間來滿足本次新建操作。這個時候,便需要當前執行緒重新申請新的 TLAB。
當 Eden 區的空間耗盡了,這個時候 Java 虛擬機器便會觸發一次 Minor GC,來收集新生代的垃圾。存活下來的物件,則會被送到 Survivor 區。當發生 MinorGC時,Eden 區和 from 指向的 Survivor 區中的存活物件會被複制到 to 指向的 Survivor 區中,然後交換 from 和 to 指標,以保證下一次 Minor GC 時,to 指向的 Survivor 區還是空的。
Java 虛擬機器會記錄 Survivor 區中的物件一共被來回複製了幾次。如果一個物件被複制 15 次,那麼該物件將被晉升至老年代。如果單個 Survivor 區已經被佔用了 50%,那麼較高複製次數的物件也會被晉升至老年代。
Minor GC 有一個問題,那就是老年代的物件可能引用新生代的物件。在標記存活物件的時候,我們需要掃描老年代中的物件。如果該物件擁有對新生代物件的引用,那麼這個引用也會被作為 GC Roots。這樣的話,就相當於進行了一次全堆掃描。
卡表
針對上述的問題,HotSpot 給出了一種解決方案叫做卡表。該技術將整個堆劃分為一個個大小為 512 位元組的卡,並且維護一個卡表,用來儲存每張卡的一個標示位。這個標示位代表對應的卡是否可能存在指向新生代物件的引用。如果可能存在,那麼我們就認為這張卡是髒的。
在進行 Minor GC 的時候,我們不用掃描整個老年代,而是在卡表中尋找髒卡,並將髒卡中的物件加入到 Minor GC 的 GC Roots 裡。當完成所有髒卡的掃描之後,Java 虛擬機器便會將所有髒卡的標示位清零。
上述總結介紹了用卡表這種方式解決全堆掃描效率低下的問題,置於如何標記髒卡,如何更新髒卡就不做深入總結了。
問答
Q:請問JVM分代收集新生代物件進入老年代,年齡為什麼是15而不是其他的?
HotSpot會在物件頭中的標記欄位裡記錄年齡,分配到的空間只有4位,最多隻能記錄到15
Q:GC ROOT到底指的是物件本身,還是引用?
嚴格來說應該是物件。像區域性變數中存放的引用只是導致物件成為GC roots的原因。我個人傾向於將這些引用作為GC roots,因為GC是從這些地方出發開始探索的。看各人理解方便吧。
總結
本文創作靈感來源於 極客時間 鄭雨迪老師的《深入拆解 Java 虛擬機器》課程,通過課後反思以及借鑑各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。
關注本人公眾號,第一時間獲取最新文章釋出,每日更新一篇技術文章。