文章目錄
- 閱讀的疑問???
- 第二部分 自動記憶體管理
- 第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 并發的可達性分析
- HotSpot虛擬機中含有兩個即時編譯器,分別是編譯耗時短但輸出代碼優化程度較低的客戶端編譯 器(簡稱為C1)以及編譯耗時長但輸出代碼優化質量也更高的服務端編譯器(簡稱為C2)
- 能提前編譯的提前編譯,不能提前編譯的交給 JVM 去解耦!
閱讀的疑問???
- import 匯入的背后原理是?
- C++ 執行緒切換時的背景關系保存在哪里?java 就是保存在
- 本地(Native) 方法服務是什么東西?
- 即時編譯技術,逃逸分析技術,堆疊上分配、標量替換優化,輕量級鎖、重量級鎖,元空間
- 對型別的卸載是什么意思?
- String 類的 intern() 方法怎么玩?
- 物件的記憶體布局中對齊填充(Padding)與 OS 中記憶體對齊的關系?
- 為什么物件頭需要存盤這么多的東西吶?
- 基本型別與包裝的基本型別有什么區別?為什么說基本型別快?
- 為什么對于不同版本的Java虛擬機和不同的作業系統,堆疊容量最小值可能會有所限制,這主要取決于作業系統記憶體分頁大小,
- 如何自定義類加載器?比如:大量使用反射、動態代理、CGLib等位元組碼框架,動態生成JSP以及OSGi這類框架
第二部分 自動記憶體管理
第2章 Java記憶體區域與記憶體溢位例外

1.程式計數器
- 當前執行緒所執行的位元組碼的行號指示器,協程應該就是通過控制它來實作的,
- 如果執行緒正在執行的是一個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.本地方法堆疊
- 作用同Java虛擬機堆疊,區別只是虛擬機堆疊為虛擬機執行Java方法(也就是位元組碼)服務,而本地方法堆疊則是為虛擬機使用到的本地(Native) 方法服務,
4.Java堆
- 根據《Java虛擬機規范》的規定,Java堆可以處于物理上不連續的記憶體空間中
- 在Java堆中沒有記憶體完成實體分配,并且堆也無法再 擴展時,Java虛擬機將會拋出 OutOfMemoryError 例外,
5.方法區
- 方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用于存盤已被虛擬機加載 的型別資訊、常量、靜態變數、即時編譯器編譯后的代碼快取等資料,
- 這區域的記憶體回收目標主要是針對常量池的回收和對型別的卸載,一般來說這個區域的回收效果比較難令人滿意,尤 其是型別的卸載,條件相當苛刻,但是這部磁區域的回收有時又確實是必要的,以前Sun公司的Bug列 表中,曾出現過的若干個嚴重的Bug就是由于低版本的HotSpot虛擬機對此區域未完全回收而導致記憶體 泄漏,
- 6.運行時常量池(屬于方法區的一部分):Class檔案中除了有類的版本、字 段、方法、介面等描述資訊外,還有一項資訊是常量池表(Constant Pool Table),用于存放編譯期生成的各種字面量與符號參考,這部分內容將在類加載后存放到方法區的運行時常量池中,
- Java語言并不要求常量 一定只有編譯期才能產生,也就是說,并非預置入Class檔案中常量池的內容才能進入方法區運行時常 量池,運行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的
intern()方法,
6.直接記憶體(我理解就是堆外記憶體吧)
HotSpot虛擬機物件探秘
1.物件的創建
- 檢查這個指令的引數是否能在常量池中定位到 一個類的符號參考,并且檢查這個符號參考代表的類是否已被加載、決議和初始化過,如果沒有,那必須先執行相應的類加載程序
- Java編譯器會在遇到new關鍵字的地方同時生成new指令和invokesp ecial指令,這兩條位元組碼指令
2.物件的記憶體布局
在HotSpot虛擬機里,物件在堆記憶體中的存盤布局可以劃分為三個部分:物件頭(Header)、實體資料(Instance Data)和對齊填充(Padding),
物件頭
- HotSpot虛擬機物件的物件頭部分包括兩類資訊,第一類是用于存盤物件自身的運行時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀態標志、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部 分資料的長度在32位和64位的虛擬機(未開啟壓縮指標)中分別為32個位元和64個位元,官方稱它 為“Mark Word”,

