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 虛擬機》課程,通過課後反思以及借鑒各位學友的發言總結,現整理出自己的知識架構,以便日後溫故知新,查漏補缺。
關註本人公眾號,第一時間獲取最新文章發布,每日更新一篇技術文章。
12 JVM 垃圾回收(下)