一、執行時資料區域
1、程式計數器:
- 當前執行緒執行位元組碼的行號指示器(通過改變計數器的值來選擇下條需要執行的位元組碼指令)
- 每個執行緒有獨立的程式計數器(執行緒私有,為了切換執行緒時能恢復到掙錢的執行位置)
- 如果執行java方法,計數器記錄正在執行的位元組碼指令地址。如果執行的是Native方法,計數器為空。
- 唯一沒規定任何OutOfMemoryError情況的區域。
2、虛擬機器棧
- 為執行Java方法服務
- 執行緒私有,宣告週期跟執行緒一致
- 一個Java方法執行到結束的過程:棧幀從入棧到出棧的過程
- 棧幀儲存區域性變數表(包括基本資料型別和物件的引用型別)、操作棧、動態連結、方法出口等資訊
- 異常:執行緒請求的棧深度大於虛擬機器允許的深度,丟擲StackOverflowError。虛擬機器動態擴充套件過程中無法申請到足夠的記憶體,會丟擲OutOfMemoryError異常。
3、本地方法棧
- 為虛擬機器用到的Native方法服務
- 也會丟擲StackOverflowError和OutOfMemoryError的異常
4、Java堆
- 用來儲存物件的例項
- 所有執行緒共享的一塊記憶體區域
- 從記憶體回收的角度可以分為新生代和老年代
5、方法區
- 存放被虛擬機器載入的類資訊、常量、靜態變數等
- 執行緒共享
6、執行時常量池
- 方法區的一部分
- 存放編譯期生成的各種字面量和符號引用
二、垃圾回收(GC)
- 哪些記憶體需要回收
- 什麼時候回收
- 怎麼回收
1、判斷物件是否存活
1、引用計數法:
- 給Java物件新增一個引用計數器,每當有一個地方引用它時,計數器+1;引用失效則-1。當計算器不為0時,判斷物件存活
- 缺點:如果兩個物件相互迴圈引用時,因為計算器不為0,不能被回收。實際上物件應該被回收。
2、可達性分析演算法:
(1)原理:把"GC Roots"的物件作為起點,然後向下搜尋,搜尋所走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈相連時,即該物件不可達,也就說明此物件是不可用的。
(2)可作為GC Root物件:
- 虛擬機器棧(棧幀中的本地變數表)中引用的物件
- 方法區中類靜態屬性引用的物件
- 方法區常量引用的物件
- 本地方法棧中JNI引用的鍍錫
3、判斷一個類可回收:
- 該類所有例項物件被回收
- 載入該類的ClassLoader已經被回收
- 該類對應的Class物件沒被引用,無法通過反射訪問該類的方法
2、引用型別
通過引用的強度分為強、軟、弱、虛四種引用型別
- 強應用:一般Object b=new Object()這類引用,只要強引用存在,GC就不會回收被引用的物件。
- 軟引用:系統在發生記憶體溢位之前,會將這些物件二次回收。如果還沒足夠記憶體,才會丟擲記憶體溢位異常。通過SoftReference來實現。
- 弱引用:GC回收時,無論記憶體是否足夠,都會收會被弱引用關聯的物件。通過WeakReference來實現。
- 虛引用:作用是在物件被回收時收到一個系統通知。通過PhantomReference來實現。
3、垃圾收集演算法
1、標記-清除演算法:標記出所有需要回收的物件,標記完統一清除被標記的物件。
缺點:
- 標記和清理的效率不高
- 標記清除後會產生大量不連續的記憶體碎片
2、複製演算法:將記憶體分為大小相等的兩塊,每次只用一塊,當這一塊記憶體用完,將存活物件複製到另一塊,將使用過的記憶體一次清理。
優缺點:
- 不會產生記憶體碎片的問題
- 缺點是將記憶體縮小到了之前的一半
- 在物件存活率高時進行多次複製操作,效率會低。
3、標記-整理演算法:標記需要回收的物件,將存活物件向一端移動(整理),清理掉可回收的物件。
4、分代收集演算法:根據物件存活週期不同,將Java堆記憶體分為新生代和老年代。
- 新生代:只有少量物件存活,使用複製演算法。
- 老年代:大量物件存活,使用標記清除或者標記整理演算法。
三、類載入機制
1、類載入時機
1、定義:
把Class檔案載入到記憶體中,並對資料進行校驗、解析和初始化,行成可被虛擬機器直接使用的Java型別。類從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體結束。
2、生命週期:
- 載入
- 驗證
- 準備
- 解析
- 初始化
- 使用
- 解除安裝 載入、驗證、準備、初始化、解除安裝的順序確定。
3、需要對類進行初始化的場景
- new例項化物件、讀取或設定類的靜態欄位,呼叫類的靜態方法(被final修飾,已在編譯期將結果放入常量池的靜態欄位除外)
- 對類進行反射
- 初始化一個類,若父類還沒初始化,先觸發父類的初始化
- 需指定一個執行的主類(包含main方法的類),虛擬機器先初始化該類
- JDK1.7動態語言,MethodHandle例項解析結果REF_getStatis、REF_putStatis、REF_invokeStatis的方法控制程式碼
4、不會方法初始化的場景:
所有引用類的方式不會觸發初始化,例如子類引用父類的靜態欄位,只觸發父類初始化。
5、介面初始化和類初始化的區別:
介面初始化時,不要求其父類介面全部完成初始化。
2、類載入過程
包括載入、驗證、準備、解析、初始化5步
1、載入:
- 通過類全限定名獲取定義該類的二進位制位元組流
- 將位元組流的靜態儲存結構轉換為方法區的執行時資料結構
- 記憶體中生成該類的Class物件,作為訪問該類資料的入口
2、驗證:
- 檔案格式驗證,驗證位元組流是否符合Class檔案格式規範
- 後設資料驗證,對位元組碼描述的資訊進行語義分析
- 位元組碼驗證,確定語義合法
- 符號引用驗證,對常量池符號引用校驗
3、準備: 為類變數(static修飾的變數)分配記憶體並設定變數初始值
4、解析: 將常量池符號引用替換為直接引用(直接指向目標的指標)的過程
5、初始化: 開始執行類中定義的Java程式碼
3、類載入器
同一個Class檔案,被兩個不同的類載入器載入,這兩個類不相等。相等包括equals、instanceOf、isInstance方法返回的結果。
1、類別:
- 啟動類載入器(Bootstrap ClassLoader):載入<JAVA_HOME>\lib目標,或者被-Xbootclasspath引數指定的路徑,可被虛擬機器識別的類庫
- 擴充套件類載入器(Extension ClassLoader):載入<JAVA_HOME>\lib\ext目錄,或被java.ext.dirs系統變數指定的路徑的類庫
- 應用類載入器(Application ClassLoader):載入ClassPath上指定的類庫
2、雙親委託機制
除了頂層的類載入器外,其他的類載入器都有自己的父類載入器。父子之間通過組合來複用父載入器程式碼。
雙親委託機制的工作流程:一個類載入器收到類載入的請求,首先將請求委託給父類載入器去完成,最終所有載入請求都會傳遞給頂層的啟動載入器中。當父載入器發現未找到所需的類而無法完成載入請求時,子載入器才嘗試去載入。
ClassLoader
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
//檢查請求的類是否已經被載入
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
//讓父類載入器去嘗試載入
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//父類載入器拋異常
}
if (c == null) {
//然後呼叫自身的findClass方法來進行類載入
c = findClass(name);
}
}
return c;
}
複製程式碼
- 先檢查是否被載入過,如果沒有則呼叫父載入類去載入
- 父載入器為空,則呼叫啟動類載入器
- 父載入器載入失敗,則丟擲ClassNotFoundException異常
- 然後去呼叫自身的findClass方法去進行類載入