天天看點

深入了解Java虛拟機:JVM記憶體管理與垃圾收集理論閱讀的疑問???第二部分 自動記憶體管理

文章目錄

  • 閱讀的疑問???
  • 第二部分 自動記憶體管理
      • 第2章 Java記憶體區域與記憶體溢出異常
        • 1.程式計數器
        • 2.Java虛拟機棧
        • 3.本地方法棧
        • 4.Java堆
        • 5.方法區
        • 6.直接記憶體(我了解就是堆外記憶體吧)
      • HotSpot虛拟機對象探秘
        • 1.對象的建立
        • 2.對象的記憶體布局
          • 對象頭
          • 執行個體資料
          • 對齊填充
        • 3.對象的通路定位
      • 實戰:OutOfMemoryError異常
          • 1.Java堆溢出(最常見)
          • 2.虛拟機棧和本地方法棧溢出
          • 3.方法區和運作時常量池溢出(常見)
          • 4.本機直接記憶體溢出
      • 第3章 垃圾收集器與記憶體配置設定政策
        • 1.可達性分析算法
        • 2.引用
        • 3.finalize()方法(可與程式設計思想關聯啦)
        • 4.回收方法區
        • 5.垃圾收集算法
          • 5.1 分代收集理論
          • 5.2 标記-清除算法
          • 5.3 标記-複制算法
          • 5.4 标記-整理算法
        • 6. HotSpot的算法細節實作(就是如何找到存活對象,以及如何進行垃圾回收的)
          • 6.1根/GC roots節點枚舉
          • 6.2 安全點
          • 6.3 安全區域
          • 6.4 記憶集與卡表
          • 6.5 寫屏障(如何維護卡表?何時變髒?誰讓他變髒)
          • 6.6 并發的可達性分析
  1. HotSpot虛拟機中含有兩個即時編譯器,分别是編譯耗時短但輸出代碼優化程度較低的用戶端編譯 器(簡稱為C1)以及編譯耗時長但輸出代碼優化品質也更高的服務端編譯器(簡稱為C2)

    - 能提前編譯的提前編譯,不能提前編譯的交給 JVM 去解耦!

閱讀的疑問???

  1. import 導入的背後原理是?
  2. C++ 線程切換時的上下文儲存在哪裡?java 就是儲存在
  3. 本地(Native) 方法服務是什麼東西?
  4. 即時編譯技術,逃逸分析技術,棧上配置設定、标量替換優化,輕量級鎖、重量級鎖,元空間
  5. 對類型的解除安裝是什麼意思?
  6. String 類的 intern() 方法怎麼玩?
  7. 對象的記憶體布局中對齊填充(Padding)與 OS 中記憶體對齊的關系?
  8. 為什麼對象頭需要存儲這麼多的東西呐?
  9. 基本類型與包裝的基本類型有什麼差別?為什麼說基本類型快?
  10. 為什麼對于不同版本的Java虛拟機和不同的作業系統,棧容量最小值可能會有所限制,這主要取決于作業系統記憶體分頁大小。
  11. 如何自定義類加載器?比如:大量使用反射、動态代理、CGLib等位元組碼架構,動态生成JSP以及OSGi這類架構

第二部分 自動記憶體管理

第2章 Java記憶體區域與記憶體溢出異常

深入了解Java虛拟機:JVM記憶體管理與垃圾收集理論閱讀的疑問???第二部分 自動記憶體管理

1.程式計數器

  1. 目前線程所執行的位元組碼的行号訓示器。協程應該就是通過控制它來實作的。
  2. 如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛拟機位元組碼指令的地 址;如果正在執行的是本地(N at ive)方法,這個計數器值則應為空(U ndefined)。此記憶體區域是唯 一一個在《Java虛拟機規範》中沒有規定任何OutOfMemoryError情況的區域。

2.Java虛拟機棧

