「Android 學習計劃」之 JVM 垃圾回收策略

1. 了解下 Java 中內存區(qū)域的劃分

Java 虛擬機在執(zhí)行 Java 程序的過程中,會把它所管理的內存劃分為若干個不同的數據區(qū)域。如圖所示:

Java 虛擬機運行時數據區(qū).png
  • 程序計數器

    程序計數器(Program Counter Register)是一塊較小的內存空間,它可以看作是當前線程所執(zhí)行的字節(jié)碼的信號指示器。

    每條線程都需要一個獨立的程序計數器,是為了線程切換后能恢復到正確的位置。

    此內存區(qū)域是唯一一個在 Java 虛擬機規(guī)范中沒有規(guī)定任何 OutOfMemoryError 情況的區(qū)域。

  • Java 虛擬機棧

    Java 虛擬機棧是線程私有的,生命周期與線程相同。虛擬棧描述的是 Java 方法執(zhí)行的內存模型:每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀,用于存儲局部變量表、操作數棧、動態(tài)鏈接、方法出口等信息。

    每一個方法從調用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。

    局部變量表存放了編譯期可知的各種基本數據類型、對象引用和 returnAddress 類型。

    局部變量表所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的的大小。

  • 本地方法棧

    本地方法棧為虛擬機使用到的 Native 方法服務。

  • Java 堆

    Java 堆是被所有線程共享的一塊內存區(qū)域,在虛擬機啟動時創(chuàng)建。此內存區(qū)域的唯一目的就是存放對象實例。

    Java 堆是垃圾收集器管理的主要區(qū)域。

  • 方法區(qū)

    方法區(qū)是各個線程共享的內存區(qū)域,用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數據。

    內存回收目標主要是針對常量池的回收和對類型的卸載

  • 運行時常量池

    運行時常量池是方法區(qū)的一部分,用于存放編譯期生成的各種字面量和符號引用。

程序計數器、虛擬機棧、本地方法棧 3 個區(qū)域隨線程而生,隨線程而滅。

棧中的棧幀隨著方法的進入和退出而有條不紊的執(zhí)行著出棧和入棧操作。每一個棧幀中分配多少內存是在類結構確定下來時就已知的,這幾個區(qū)域的內存分配和回收多具備確定性。這幾個區(qū)域不需要過多考慮回收的問題,方法結束或者線程結束時。內存就自然跟隨著回收了。

Java 堆和方法區(qū),只有在程序運行期間才能知道會創(chuàng)建哪些對象,內存的分配和回收都是動態(tài)的。

2. JVM 在進行垃圾回收之前,需要判斷哪些對象是需要回收的

  • 引用計數算法

    給對象中添加一個的引用計數器, 每當有一個地方 引用它時, 計數器值就加 1; 當引用失效時, 計數器值就減 1; 任何時刻計數器為 0 的 對象 就是不可能再被使用的。

    public class ReferenceCountingGC {
    
        public Object instance = null;
        private static final int _1MB = 1024*1024;
        private byte[] bigSize = new byte[2 * _1MB];
    
        public static void main(String[] args) {
            testGc();
        }
    
        private static void testGc() {
            ReferenceCountingGC objA = new ReferenceCountingGC();
            ReferenceCountingGC objB = new ReferenceCountingGC();
            objA.instance = objB;
            objA.instance = objA;
            objA = null;
            objB = null;
            System.gc();
        }
    }    
    

    弊端:很難解決對象之間相互循環(huán)引用的問題。

  • 可達性分析算法

    通過一系列的稱為 GC Roots 的對象作為起始點,從這些節(jié)點開始向下搜索。搜索所有走過的路徑稱為引用鏈,當一個對象到 GC Roots 對象沒有任何引用鏈相連,則證明此對象是不可用的。

    可達性分析算法判定對象是否可回收.png

    可作為 GC Roots 的對象包括下面幾種:

    虛擬機棧(棧幀中的本地變量表)中引用的對象

    方法區(qū)中類靜態(tài)屬性引用的對象

    方法區(qū)中常量引用的對象

    本地方法中 JNI 引用的對象

回收方法區(qū)

方法區(qū)(永久代)的垃圾收集主要回收兩部分:廢棄常量和無用的類。

無用的類判定條件:

  • 該類所有的實例都已被回收。

  • 加載該類的 ClassLoader 被回收。

  • 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

3. 通過垃圾收集算法進行垃圾回收

  • 標記清除算法

    首先標記出所有需要回收的對象,在標記完成后統(tǒng)一回收所有被標記的對象。

    標記清除算法不足:

    • 效率問題

      標記和清除兩個過程的效率都不高。

    • 空間問題

      標記清除之后會產生大量不連續(xù)的內存碎片, 空間碎片太多可能會導致以后在程序運行 過程中 需要分配較大對象時,無法找到足夠的連續(xù)內存 而不得不提前觸發(fā)另一次垃圾收集動作。

    標記-清除算法示意圖.png
  • 復制算法

    將可用內存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還活著的對象復制到另一塊上面,然后再把已使用的內存空間一次清理掉。

    復制算法示意圖.png
  • 標記整理算法

    標記過程仍然與標記清除算法一樣,但是后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內存。

    標記整理算法示意圖.png
  • 分代收集算法

    根據對象生活周期的不同將內存劃分為幾塊,一般是把 Java 堆分為新生代和老年代,這樣就可以根據各個年代的特點采用最適當的算法。

    在新生代中,每次垃圾收集時都發(fā)現有大批對象死去,只有少量存活,那就選用復制算法,只需要付出少量存活對象的復制成本就可以完成收集。

    在老年代中,因為對象存活率高、沒有額外空間對它進行分配擔保,就必須使用標記清理或標記整理算法來 實現。

    參考資料「 深入理解Java虛擬機:JVM高級特性與最佳實踐(第2版)」

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 1.什么是垃圾回收? 垃圾回收(Garbage Collection)是Java虛擬機(JVM)垃圾回收器提供...
    簡欲明心閱讀 90,359評論 17 311
  • 所有知識點已整理成app app下載地址 J2EE 部分: 1.Switch能否用string做參數? 在 Jav...
    侯蛋蛋_閱讀 2,707評論 1 4
  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,793評論 11 349
  • 落幕黃昏,和好來臨之際,當最后一絲光亮從天際消失,一切都靜了下來。也許可以看見幾萬光年前曾停留過那星輝燦爛的...
    未瀟閱讀 697評論 0 1
  • 昨天下午,坐在床上對著筆電敲打鍵盤的時候,一個念頭如流星般閃過——是時候該認真地寫微信公眾號了。于是,我放下手頭的...
    stardust二零二三閱讀 779評論 0 0

友情鏈接更多精彩內容