zhouer's blog

閱讀、學習、寫作自留地

《深入理解 Java 虛擬機》學習筆記

zhouer / 2022-04-29


走近 Java

image-20220429095119493

自動內存管理

Java 內存區域與內存溢出異常

運行時數據區域

image-20220429102508662

程序計數區

Program Counter Register,可以看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,它是程序控製流的指示器,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

由於Java虛擬機的多線程是通過線程輪流切換、分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個內核)都只會執行一條線程中的指令。因此,為了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域為「線程私有」的內存。

當實現 Native 方法時,這個計數器值為Undefined.

Java 虛擬機棧

Java Virtual Machine Stack,線程私有的,生命週期與線程相同。每個方法被執行時,虛擬機就同步創建一個棧幀用於存儲局部變量表、操作數棧、動態連接、方法出口等信息。每一個方法被調用直至執行完畢的過程,就對應著一個棧幀在虛擬機棧中從入棧到出棧的過程。

局部變量表存放了編譯器可知的各種 Java 虛擬機基本數據類型、對象引用和 returnAddress 類型(指向了一條字節碼指令的地址)。局部變量表以 Slot 的基本單元儲存數據,局部變量表所需的內存空間在編譯期間完成分配在方法運行期間不會改變局部變量表的大小(指 Slot 的數量)。

本地方法棧

Native Method Stack,為調用本地方法服務。

Java 堆

Java Heap,”幾乎“所有的對象實例都在這裡分配內存。所有線程共享的 Java 堆中可以劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer, TLAB),以提升對象分配時的效率。

方法區

Method Area,用於存儲已被虛擬機加載的類型信息、常量、靜態變量、及時編譯器編譯後的代碼緩存等數據。作為一個概念,在 HotSpot 中以永久代/元空間實現。

運行時常量池

Runtime Constant Pool,屬於方法區的一部分。Class 文件中用於存放編譯期生成的各種字面量與符號引用的常量池表在類加載後放到方法區的運行時常量池中。

直接內存

Direct Memory,在 NIO 的引入後虛擬機可以使用 Native 函數庫直接分配堆外內存,然後通過通過一個 DirectByteBuffer 對象作為這塊內存的引用進行操作。

對象

對象的創建

  1. 虛擬機遇到 new 指令;
  2. 檢查這一指令的藏書是否能在常量池中定位的到一個類的符號引用,並檢查這個類是否被加載、解析和初始化。沒有的話就進行類加載。
  3. 為新對象在堆中分配內存;(多線程情況下,採用 CAS 或 TLAB)
  4. 將分配到的內存空間(不包括對象頭)都初始化為零值;
  5. 對對象進行必要的設置,信息存儲在對象頭裡。

此時Class 文件中的 <init> 方法尚未執行,所有字段都為零值。

對象的內存佈局

包括三部分對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。

對象的訪問定位

句柄訪問:堆內劃分出一塊句柄池,reference 中存儲的就是對象的句柄地址,句柄中包含了對象示例數據與類型數據各自的地址信息。

image-20220429111250791

直接指針訪問:reference 中存儲的就是對象地址,訪問速度快,HotSpot 中主要使用。

image-20220429111607509

垃圾收集器與內存分配策略

對象存活判定算法

引用計數算法

Reference Counting,在對象中添加一個引用計數器,計數為0時即為垃圾。需要額外工作配合例外狀態,比如循環引用。

可達性分析算法

通過一系列被稱為 “GC Roots” 的根對象作為起始點集,從這些節點開始根據引用關係向下搜索,走過的路程稱為“引用鏈”(Reference Chain),如果有搞對象和 “GC Roots” 間沒有任何引用鏈相連,或者不可達時,證明此對象可被回收。Java 虛擬機中採用該算法。

Java中,GC Roots 對象包括:

引用

回收方法區

一個類被允許回收需要同時滿足三個判定條件:

  1. 該類所有實例都已被回收;
  2. 加載該類的類加載器已被回收;
  3. 該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類方法。

垃圾收集算法

分代收集理論

Generational Collection,建立在三個分代假說之上:

收集器應該將堆劃分出不同的區域,然後將回收對象依據年齡分配到不同區域之中存儲。對於跨代引用,只在新生代建立一個全局的數據結構(記憶集),這個結構把老年代劃分成若干小塊,標識出老年代的哪塊內存會存在跨代引用,發生 Minor GC 時只把這一塊內存加入 GC Roots 進行掃描,減少 STW 時間。

標記-清除算法