虛拟機棧描述的是Java方法執行的線程記憶體模型:每個方法被執行的時候,Java虛拟機都 會同步建立一個棧幀用于存儲局部變量表、操作數棧、動态連接配接、方法出口等資訊。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛拟機棧中從入棧到出棧的過程。

14. 局部變量表(存放了編譯期可知的各種Java虛拟機基本資料類型(boolean、byte、char、short、int、 float、long、double)、對象引用(reference類型,它并不等同于對象本身,可能是一個指向對象起始 位址的引用指針,也可能是指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress 類型(指向了一條位元組碼指令的位址))所需的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在棧幀中配置設定多大的局部變量空間是完全确定 的,在方法運作期間不會改變局部變量表的大小。

15. 在《Java虛拟機規範》中,對這個記憶體區域規定了兩類異常狀況:如果線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常;如果Java虛拟機棧容量可以動态擴充,當棧擴充時無法申請到足夠的記憶體會抛出 OutOfMemoryError異常。

16. 隻要是申請到記憶體就隻會發生 StackOverflowError異常,如果無法申請到才會發生 OutOfMemoryError 異常。

3.本地方法棧

  1. 作用同Java虛拟機棧,差別隻是虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛拟機使用到的本地(Native) 方法服務。

4.Java堆

  1. 根據《Java虛拟機規範》的規定,Java堆可以處于實體上不連續的記憶體空間中
  2. 在Java堆中沒有記憶體完成執行個體配置設定,并且堆也無法再 擴充時,Java虛拟機将會抛出 OutOfMemoryError 異常。

5.方法區

  1. 方法區(Method Area)與Java堆一樣,是各個線程共享的記憶體區域,它用于存儲已被虛拟機加載 的類型資訊、常量、靜态變量、即時編譯器編譯後的代碼緩存等資料。
  2. 這區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝,一般來說這個區域的回收效果比較難令人滿意,尤 其是類型的解除安裝,條件相當苛刻,但是這部分區域的回收有時又确實是必要的。以前Sun公司的Bug列 表中,曾出現過的若幹個嚴重的Bug就是由于低版本的HotSpot虛拟機對此區域未完全回收而導緻記憶體 洩漏。
  3. 6.運作時常量池(屬于方法區的一部分):Class檔案中除了有類的版本、字 段、方法、接口等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符号引用,這部分内容将在類加載後存放到方法區的運作時常量池中。
  4. Java語言并不要求常量 一定隻有編譯期才能産生,也就是說,并非預置入Class檔案中常量池的内容才能進入方法區運作時常 量池,運作期間也可以将新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的

    intern()方法。

6.直接記憶體(我了解就是堆外記憶體吧)

HotSpot虛拟機對象探秘

1.對象的建立

  1. 檢查這個指令的參數是否能在常量池中定位到 一個類的符号引用,并且檢查這個符号引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程
  2. Java編譯器會在遇到new關鍵字的地方同時生成new指令和invokesp ecial指令,這兩條位元組碼指令

2.對象的記憶體布局

在HotSpot虛拟機裡,對象在堆記憶體中的存儲布局可以劃分為三個部分:對象頭(Header)、執行個體資料(Instance Data)和對齊填充(Padding)。

對象頭
  1. HotSpot虛拟機對象的對象頭部分包括兩類資訊。第一類是用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等,這部 分資料的長度在32位和64位的虛拟機(未開啟壓縮指針)中分别為32個比特和64個比特,官方稱它 為“Mark Word”。
    深入了解Java虛拟機:JVM記憶體管理與垃圾收集理論閱讀的疑問???第二部分 自動記憶體管理
  2. 對象頭的另外一部分是類型指針,即對象指向它的類型中繼資料的指針,Java虛拟機通過這個指針 來确定該對象是哪個類的執行個體
  3. 如果對象是一個Java數組,那在對象頭中還必須有一塊用于記錄數組長度的資料,因為虛拟機可以通過普通 Java對象的中繼資料資訊确定Java對象的大小,但是如果數組的長度是不确定的,将無法通過中繼資料中的資訊推斷出數組的大小(我了解應該也是為了能夠快速擷取長度size等值)。
執行個體資料

執行個體資料部分是對象真正存儲的有效資訊,即我們在程式代碼裡面所定義的各種類型的字段内容,無論是從父類繼承下來的,還是在子類中定義的字段都必須記錄起來。這部分的存儲順序會 受到虛拟機配置設定政策參數(-XX:FieldsAllocationSty le參數)和字段在Java源碼中定義順序的影響。 HotSpot虛拟機預設的配置設定順序為longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers,OOPs),從以上預設的配置設定政策中可以看到,相同寬度的字段總是被配置設定到一起存放(其實可以看出真的是為了記憶體對齊),在滿足這個前提條件的情況下,在父類中定義的變量會出現在子類之前。如果HotSpot虛拟機的 +XX:CompactFields參數值為true(預設就為true),那子類之中較窄的變量也允許插入父類變量的空 隙之中,以節省出一點點空間。

