Java虛擬機
- 2020 年 9 月 22 日
- 筆記
虛擬機記憶體劃分
- 程式計數器
計數器記錄了正在執行的虛擬機位元組碼指令的地址,如果執行的是native方法,計數器值為空
- Java虛擬機棧
虛擬機棧描述的是Java方法執行的記憶體模型,每一個方法從調用到執行完成的過程,對應著一個棧幀在虛擬機中入棧到出棧的過程虛擬機棧不可以動態擴展時,如果執行緒請求的棧深度大於虛擬機所允許的深度,會拋出StackOverflowError異常。虛擬機棧動態擴展時,如果擴展時無法申請到足夠的記憶體,就會拋出OutOfMemoryError異常。
- 本地方法棧
與虛擬機棧相似,不過對應的是native方法服務,同樣會拋出上述異常。
- Java堆
存放對象實例,邏輯上是連續的,物理上 不一定連續,可通過-Xmx和-Xms擴展
- 方法區
用於存儲已被虛擬機載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等數據。
- 運行時常量池
用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類載入後進入方法去的運行時常量池中存放,以及string常量,常用於intern方法(當調用 intern 方法時,如果池已經包含一個等於此 String 對象的字元串(用 equals(Object) 方法確定),則返回池中的字元串。否則,將此 String 對象添加到池中,並返回此 String 對象的引用。)
對象的創建過程(p45)
虛擬機遇到一個new指令後,首先檢查該指令參數能否在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已被載入,解析初始化,如果沒有則執行類載入過程。
對象的訪問定位(Java程式需要通過棧上的reference數據來操作堆上的具體對象)
- 句柄方式
Java堆中會劃分出一塊記憶體來作為句柄池,reference中存儲了對象的句柄地址,而句柄包含了對象的具體地址。
- 直接指針訪問
Reference中存儲了對象的的真實地址
優劣:對象被移動只會改變句柄鍾到實例數據指針,reference不需要修改,直接指針速度快。
垃圾回收演算法
- 引用計數演算法
對象保存一個引用計數器,每當有一個地方引用它時,計數器值加一,引用是失效時則減一,為0則不在被使用,缺點是難以解決循環引用的問題。
- 可達性分析演算法
通過稱為「GC root」的對象為起始點,從這些起始點向下搜索,當一個對象到GC root 沒有任何引用鏈相連,則這個對象是不可達的,在枚舉根節點分析時保證對象引用關係不變化,所以會造成停頓。
GC root對象:
a. 虛擬機棧中引用的對象
b. 方法區中靜態變數屬性引用的對象
c. 方法區中常量引用的對象
d. 本地方法棧中引用的對象
- 標記清除演算法
標記完後續統一回收,但標記清除效率不高,會產生大量不連續的記憶體碎片,該演算法是GC的基礎。
- 複製演算法
Java中將堆分為新生代和老年代,新生代又分為Eden和兩個servivor空間,每次將存活對象複製到另一個servivor中,如果servivor空間不足,則由老年代進行分配擔保,存儲這些對象。
- 標記整理演算法
該演算法工作在老年代,在標記後將存活對象向一端移動,清理另一邊的記憶體,以應對對象存活率較高的情況。
- 分代收集演算法
即將堆分為新生代和老年代,再在不同代的堆運行不同的演算法,新生代採用複製演算法,老年代使用標記清除或者標記整理演算法。
對象回收過程
首先進行可達性分析,這裡會造成GC停頓,停頓指的是執行緒執行到安全點停頓,如果對象不可達,則開始第一次標記篩選,篩選條件是是否有必要執行finalize方法,如果覆蓋了finalize方法,且未被執行過,則將對象放置在F-Queue隊列中,稍後GC會對該隊列進行第二次標記並執行finalize方法,在finalize方法中沒有將自身引用賦值給其他變數,則回收。
垃圾收集器
Serial收集器
單執行緒收集器,工作在新生代,進行垃圾收集時,必須暫停其他執行緒,在新生代採取複製演算法
ParNew收集器
Serial收集器的多執行緒版本,該收集器多個GC執行緒並行執行,但同樣需要暫停用戶執行緒,能與CMS收集器配合各工作,因CMS只能和ParNew或者Serial收集器配合工作,所以該收集器時Server模式首選收集器。
Parallel Scavenge收集器
新生代收集器,採用複製演算法,該收集器的特點是吞吐量可控制,即CPU用戶執行緒運行的時間與總時間的比值
Serial Old收集器
Serial收集器的老年代版本,採用標記整理演算法,主要給client模式下的虛擬機使用,單執行緒。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本,採用標記整理演算法,多執行緒
CMS收集器
工作在老年代,以獲取最短回收停頓時間為目標的收集器,響應速度快,用戶體驗好。基於標記清除演算法,分為四步:初始標記僅標記下GC Root直接關聯的對象,需要暫停用戶執行緒;並發標記不用暫停用戶執行緒;重新標記修正並發標記時標記變動的那部分對象,需要暫停用戶執行緒;最後時並發清除,不需要暫停用戶執行緒。
G1收集器
地表最強收集器,堆不再劃分成新生代和老年代,而是一塊塊大小相等的記憶體區域,G1會根據小堆的垃圾佔比進行有優先順序的區域回收方式。G1收集器分為四個步驟;初始標記,並發標記,最終標記,篩選回收。初始標記僅標記GC Roots直接關聯的對象,需停頓用戶執行緒;並發標記與用戶執行緒並發執行,對堆中對象進行可達性分析,找出存活對象;最終標記修正並行標記階段標記產生變化的記錄,可並行執行,但需停頓用戶執行緒;篩選回收則清理不可用對象,可與用戶執行緒並發執行,但一般停頓用戶執行緒效率更高
Minor GC和Full GC的區別
Minor GC發生在新生代,比較頻繁,Full GC發生在老年代,速度慢。堆中記憶體不足會觸發GC,要避免老年代的GC。
對象進入老年代的時機
大對象直接進入老年代,所以大對象存活時間又短的容易觸發Full GC;Minor GC時,survivor空間不足,對象因分配擔保進入老年代;對象保存有年齡計數器,每進行一次Minor GC,年齡加一,到了閾值會進入老年代;動態年齡判定,在survivor空間中相同年齡所有對象大於survivor的一半,該年齡以上的對象進入老年代。
類載入的時機
-
使用new關鍵字實例化時,讀取或設置一個類的靜態欄位時(對於final修飾的類常量,在調用時並不會觸發被調用類的初始化,因為該常量已被編譯到調用類的常量池位元組碼中),調用一個類的靜態方法時。
-
使用java.lang.reflect包的方法對類進行反射調用時,類未被初始化。
-
初始化一個類時,父類沒有被初始化,需先初始化父類(介面不要求父介面已初始化,除非用到了父類介面)。
-
虛擬機啟動時初始化mian方法主類。
-
使用動態語言支援時。
除此之外,其他任何情況都不會觸發類的初始化,如通過子類調用父類的靜態欄位,不會初始化子類。
類載入過程
- 載入
將類載入進方法區,生成代表該類的class對象,作為方法區該類的各種數據入口。
- 驗證
驗證位元組流是否符合class文件規範,確保class文件的位元組流符合當前虛擬機規範。
- 準備
為類變數分配記憶體並設置類變數初始值,除final類型的變數,其餘都賦值為零或null或false。
- 解析
將符號引用替換為直接引用
- 初始化
執行類中Java程式碼,將初始值替換為真實賦值。
注意:初始化時程式碼執行是從上往下執行的,靜態語句塊中只能訪問到定義在靜態語句塊之前的變數,定義在它之後的類變數,只能賦值(在準備階段已經分配了記憶體),不能訪問(還沒有賦真值,訪問的值是無效的,這就不安全了)。
子類和父類初始化順序:
1,在類載入的時候執行父類的static程式碼塊,並且只執行一次(因為類只載入一次);
2,執行子類的static程式碼塊,並且只執行一次(因為類只載入一次);
3,執行父類的類成員初始化,並且是從上往下按出現順序執行;
4,執行父類的構造函數;
5,執行子類的類成員初始化,並且是從上往下按出現順序執行。
6,執行子類的構造函數。
雙親委派模型
Java中類載入器可分為三類:
- 啟動類載入器
使用c++語言實現,負責載入lib目錄中的類庫,無法被Java程式直接引用。
- 擴展類載入器
使用Java實現,負責載入lib\ext目錄中的類庫,開發之可以直接使用。
- 應用程式類載入器
由ClassLoader實現,負責載入用戶類路徑(classpath)上指定的類庫,開發者可以直接使用。
雙親委派模型的工作過程:如果一個類收到了類載入的請求,會首先把這個請求委派給父類載入器(使用組合關係來複用父載入器的程式碼),因此所有的載入請求都委派到頂層的啟動類載入器,只有父類載入器無法完成這個載入請求,子載入器才會嘗試自己去載入,這樣保證了object類在程式中都是同一個類,保證了程式的穩定。