Mark-Sweep,缺點在於執行效率不穩定和內存空間碎片化問題。

image-20220503103743505

標記-複製算法

image-20220503103912973

標記-整理算法

標記-複製算法在對象存活率高時要進行較多的複製操作並且為了防止內存溢出需要有額外的空間進行分配擔保,所以老年代不能採用標記-複製算法。標記-整理算法缺點在於需要暫停用戶進程,產生 “Stop The World”。

image-20220503104430058

//TODO HotSpot 的算法細節實現

經典垃圾收集器

image-20220503105054483

Serial 收集器

單線程收集器,進行垃圾收集時必須暫停其他所有工作線程。簡單高效(與其他收集器的單線程比起來),額外內存消耗小,對於小內存服務有優勢。

image-20220503112355530

ParNew 收集器

Serial 收集器的多線程版,在 JDK 9以後只能和 CMS 搭配使用。

image-20220503112650937

Parallel Scavenge 收集器

主要目標是達到一個可控的吞吐量,兼具自適應功能。

Serial Old 收集器

作為 CMS 的後備預案。

image-20220503113127838

Parallel Old 收集器

搭配 Parallel Scavenge 收集器使用。

image-20220503113259887

CSM 收集器

Concurrent Mark Sweep,以獲取最短停段時間為目標的收集器,適用于關注響應時間的服務端。運作過程包括:1)初始標記;2)並發標記;3)重新標記;4)並發清除。初始標記和重新標記需要"Stop The World"。

image-20220503113652217

缺點在於:1)對處理器資源敏感,會降低總吞吐量;2)無法處理浮動垃圾(Floating Garbage,在標記過程結束後產生的垃圾),有可能出現"Concurrent Mode Failure" 失敗而導致另一次 Full GC,因此 CMS 收集器需要預留一部分空間造成 GC 頻率提高;3)採用標記-清除算法,產生空間碎片,會提高引發 Full GC 的頻率。

Garbage First 收集器

面向堆內存任何部分來組成回收集(Collection Set,CSet)進行回收。G1 基於 Region 的堆內存佈局,不再堅持固定大小以及固定數量的分代區域劃分,而是把連續的堆內存劃分成多個大小相等的獨立區域(Region),每一個 Region 都可以根據需要扮演 Eden 區、Survivor 區、老年代。Region 中還有一類特殊的 Humongous 區域,用於存儲大對象。

G1 收集器將 Region 作為單次回收的最小單元,避免了對整個堆進行的全區域回收,具體處理思路就是讓 G1 收集器去跟蹤各個 Region 裡面垃圾堆積的“價值”大小,價值即回收所獲得的空間大小以及所需時間的經驗值,然後在後台維護一個優先隊列,根據用戶設定的允許的收集停頓時間(-XX:MaxGCPauseMillis 默認200毫秒),優先處理價值大的 Region 。

image-20220503120211050

運作大致劃分為四個步驟:1)初始標記;2)並發標記;3)最終標記;4)篩選回收:採用標記-複製算法整體處理 Region 到一個空的 Region。除了並發標記外都需要 STW 。

image-20220503120725530

G1 從整體來看基於標記-整理算法,局部(兩個 Region 之間)上基於標記-複製算法,意味著 G1 不會產生內存碎片。G1 比 CMS 處理器需要更多的內存佔用和程序運行時的額外執行負載。

//TODO低延遲垃圾收集器

垃圾收集器調優參數

image-20220503121445535

image-20220503121517001

虛擬機性能監控、故障處理工具

基礎故障處理工具

jps:虛擬機進程狀況工具

jps [ options ] [ hostid ]
image-20220505100156586

jstat:虛擬機統計信息監視工具

jstat [ option vmid [ interval [ s|ms ] [ count ] ] ]
image-20220505100534181

jinfo:Java 配置信息工具

實時查看和調整虛擬機的各項參數。

jinfo [ option ] pid

jmap:Java 內存映像工具

jinfo [ option ] vmid
image-20220505101331591

jhat:虛擬機堆轉儲快照分析工具

與 jmap 搭配使用,別用。

jstack:Java 堆棧跟蹤工具

用於生成虛擬機當前時刻的線程快照。

jstack [ option ] vmid
image-20220505101755056

小結

image-20220505102002699 image-20220505102036644

可視化故障處理工具

JHSDB:基於服務性代理的調試工具

JConsole:Java 監視與管理控制台

VisualVM:多合-故障處理工具

Java Mission Control:可持續在線的監控工具