對齊填充

占位的作用!

3.對象的通路定位

對象通路方式也是由虛拟機實 現而定的,主流的通路方式主要有使用句柄和直接指針兩種:

29. 使用句柄來通路的最大好處就是reference中存儲的是穩定句柄位址,在對象被移動(垃圾收集時移動對象是非常普遍的行為)時隻會改變句柄中的執行個體資料指針,而 reference本身不需要被修改。(解耦啦)

實戰:OutOfMemoryError異常

1.Java堆溢出(最常見)
  1. 第一步首先應确認記憶體中導緻OOM的對象是否是必要的,也就是要先厘清楚到底是出現了記憶體洩漏(Memory Leak)還是記憶體溢出(Memory Overflow)
  2. 如果是記憶體洩漏,可進一步通過工具檢視洩漏對象到GC Roots的引用鍊,找到洩漏對象是通過怎樣的引用路徑、與哪些GC Roots相關聯,才導緻垃圾收集器無法回收它們,根據洩漏對象的類型資訊 以及它到GC Roots引用鍊的資訊,一般可以比較準确地定位到這些對象建立的位置,進而找出産生記憶體洩漏的代碼的具體位置。
2.虛拟機棧和本地方法棧溢出

由于HotSpot虛拟機中并不區分虛拟機棧和本地方法棧,是以對于HotSpot來說,-Xoss參數(設定 本地方法棧大小)雖然存在,但實際上是沒有任何效果的,棧容量隻能由-Xss參數來設定。一共存在兩種異常:

32. 如 果 線 程 請 求 的 棧 深 度 大 于 虛 拟 機 所 允 許 的 最 大 深 度 , 将 抛 出 St a c k O v e r f l o w E r r o r 異 常 。

33. 如果虛拟機的棧記憶體允許動态擴充,當擴充棧容量無法申請到足夠的記憶體時,将抛出 OutOfMemoryError 異常。

3.方法區和運作時常量池溢出(常見)
  1. 使用**-Xmx參數限制最大堆**到6M B
  2. Caused by: java.lang.OutOfMemoryError: PermGen space
4.本機直接記憶體溢出
  1. 直接記憶體(Direct Memory)的容量大小可通過**-XX:MaxDirectMemorySize參數來指定**,如果不 去指定,則預設與Java堆最大值(由-Xmx指定)一緻
/**
     * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M * @author zzm
     */
    public class DirectMemoryOOM {
        private static final int _1MB = 1024 * 1024;
        
        public static void main(String[] args) throws Exception {
            Field unsafeField = Unsafe.class.getDeclaredFields()[0];
            unsafeField.setAccessible(true);
            Unsafe unsafe = (Unsafe) unsafeField.get(null);
            while (true) {
                unsafe.allocateMemory(_1MB);
            }
        }
    }
           