- 物件頭的另外一部分是型別指標,即物件指向它的型別元資料的指標,Java虛擬機通過這個指標 來確定該物件是哪個類的實體
- 如果物件是一個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堆溢位(最常見)
- 第一步首先應確認記憶體中導致OOM的物件是否是必要的,也就是要先分清楚到底是出現了記憶體泄漏(Memory Leak)還是記憶體溢位(Memory Overflow)
- 如果是記憶體泄漏,可進一步通過工具查看泄漏物件到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.方法區和運行時常量池溢位(常見)
- 使用**-Xmx引數限制最大堆**到6M B
- Caused by: java.lang.OutOfMemoryError: PermGen space
4.本機直接記憶體溢位
- 直接記憶體(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到這個物件不可達時,則證明此物件是不可能再被使用的,

2.參考
在JDK 1.2版之前,Java里面的參考是很傳統的定義: 如果reference型別的資料中存盤的數值代表的是另外一塊記憶體的起始地址,就稱該reference資料是代表 某塊記憶體、某個物件的參考,這種定義并沒有什么不對,只是現在看來有些過于狹隘了,一個物件在 這種定義下只有“被參考”或者“未被參考”兩種狀態,對于描述一些“食之無味,棄之可惜”的物件就顯 得無能為力,譬如我們希望能描述一類物件:當記憶體空間還足夠時,能保留在記憶體之中,如果記憶體空 間在進行垃圾收集后仍然非常緊張,那就可以拋棄這些物件——很多系統的快取功能都符合這樣的應用場景,
在JDK 1.2版之后,擴充了四種參考型別:
- ·強參考是最傳統的“參考”的定義,是指在程式代碼之中普遍存在的參考賦值,即類似“Object obj=new Object()”這種參考關系,無論任何情況下,只要強參考關系還存在,垃圾收集器就永遠不會回 收掉被參考的物件,
- ·軟參考是用來描述一些還有用,但非必須的物件,只被軟參考關聯著的物件,在系統將要發生內 存溢位例外前,會把這些物件列進回收范圍之中進行第二次回收,如果這次回識訓沒有足夠的記憶體, 才會拋出記憶體溢位例外,在JDK 1.2版之后提供了SoftReference類來實作軟參考,
- 弱參考也是用來描述那些非必須物件,但是它的強度比軟參考更弱一些,被弱參考關聯的物件只 能生存到下一次垃圾收集發生為止,當垃圾收集器開始作業,無論當前記憶體是否足夠,都會回收掉只 被弱參考關聯的物件,在JDK 1.2版之后提供了WeakReference類來實作弱參考,
- ·虛參考也稱為“幽靈參考”或者“幻影參考”,它是最弱的一種參考關系,一個物件是否有虛參考的 存在,完全不會對其生存時間構成影響,也無法通過虛參考來取得一個物件實體,為一個物件設定虛參考關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知,在JDK 1.2版之后提供 了Phant omReference類來實作虛參考,
3.finalize()方法(可與編程思想聯動啦)
即使在可達性分析演算法中判定為不可達的物件,也不是“非死不可”的,這時候它們暫時還處于“緩 刑”階段,要真正宣告一個物件死亡,至少要經歷兩次標記程序:
- 如果物件在進行可達性分析后發現沒 有與GC Roots相連接的參考鏈,那它將會被第一次標記,
- 隨后進行一次篩選,篩選的條件是此物件是否有必要執行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”常量就將會被系統清理出常量池,常量池中其他類(接 口)、方法、欄位的符號參考也與此類似,
- 判定一個型別是否屬于“不再被使用的類”的條件就 比較苛刻了,需要同時滿足下面三個條件:
- 該類所有的實體都已經被回收,也就是Java堆中不存在該類及其任何派生子類的實體
- 加載該類的類加載器已經被回收,這個條件除非是經過精心設計的可替換類加載器的場景,如OSGi、JSP的重加載等,否則通常是很難達成的
- ·該類對應的java.lang.Class物件沒有在任何地方被參考,無法在任何地方通過反射訪問該類的方法,
關于是否要對型別進行回收,HotSpot虛擬機提供了- Xnoclassgc引數進行控制,還可以使用-verbose:class以及-XX:+TraceClass-Loading、-XX: +TraceClassUnLoading查看類加載和卸載資訊,其中-verbose:class和-XX:+TraceClassLoading可以在
Product版的虛擬機中使用,-XX:+TraceClassUnLoading引數需要FastDebug版[1]的虛擬機支持,
5.垃圾收集演算法
5.1 分代收集理論
建立在兩個分 代假說之上:
- 弱分代假說(Weak Generational Hypothesis):絕大多數物件都是朝生夕滅的,
- 強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集程序的物件就越難以消亡,
在Java堆劃分出不同的區域之后,垃圾收集器才可以每次只回收其中某一個或者某些部分的區域 ——因而才有了“Minor GC”“Major GC”“Full GC”這樣的回收型別的劃分;也才能夠針對不同的區域安排與里面存盤物件存亡特征相匹配的垃圾收集演算法——因而發展出了**“標記-復制演算法”“標記-清除算 法”“標記-整理演算法**”等針對性的垃圾收集演算法,

5.2 標記-清除演算法
演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回 收的物件,在標記完成后,統一回收掉所有被標記的物件,也可以反過來,標記存活的物件,統一回 收所有未被標記的物件,標記程序就是物件是否屬于垃圾的判定程序,
但是有兩個缺點:
- 第一個是執行效率不穩定,如果Java堆中包含大量物件,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個程序的執行效率都隨物件數量增長而降低,(感覺這個問題怎么做都是這樣啊?除非是大部分需要回收時,我只快速的先回收一部分)
- 記憶體空間的碎片化問題,標記、清除之后會產生大 量不連續的記憶體碎片,空間碎片太多可能會導致當以后在程式運行程序中需要分配較大物件時無法找 到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作,
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進行的垃圾收集,
安全點的兩個思考問題:
- 如何選擇安全點?
- 如何在垃圾收集發生時讓所有執行緒(這里其實不包括 執行JNI呼叫的執行緒)都跑到最近的安全點,然后停頓下來,
答:
- 安全點位置的選取基本上是以“是否具有讓程式長時間執行的特征”為標準 進行選定的,因為每條指令執行的時間都非常短暫,程式不太可能因為指令流長度太長這樣的原因而 長時間執行,“長時間執行”的最明顯特征就是指令序列的復用,例如方法呼叫、回圈跳轉、例外跳轉 等都屬于指令序列復用,所以只有具有這些功能的指令才會產生安全點,
- 第二個問題有兩種解決解決方案:(1)搶先式中斷不需要執行緒的執行代碼 主動去配合,在垃圾收集發生時,系統首先把所有用戶執行緒全部中斷,如果發現有用戶執行緒中斷的地 方不在安全點上,就恢復這條執行緒執行,讓它一會再重新中斷,直到跑到安全點上,現在幾乎沒有虛 擬機實作采用搶先式中斷來暫停執行緒回應GC事件,(2)主動式中斷的思想是當垃圾收集需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單地設定一 個標志位,各個執行緒執行程序時會不停地主動去輪詢這個標志,一旦發現中斷標志為真時就自己在最 近的安全點上主動中斷掛起,輪詢標志的地方和安全點是重合的,
6.3 安全區域
如何確定安全區域?為什么會這樣檢查虛擬機?收到的信號是什么?從哪里來?

6.4 記憶集與卡表
記憶集:只需在新生代上建立一個全域的資料結構(該結構被稱 為“記憶集”,Remembered Set),這個結構把老年代劃分成若干小塊,標識出老年代的哪一塊記憶體會存在跨代參考,此后當發生Minor GC時,只有包含了跨代參考的小塊記憶體里的物件才會被加入到GC Roots進行掃描,
收集器只需要通過記憶集判斷出某一塊非收集區域是否存在有指向了收集區域的指標就可以了,并不需要了解這些跨代指標的全部細節,因此有了下面的這些記錄精度:
- 字長精度:每個記錄精確到一個機器字長(就是處理器的尋址位數,如常見的32位或64位,這個 精度決定了機器訪問物理記憶體地址的指標長度),該字包含跨代指標,
- 物件精度:每個記錄精確到一個物件,該物件里有欄位含有跨代指標,
- 卡精度(卡表):每個記錄精確到一塊記憶體區域,該區域內有物件含有跨代指標,
卡表最簡單的形式可以只是一個位元組陣列,而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堆容量直接成正比例關系了:堆越大,存盤的物件越多,對 象圖結構越復雜,要標記更多物件而產生的停頓時間自然就更長,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/290541.html
標籤:其他
上一篇:初 揭 JVM 神 秘 面 紗