虛擬機執行子系統

類文件結構

Class 類文件的結構

Class 文件格式採用一種類似於 C 語言結構體的偽結構來存儲數據,這種偽結構只有兩種數據類型:“無符號數”和“表”。

image-20220509101722780

//TODO

虛擬機類加載機制

Java 虛擬機吧描述類的數據從 Class 文件加載到內存,並對數據進行校驗、轉換解析和初始化,最總形成可以被虛擬機直接使用的 Java 類型,這個過程被稱作虛擬機的類加載機制。

類加載的時機

image-20220514110323086

對於初始化階段有嚴格規定有且只有六種情況必須立即對類進行初始化(加載、驗證、準備自然需要在此之前開始):

  1. 遇到 new、getstatic、putstatic 或 invokestatic 這四條字節碼指令時,若類沒有初始化過就需要進行初始化,具體表示如下:
    • 使用 new 關鍵字實例化對象時;
    • 讀取或設置一個類型的靜態字段(被 finial 修飾、已在編譯期把結果放入常量池的靜態字段除外)時;
    • 調用一個類型的靜態方法時。
  2. 使用 java.lang.reflect 包方法對類型進行反射調用時;
  3. 當初始化類時,發現其父類沒有初始化,就要先觸發父類的初始化;
  4. 當虛擬機啟動時,虛擬機會先初始化用戶指定的主類;
  5. 當時用 JDK 7 的動態語言支持時,如果 java.lang.invoke.MethodHandle 實例最後的解析結果為 REF_getStatic、REF_putStatic、REF_newInvokeSpecial 四種類型的方法句柄,並且這個方法句柄沒有被初始化;
  6. 當一個接口中定義了 default 方法,如果該接口的實現類發生初始化,那該接口需要在此之前被初始化。

這六種場景中的行為被稱作對一個類型的主動引用,除此之外,所有的引用類型的方式都不會出發初始化,稱作被動引用。

examples:

類加載的過程

加載

  1. 通過一個類的全限定名來獲取定義此類的二進制字節流;
  2. 將這個字節流所代表的的靜態存儲結構轉化為方法區的運行時數據結構;
  3. 在內存中生成一個代表這個類的 java.lang.Class 對象,作為方法區這個類的各種數據的訪問入口。

驗證

  1. 文件格式驗證
  2. 元數據驗證
  3. 字節碼驗證
  4. 符號引用驗證

準備

正式對類中定義的類變量分配內存並設置類變量初始值。

解析

將常量池內的符號引用替換為直接引用的過程。

解析動作主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用點限定符。

初始化

初始化階段就是執行類構造器 <clinit>() 方法的過程,<clinit>() 方法是 Javac 編譯期的自動生成物。

類加載器

image-20220514122517760

雙親委派機制:如果一個類加載器收到了類加載的請求,他首先不會自己去嘗試加載這個類,而是把請求委派給父類加載器去完成,每一個層次的類加載器都是如此,因此所有的加載請求最終都會傳遞到最頂層的啟動類加載器,只有當父加載器自己無法完成這個加載請求時,自加載類才會嘗試自己完成加載。

##虛擬機字節碼執行引擎

運行時棧幀結構

image-20220515143117263

方法調用

解析

調用目標在程序代碼寫好、編譯器進行編譯的時候就已經確定下來了,這類方法的調用被稱作解析。主要包括靜態方法、私有方法、實例構造器、父類方法和 final 修飾的方法,這些方法被稱作非虛方法。

分派

//TODO程序編譯與代碼優化

高效並發

Java 內存模式與線程

Java 內存模型

所有變量都存儲在主內存(Main Memory)中,每個線程有自己私有的工作內存(Working Memory)。線程的工作內存中保存了被該線程使用的變量的主內存副本,線程對變量的所有操作都必須在工作內存中進行。

image-20220516124137346

volatile

volatile 保證了變量對所有線程的可見性(當一條線程修改了這個變量的值,新值對於其他線程來說是立即得知的);禁止了指令重排優化。JMM 要求:

先行發生原則

Happens-Before,先行發生是 JMM 中定義的兩項操作之間的偏序關係。

Java 與線程

Java 定義了6种線程狀態:

image-20220516132857869

線程安全與鎖優化

當多個線程同時訪問一個對象時,如果不用考慮這些線程在運行時環境下的調度和交替執行,也不需要進行額外的同步,或者在調用方法進行任何其他的協調操作,調用這個對象的行為都可以獲得正確的結果,那就稱這個對象是線程安全的。