由直接記憶體導緻的記憶體溢出,一個明顯的特征是在Heap Dump檔案中不會看見有什麼明顯的異常 情況,如果讀者發現記憶體溢出之後産生的Dump檔案很小,而程式中又直接或間接使用了DirectM emory(典型的間接使用就是NIO),那就可以考慮重點檢查一下直接記憶體方面的原因了。

第3章 垃圾收集器與記憶體配置設定政策

1.可達性分析算法

算法的基本思路就是通過 一系列稱為“GC Roots”的根對象作為起始節點集,從這些節點開始,根據引用關系向下搜尋,搜尋過 程所走過的路徑稱為“引用鍊”(Reference Chain),如果某個對象到GC Roots間沒有任何引用鍊相連, 或者用圖論的話來說就是從GC Roots到這個對象不可達時,則證明此對象是不可能再被使用的。

深入了解Java虛拟機:JVM記憶體管理與垃圾收集理論閱讀的疑問???第二部分 自動記憶體管理

2.引用

在JDK 1.2版之前,Java裡面的引用是很傳統的定義: 如果reference類型的資料中存儲的數值代表的是另外一塊記憶體的起始位址,就稱該reference資料是代表 某塊記憶體、某個對象的引用。這種定義并沒有什麼不對,隻是現在看來有些過于狹隘了,一個對象在 這種定義下隻有“被引用”或者“未被引用”兩種狀态,對于描述一些“食之無味,棄之可惜”的對象就顯 得無能為力。譬如我們希望能描述一類對象:當記憶體空間還足夠時,能保留在記憶體之中,如果記憶體空 間在進行垃圾收集後仍然非常緊張,那就可以抛棄這些對象——很多系統的緩存功能都符合這樣的應用場景。

在JDK 1.2版之後,擴充了四種引用類型:

  1. ·強引用是最傳統的“引用”的定義,是指在程式代碼之中普遍存在的引用指派,即類似“Object obj=new Object()”這種引用關系。無論任何情況下,隻要強引用關系還存在,垃圾收集器就永遠不會回 收掉被引用的對象。
  2. ·軟引用是用來描述一些還有用,但非必須的對象。隻被軟引用關聯着的對象,在系統将要發生内 存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體, 才會抛出記憶體溢出異常。在JDK 1.2版之後提供了SoftReference類來實作軟引用。
  3. 弱引用也是用來描述那些非必須對象,但是它的強度比軟引用更弱一些,被弱引用關聯的對象隻 能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論目前記憶體是否足夠,都會回收掉隻 被弱引用關聯的對象。在JDK 1.2版之後提供了WeakReference類來實作弱引用。
  4. ·虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關系。一個對象是否有虛引用的 存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個對象執行個體。為一個對象設定虛引用關聯的唯一目的隻是為了能在這個對象被收集器回收時收到一個系統通知。在JDK 1.2版之後提供 了Phant omReference類來實作虛引用。

3.finalize()方法(可與程式設計思想關聯啦)

即使在可達性分析算法中判定為不可達的對象,也不是“非死不可”的,這時候它們暫時還處于“緩 刑”階段,要真正宣告一個對象死亡,至少要經曆兩次标記過程:

  1. 如果對象在進行可達性分析後發現沒 有與GC Roots相連接配接的引用鍊,那它将會被第一次标記。
  2. 随後進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。假如對象沒有覆寫finalize()方法,或者finalize()方法已經被虛拟機調用過,那麼虛拟機将這兩種情況都視為“沒有必要執行”

一次對象的自我拯救示範:

/**
 1. 此代碼示範了兩點:
 2. 1.對象可以在被GC時自我拯救。
 3. 2.這種自救的機會隻有一次,因為一個對象的finalize()方法最多隻會被系統自動調用一次 * @author zzm
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, i am still alive :)");
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws Throwable {
        SAVE_HOOK = new FinalizeEscapeGC();
//對象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();
// 因為Finalizer方法優先級很低,暫停0.5秒,以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no, i am dead :(");
        }
// 下面這段代碼與上面的完全相同,但是這次自救卻失敗了

    }
}
//        SAVE_HOOK=null;
//        System.gc();
//        // 因為Finalizer方法優先級很低,暫停0.5秒,以等待它 Thread.sleep(500);
//        if(SAVE_HOOK!=null){
//        SAVE_HOOK.isAlive();}else{
//        System.out.println("no, i am dead :(");}
           

4.回收方法區

堆的一次垃圾回收可以回收掉70%至99%的記憶體空間。

方法區的垃圾收集主要回收兩部分内容:廢棄的常量和不再使用的類型。

  • 舉個常量池中字面量回收的例子,假如一個字元串“ java”曾經進入常量池中,但是目前系統又沒有任何一個字元串對象的值是“ java”,換句話說,已經沒有任何字元串對象引用 常量池中的“ java”常量,且虛拟機中也沒有其他地方引用這個字面量。如果在這時發生記憶體回收,而且 垃圾收集器判斷确有必要的話,這個“ java”常量就将會被系統清理出常量池。常量池中其他類(接 口)、方法、字段的符号引用也與此類似。
  • 判定一個類型是否屬于“不再被使用的類”的條件就 比較苛刻了。需要同時滿足下面三個條件:
    1. 該類所有的執行個體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的執行個體
    2. 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的
    3. ·該類對應的java.lang.Class對象沒有在任何地方被引用,無法在任何地方通過反射通路該類的方法。

關于是否要對類型進行回收,HotSpot虛拟機提供了- Xnoclassgc參數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading檢視類加載和解除安裝資訊,其中-verbose:class和-XX:+TraceClassLoading可以在

Product版的虛拟機中使用,-XX:+TraceClassUnLoading參數需要FastDebug版[1]的虛拟機支援。

5.垃圾收集算法

5.1 分代收集理論

建立在兩個分 代假說之上:

  1. 弱分代假說(Weak Generational Hypothesis):絕大多數對象都是朝生夕滅的。
  2. 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的對象就越難以消亡。

    在Java堆劃分出不同的區域之後,垃圾收集器才可以每次隻回收其中某一個或者某些部分的區域 ——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收類型的劃分;也才能夠針對不同的區域安排與裡面存儲對象存亡特征相比對的垃圾收集算法——因而發展出了**“标記-複制算法”“标記-清除算 法”“标記-整理算法**”等針對性的垃圾收集算法。

    深入了解Java虛拟機:JVM記憶體管理與垃圾收集理論閱讀的疑問???第二部分 自動記憶體管理
5.2 标記-清除算法

算法分為“标記”和“清除”兩個階段:首先标記出所有需要回 收的對象,在标記完成後,統一回收掉所有被标記的對象,也可以反過來,标記存活的對象,統一回 收所有未被标記的對象。标記過程就是對象是否屬于垃圾的判定過程。

但是有兩個缺點:

  1. 第一個是執行效率不穩定,如果Java堆中包含大量對象,而且其中大部分是需要被回收的,這時必須進行大量标記和清除的動作,導緻标記和清除兩個過程的執行效率都随對象數量增長而降低。(感覺這個問題怎麼做都是這樣啊?除非是大部分需要回收時,我隻快速的先回收一部分)
  2. 記憶體空間的碎片化問題,标記、清除之後會産生大 量不連續的記憶體碎片,空間碎片太多可能會導緻當以後在程式運作過程中需要配置設定較大對象時無法找 到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。
5.3 标記-複制算法

為了解決标記-清除算法面對大量可回收對象時執行效率低的問題,1969年Fenichel提出了一種稱為“半區複制”(Semispace Copying)的垃圾收集算法,它将可用 記憶體按容量劃分為大小相等的兩塊,每次隻使用其中的一塊。當這一塊的記憶體用完了,就将還存活着 的對象複制到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。如果記憶體中多數對象都是存 活的,這種算法将會産生大量的記憶體間複制的開銷,但對于多數對象都是可回收的情況,算法需要複 制的就是占少數的存活對象,而且每次都是針對整個半區進行記憶體回收,配置設定記憶體時也就不用考慮有 空間碎片的複雜情況,隻要移動堆頂指針,按順序配置設定即可。這樣實作簡單,運作高效,不過其缺陷 也顯而易見,這種複制回收算法的代價是将可用記憶體縮小為了原來的一半,空間浪費未免太多了一 點。

5.4 标記-整理算法

方法與标記清除基本相同,但是他是一種移動式的方法,就是将所有存活的對象都向記憶體空間一端移動,然後直接清理掉邊界以外的記憶體。 移動需要時間,以及需要更新所有引用這些對象的地方,而且在移動的過程中程式是不可用的(這個和我們現在做的私有化一樣)。不移動,那就是造成很多的記憶體鎖片,需要類似于slab配置設定器之類的東西來進行記憶體管理。

6. HotSpot的算法細節實作(就是如何找到存活對象,以及如何進行垃圾回收的)

6.1根/GC roots節點枚舉

固定可作為GC Roots的節點主要在全局性的引用(例如常量或類靜态屬性)與執行上下文(例如 棧幀中的本地變量表)中,盡管目标明确,但查找過程要做到高效并非一件容易的事情,現在Java應用越做越龐大,光是方法區的大小就常有數百上千兆,裡面的類、常量等更是恒河沙數,若要逐個檢查以這裡為起源的引用肯定得消耗不少時間。

Q:問什麼從這些根節點開始呐?

  • 枚舉根節點時也是必須要停頓的
  • 當使用者線程停頓下來之後,其實并不需要一個不漏地檢查完所有 執行上下文和全局的引用位置,虛拟機應當是有辦法直接得到哪些地方存放着對象引用的。在HotSpot 的解決方案裡,是使用一組稱為OopMap的資料結構來達到這個目的。一旦類加載動作完成的時候, HotSpot就會把對象内什麼偏移量上是什麼類型的資料計算出來,在即時編譯(見第11章)過程中,也 會在特定的位置記錄下棧裡和寄存器裡哪些位置是引用。這樣收集器在掃描時就可以直接得知這些信 息了,并不需要真正一個不漏地從方法區等GC Roots開始查找。
6.2 安全點

引用關系會發生變化,或者說導緻OopMap内容變化的指令非常多,如果為每一條指令都生成 對應的OopMap,那将會需要大量的額外存儲空間,這樣垃圾收集伴随而來的空間成本就會變得無法忍受的高昂。

實際上HotSpot也的确沒有為每條指令都生成OopMap,前面已經提到,隻是在“特定的位置”記錄 了這些資訊,這些位置被稱為安全點(Safep oint)

是以:是在安全點的時候,JVM進行的垃圾收集。

安全點的兩個思考問題:

  1. 如何選擇安全點?
  2. 如何在垃圾收集發生時讓所有線程(這裡其實不包括 執行JNI調用的線程)都跑到最近的安全點,然後停頓下來。

    答:

  • 安全點位置的選取基本上是以“是否具有讓程式長時間執行的特征”為标準 進行標明的,因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這樣的原因而 長時間執行,“長時間執行”的最明顯特征就是指令序列的複用,例如方法調用、循環跳轉、異常跳轉 等都屬于指令序列複用,是以隻有具有這些功能的指令才會産生安全點。
  • 第二個問題有兩種解決解決方案:(1)搶先式中斷不需要線程的執行代碼 主動去配合,在垃圾收集發生時,系統首先把所有使用者線程全部中斷,如果發現有使用者線程中斷的地 方不在安全點上,就恢複這條線程執行,讓它一會再重新中斷,直到跑到安全點上。現在幾乎沒有虛 拟機實作采用搶先式中斷來暫停線程響應GC事件。(2)主動式中斷的思想是當垃圾收集需要中斷線程的時候,不直接對線程操作,僅僅簡單地設定一 個标志位,各個線程執行過程時會不停地主動去輪詢這個标志,一旦發現中斷标志為真時就自己在最 近的安全點上主動中斷挂起。輪詢标志的地方和安全點是重合的。
6.3 安全區域

如何确定安全區域?為什麼會這樣檢查虛拟機?收到的信号是什麼?從哪裡來?

深入了解Java虛拟機:JVM記憶體管理與垃圾收集理論閱讀的疑問???第二部分 自動記憶體管理
6.4 記憶集與卡表

記憶集:隻需在新生代上建立一個全局的資料結構(該結構被稱 為“記憶集”,Remembered Set),這個結構把老年代劃分成若幹小塊,辨別出老年代的哪一塊記憶體會存在跨代引用。此後當發生Minor GC時,隻有包含了跨代引用的小塊記憶體裡的對象才會被加入到GC Roots進行掃描。

收集器隻需要通過記憶集判斷出某一塊非收集區域是否存在有指向了收集區域的指針就可以了,并不需要了解這些跨代指針的全部細節。是以有了下面的這些記錄精度:

  1. 字長精度:每個記錄精确到一個機器字長(就是處理器的尋址位數,如常見的32位或64位,這個 精度決定了機器通路實體記憶體位址的指針長度),該字包含跨代指針。
  2. 對象精度:每個記錄精确到一個對象,該對象裡有字段含有跨代指針。
  3. 卡精度(卡表):每個記錄精确到一塊記憶體區域,該區域内有對象含有跨代指針。

卡表最簡單的形式可以隻是一個位元組數組,而HotSpot虛拟機确實也是這樣做的。

位元組數組 CARD_TABLE 的每一個元素都對應着其辨別的記憶體區域中一塊特定大小的記憶體塊,這個 記憶體塊被稱作“卡頁”(Card Page)。一般來說,卡頁大小都是以2的N次幂的位元組數。

一個卡頁的記憶體中通常包含不止一個對象,隻要卡頁内有一個(或更多)對象的字段存在着跨代 指針,那就将對應卡表的數組元素的值辨別為1,稱為這個元素變髒(Dirty),沒有則辨別為0。在垃圾收集發生時,隻要篩選出卡表中變髒的元素,就能輕易得出哪些卡頁記憶體塊中包含跨代指針,把它 們加入GC Roots中一并掃描。

6.5 寫屏障(如何維護卡表?何時變髒?誰讓他變髒)

何時變髒:有其他分代區域中對象引用了本區域對象時,其對應的卡表元素就應該變髒,變髒時間點原則上應該發生在引用類型字段指派的那一刻。

如何變髒:如果是解釋執行的位元組碼,JVM介入處理。如果是編譯執行的場景,那就使用寫屏障(Write Barrier)技術維護卡表狀态。寫屏障可以看作在虛拟機層面對“引用類型字段指派”這個動作的AOP切面,在引用對象指派時會産生一個環形(Around)通知,供程式執行額外的動作,也就是說指派的 前後都在寫屏障的覆寫範疇内。在指派前的部分的寫屏障叫作寫前屏障(Pre-Write Barrier),在指派 後的則叫作寫後屏障(Post-Write Barrier)。

// 寫後屏障更新卡表
void oop_field_store(oop* field, oop new_value) { // 引用字段指派操作
*field = new_value;
// 寫後屏障,在這裡完成卡表狀态更新 post_write_barrier(field, new_value);
}
           

僞共享問題:其實就是緩存行沖突的問題。具體見書吧。

6.6 并發的可達性分析

在根節點枚舉這個步驟中,由于GC Roots相比 起整個Java堆中全部的對象畢竟還算是極少數,且在各種優化技巧(如OopMap)的加持下,它帶來 的停頓已經是非常短暫且相對固定(不随堆容量而增長)的了。可從GC Roots再繼續往下周遊對象 圖,這一步驟的停頓時間就必定會與Java堆容量直接成正比例關系了:堆越大,存儲的對象越多,對 象圖結構越複雜,要标記更多對象而産生的停頓時間自然就更長。