這些不可不知的JVM知識,我都用思維導圖整理好了

三分惡發表於2021-02-14

JVM是面試中必問的部分,本文通過思維導圖以面向面試的角度整理JVM中不可不知的知識。

先上圖:

JVM必備知識

1、JVM基本概念

1.1、JVM是什麼

JVM 的全稱是 「Java Virtual Machine」,也就是我們耳熟能詳的 Java 虛擬機器。

JVM具備著計算機的基本運算方式,它主要負責把 Java 程式生成的位元組碼檔案,解釋成具體系統平臺上的機器指令,讓其在各個平臺執行。

JVM是執行在作業系統上的,它與硬體沒有直接的互動。

當然,嚴格來說JVM也是虛擬機器規範,有很多不同的實現,Sun/OracleJDK和OpenJDK中的預設Java虛擬機器是HotSpot虛擬機器,是目前使用範圍最廣的Java虛擬機器,一般講到的JVM預設指的就是HotSpot虛擬機器。

1.2、Java程式執行過程

我們都知道 Java 原始檔,通過編譯器,能夠生產相應的.Class 檔案,也就是位元組碼檔案,而位元組碼檔案又通過 Java 虛擬機器中的直譯器,編譯成特定機器上的機器碼 。

也就是如下:

image-20210213164039026

每一種平臺的直譯器是不同的,但是實現的虛擬機器是相同的,這也就是 Java 為什麼能夠跨平臺的原因了 ,當一個程式從開始執行,這時虛擬機器就開始例項化了,多個程式啟動就會存在多個虛擬機器例項。程式退出或者關閉,則虛擬機器例項消亡,多個虛擬機器例項之間資料不能共享。

1.3、JDK、JRE、JVM

  • JDK(Java Development Kit Java 開發工具包),JDK 是提供給 Java 開發人員使用的,其中包含了 Java 的開發工具,也包括了 JRE。其中的開發工具包括編譯工具(javac.exe) 打包工具(jar.exe)等。

  • JRE(Java Runtime Environment Java 執行環境) 是 JDK 的子集,也就是包括 JRE 所有內容,以及開發應用程式所需的編譯器和偵錯程式等工具。JRE 提供了庫、Java 虛擬機器(JVM)和其他元件,用於執行 Java 程式語言、小程式、應用程式。

  • JVM(Java Virtual Machine Java 虛擬機器),JVM 可以理解為是一個虛擬出來的計算機,具備著計算機的基本運算方式,它主要負責把 Java 程式生成的位元組碼檔案,

    解釋成具體系統平臺上的機器指令,讓其在各個平臺執行。

JDK中包含JRE,也包括JDK,而JRE也包括JDK。

範圍關係:JDK>JRE>JVM。

image-20210213164531155

2、JVM記憶體區域

Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。根據《Java虛擬機器規範》的規定,Java虛擬機器所管理的記憶體將會包括以下幾個執行時資料區域:

image-20210213172256916

當然,實際上,為了更好的適應 CPU 效能提升,最大限度提升JVM 執行效率,JDK中各個版本對JVM進行了一些迭代,示意圖如下:

image-20210213172547779

JDK1.6、JDK1.7、JDK1.8 JVM 記憶體模型主要有以下差異:

  • JDK 1.6:有永久代,靜態變數存放在永久代上。

  • JDK 1.7:有永久代,但已經把字串常量池、靜態變數,存放在堆上。逐漸的減少永久代的使用。

  • JDK 1.8:無永久代,執行時常量池、類常量池,都儲存在後設資料區,也就是常說的元空間。但字串常量池仍然存放在堆上。

2.1、程式計數器

一塊較小的記憶體空間, 是當前執行緒所執行的位元組碼的行號指示器,每條執行緒都要有一個獨立的程式計數器,這類記憶體也稱為“執行緒私有”的記憶體。

正在執行 java 方法的話,計數器記錄的是虛擬機器位元組碼指令的地址(當前指令的地址)。如果還是 Native 方法,則為空。

這個記憶體區域是唯一一個在虛擬機器中沒有規定任何 OutOfMemoryError 情況的區域。

2.2、Java虛擬機器棧

與程式計數器一樣,Java虛擬機器棧(Java Virtual Machine Stack)也是執行緒私有的,它的生命週期與執行緒相同。

虛擬機器棧描述的是Java方法執行的執行緒記憶體模型:每個方法被執行的時候,Java虛擬機器都 會同步建立一個棧幀(Stack Frame)用於儲存區域性變數表、運算元棧、動態連線、方法出口等資訊。每一個方法被呼叫直至執行完畢的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

棧示意圖1

區域性變數表存放了編譯期可知的各種Java虛擬機器基本資料型別、物件引用(reference型別,它並不等同於物件本身,可能是一個指向物件起始址的引用指標,也可能是指向一個代表物件的控制程式碼或者其他與此物件相關的位置)和returnAddress 型別(指向了一條位元組碼指令的地址)。

image-20210214142724540

2.3、本地方法棧

本地方法棧(Native Method Stacks)與虛擬機器棧所發揮的作用是非常相似的,其區別只是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的本地(Native) 方法服務。

Hot-Spot虛擬機器直接把本地方法棧和虛擬機器棧合二為一。

與虛擬機器棧一樣,本地方法棧也會在棧深度溢位或者棧擴充套件失敗時分別丟擲StackOverflowError和OutOfMemoryError異常。

2.4、Java堆

對於Java應用程式來說,Java堆(Java Heap)是虛擬機器所管理的記憶體中最大的一塊。Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,Java 世界裡“幾乎”所有的物件例項都在這裡分配記憶體。

Java堆是垃圾收集器管理的記憶體區域,因此一些資料中它也被稱作“GC堆”。

從回收記憶體的角度看,

  • Java 堆,由年輕代和年老代組成,分別佔據 1/3 和 2/3。

  • 而年輕代又分為三部分,EdenFrom SurvivorTo Survivor,佔據比例為 8:1:1,可調。

需要注意的是這些區域劃分僅僅是一部分垃圾收集器的共同特性或者說設計風格而已,而非某個Java虛擬機器具體實現的固有記憶體佈局,HotSpot裡面已經出現了不採用分代設計的新垃圾收集器。

image-20210213193521527

Java堆既可以被實現成固定大小的,也可以是可擴充套件的,不過當前主流的Java虛擬機器都是按照可擴充套件來實現的(通過引數-Xmx和-Xms設定)。如果在Java堆中沒有記憶體完成例項分配,並且堆也無法再擴充套件時,Java虛擬機器將會丟擲OutOfMemoryError異常。

2.5、方法區(JDK1.8移除)

方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入 的型別資訊、常量、靜態變數、即時編譯器編譯後的程式碼快取等資料。

在JDK1.8以前,HotSpot使用永久代來實現方法區,所以某些場合也認為方法區和永久代是一個概念。

在JDK 6的 時候HotSpot開發團隊就有放棄永久代,逐步改為採用本地記憶體(Native Memory)來實現方法區的計劃了,到了JDK 7的HotSpot,已經把原本放在永久代的字串常量池、靜態變數等移出,而到了 JDK 8,終於完全廢棄了永久代的概念,改用在本地記憶體中實現的元空間(Meta- space)來代替,把JDK 7中永久代還剩餘的內容(主要是型別資訊)全部移到元空間中。

如果方法區無法滿足新的記憶體分配需求時,將丟擲OutOfMemoryError異常。

2.6、執行時常量池

執行時常量池(Runtime Constant Pool)是方法區的一部分——在JDK1.8已經被移到了元空間。

執行時常量池相對於Class檔案常量池的一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是說,並非預置入Class檔案中常量池的內容才能進入執行時常量池,執行期間也可以將新的常量放入池中,這種特性被開發人員利用得比較多的便是String類的 intern()方法。

2.7、直接記憶體

直接記憶體(Direct Memory)並不是虛擬機器執行時資料區的一部分。

顯然,本機直接記憶體的分配不會受到Java堆大小的限制,但是,既然是記憶體,則肯定還是會受到本機總記憶體(包括實體記憶體、SWAP分割槽或者分頁檔案)大小以及處理器定址空間的限制。

元空間從虛擬機器 Java 堆中轉移到本地記憶體,預設情況下,元空間的大小僅受本地記憶體的限制。jdk1.8 以前版本的 class 和 JAR 包資料儲存在 PermGen 下面 ,PermGen 大小是固定的,而且專案之間無法共用,公有的 class,所以比較容易出現 OOM 異常。

升級 JDK 1.8 後,元空間配置引數,-XX:MetaspaceSize=512M XX:MaxMetaspaceSize=1024M。

3、JVM中的物件

上面已經瞭解Java虛擬機器的執行時資料區域,我們接下來更進一步瞭解這些虛擬機器記憶體中資料的其他細節,譬如它們是如何建立、如何佈局以及如何訪問的。以最常用的虛擬機器HotSpot和最常用的記憶體區域Java堆為例,瞭解一下HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的全過程。

3.1、物件的建立

Java物件建立的大概過程如下:

image-20210214114717010

  • 類載入檢查: 虛擬機器遇到⼀條 new 指令時,⾸先將去檢查這個指令的引數是否能在常量池中定位到這個類的符號引⽤,並且檢查這個符號引⽤代表的類是否已被載入過、解析和初始化過。如果沒有,那必須先執⾏相應的類載入過程。

  • 分配記憶體: 在類載入檢查通過後,接下來虛擬機器將為新⽣物件分配記憶體。物件所需的記憶體⼤⼩在類載入完成後便可確定,為物件分配空間的任務等同於把⼀塊確定⼤⼩的記憶體從 Java 堆中劃分出來。分配⽅式有 指標碰撞空閒列表 兩種,選擇那種分配⽅式由 Java 堆是否規整決定,⽽Java堆是否規整⼜由所採⽤的垃圾收集器是否帶有壓縮整理功能決定。

記憶體分配的兩種⽅式:選擇以上兩種⽅式中的哪⼀種,取決於 Java 堆記憶體是否規整。⽽ Java 堆記憶體是否規整,取決於 GC收集器的演算法是"標記-清除",還是"標記-整理"(也稱作"標記-壓縮"),值得注意的是,複製演算法記憶體也是規整的。

image-20210214115130021

  • 初始化零值: 記憶體分配完成後,虛擬機器需要將分配到的記憶體空間都初始化為零值(不包括物件頭),這⼀步操作保證了物件的例項欄位在 Java 程式碼中可以不賦初始值就直接使⽤,程式能訪問到這些欄位的資料型別所對應的零值。

  • 設定物件頭: 初始化零值完成之後,虛擬機器要對物件進⾏必要的設定,例如這個物件是那個類的例項、如何才能找到類的後設資料資訊、物件的雜湊嗎、物件的 GC 分代年齡等資訊。 這些資訊存放在物件頭中。 另外,根據虛擬機器當前運⾏狀態的不同,如是否啟⽤偏向鎖等,物件頭會有不同的設定⽅式。

  • 執⾏ init ⽅法: 在上⾯⼯作都完成之後,從虛擬機器的視⻆來看,⼀個新的物件已經產⽣了,但從Java 程式的視⻆來看,物件建立才剛開始, ⽅法還沒有執⾏,所有的欄位都還為零。所以⼀般來說,執⾏ new 指令之後會接著執⾏ ⽅法,把物件按照程式設計師的意願進⾏初始化,這樣⼀個真正可⽤的物件才算完全產⽣出來。

3.2、物件的記憶體佈局

在HotSpot虛擬機器裡,物件在堆記憶體中的儲存佈局可以劃分為三個部分:物件頭(Header)、例項資料(Instance Data)和對齊填充(Padding)。

img

HotSpot虛擬機器物件的物件頭部分包括兩類資訊。第一類是用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的鎖、偏向執行緒ID、偏向時間戳等,這部分資料的長度在32位和64位的虛擬機器(未開啟壓縮指標)中分別為32個位元和64個位元,官方稱它為“Mark Word”。

3.3、物件的訪問定位

建⽴物件就是為了使⽤物件,我們的Java程式通過棧上的 reference 資料來操作堆上的具體物件。物件的訪問⽅式有虛擬機器實現⽽定,⽬前主流的訪問⽅式有使⽤控制程式碼和直接指標兩種:

  • 控制程式碼: 如果使⽤控制程式碼的話,那麼Java堆中將會劃分出⼀塊記憶體來作為控制程式碼池,reference 中儲存的就是物件的控制程式碼地址,⽽控制程式碼中包含了物件例項資料與型別資料各⾃的具體地址資訊。

image-20210214120115895

  • 直接指標: 如果使⽤直接指標訪問,那麼 Java 堆物件的佈局中就必須考慮如何放置訪問型別資料的相關資訊,⽽reference 中儲存的直接就是物件的地址。

image-20210214120227426

4、GC垃圾回收

對於垃圾回收,主要考慮的就是完成三件事:

  • 哪些記憶體需要回收?

  • 什麼時候回收?

  • 如何回收?

4.1、如何判斷物件需要回收?

4.1.1、引用計數法

引用計數法的演算法:

  • 在物件中新增一個引用計數器,每當有一個地方引用它時,計數器值就加一;
  • 當引用失效時,計數器值就減一;
  • 任何時刻計數器為零的物件就是不可 能再被使用的。

客觀地說,引用計數演算法(Reference Counting)雖然佔用了一些額外的記憶體空間來進行計數,但它的原理簡單,判定效率也很高,在大多數情況下它都是一個不錯的演算法。也有一些比較著名的應用案例,例如微軟COM(Component Object Model)技術、使用ActionScript 3的FlashPlayer、Python語言以及在遊戲指令碼領域得到許多應用的Squirrel中都使用了引用計數演算法進行記憶體管理。

但是,在Java 領域,至少主流的Java虛擬機器裡面都沒有選用引用計數演算法來管理記憶體,主要原因是,這個看似簡單的演算法有很多例外情況要考慮,例如在處理處理一些相互依賴、迴圈引用時非常複雜。

4.1.2、可達性分析演算法

當前主流的商用程式語言(Java、C#,上溯至前面提到的古老的Lisp)的記憶體管理子系統,都是通過可達性分析(Reachability Analysis)演算法來判定物件是否存活的。這個演算法的基本思路就是通過一系列稱為“GC Roots”的根物件作為起始節點集,從這些節點開始,根據引用關係向下搜尋,搜尋過程所走過的路徑稱為“引用鏈”(Reference Chain),如果某個物件到GC Roots間沒有任何引用鏈相連, 或者用圖論的話來說就是從GC Roots到這個物件不可達時,則證明此物件是不可能再被使用的。


image-20210214121217760

GC Roots 包括;

  • 全域性性引用,對方法區的靜態物件、常量物件的引用

  • 執行上下文,對 Java 方法棧幀中的區域性物件引用、對 JNI handles 物件引用

  • 已啟動且未停止的 Java 執行緒

4.1.3、引用

無論是通過引用計數演算法判斷物件的引用數量,還是通過可達性分析演算法判斷物件是否引用鏈可達,判定物件是否存活都和“引用”離不開關係。

Java的引用分為四種:強引用(Strongly Re-ference)軟引用(Soft Reference)弱引用(Weak Reference)虛引用(Phantom Reference)

  • 強引用是最傳統的“引用”的定義,是指在程式程式碼之中普遍存在的引用賦值,即類似“Object obj=new Object()”這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。

  • 軟引用是用來描述一些還有用,但非必須的物件。只被軟引用關聯著的物件,在系統將要發生記憶體溢位異常前,會把這些物件列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的記憶體, 才會丟擲記憶體溢位異常。Java提供提供了SoftReference類來實現軟引用。

  • 弱引用也是用來描述那些非必須物件,但是它的強度比軟引用更弱一些,被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。當垃圾收集器開始工作,無論當前記憶體是否足夠,都會回收掉只被弱引用關聯的物件。Java提供了WeakReference類來實現弱引用。

  • 虛引用也稱為“幽靈引用”或者“幻影引用”,它是最弱的一種引用關係。一個物件是否有虛引用的 存在,完全不會對其生存時間構成影響,也無法通過虛引用來取得一個物件例項。為一個物件設定虛引用關聯的唯一目的只是為了能在這個物件被收集器回收時收到一個系統通知。Java提供了PhantomReference類來實現虛引用。

4.2、垃圾收集演算法

4.2.1、標記-清除演算法

最早出現也是最基礎的垃圾收集演算法是“標記-清除”(Mark-Sweep)演算法,

演算法分為“標記”和“清除”兩個階段:首先標記出所有需要回收的物件,在標記完成後,統一回收掉所有被標記的物件,也可以反過來,標記存活的物件,統一回

收所有未被標記的物件。標記過程就是物件是否屬於垃圾的判定過程。

後續的收集演算法大多都是以標記-清除演算法為基礎,對其缺點進行改進而得到的。

它的主要缺點有兩個:

  • 第一個是執行效率不穩定,如果Java堆中包含大量物件,而且其中大部分是需要被回收的,這時必須進行大量標記和清除的動作,導致標記和清除兩個過

程的執行效率都隨物件數量增長而降低;

  • 第二個是記憶體空間的碎片化問題,標記、清除之後會產生大量不連續的記憶體碎片,空間碎片太多可能會導致當以後在程式執行過程中需要分配較大物件時無法找到足夠的連續記憶體而不得不提前觸發另一次垃圾收集動作。

標記-清除演算法的執行過程如圖:

image-20210213205500815

4.2.2、標記-複製演算法

標記-複製演算法常被簡稱為複製演算法。為了解決標記-清除演算法面對大量可回收物件時執行效率低的問題。

它將可用記憶體按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當這一塊的記憶體用完了,就將還存活著的物件複製到另外一塊上面,然後再把已使用過的記憶體空間一次清理掉。

這樣實現簡單,執行高效,不過其缺陷也顯而易見,這種複製回收演算法的代價是將可用記憶體縮小為了原來的一半,空間浪費較多。

標記-複製演算法的執行過程如圖所示。

image-20210213205852314

4.2.3、標記-整理演算法

標記-整理演算法的標記過程仍然與“標記-清除”演算法一樣,但後續步驟不是直接對可回收物件進行清理,而是讓所有存活的物件都向記憶體空間一端移動,然後直接清理掉邊界以外的記憶體。

“標記-整理”演算法的示意圖如圖:

image-20210213210117656

4.3、分代收集理論

當前商業虛擬機器的垃圾收集器,大多數都遵循了“分代收集”(Generational Collection)的理論進行設計,分代收集名為理論,實質是一套符合大多數程式執行實際情況的經驗法則,它建立在兩個分代假說之上:

  • 1)弱分代假說(Weak Generational Hypothesis):絕大多數物件都是朝生夕滅的。

  • 2)強分代假說(Strong Generational Hypothesis):熬過越多次垃圾收集過程的物件就越難以消亡。

基於這兩個假說,收集器應該將Java堆劃分出不同的區域,然後將回收物件依據其年齡(年齡即物件熬過垃圾收集過程的次數)分配到不同的區域之中儲存。

設計者一般至少會把Java堆劃分為新生代 (Young Generation)和老年代(Old Generation)兩個區域。顧名思義,在新生代中,每次垃圾收集

時都發現有大批物件死去,而每次回收後存活的少量物件,將會逐步晉升到老年代中存放。

基於這種分代,老年代和新生代具備不同的特點,可以採用不同的垃圾收集演算法。

  • ⽐如在新⽣代中,每次收集都會有⼤量物件死去,所以可以選擇標記-複製演算法,只需要付出少量物件的複製成本就可以完成每次垃圾收集。
  • ⽽⽼年代的物件存活⼏率是⽐較⾼的,⽽且沒有額外的空間對它進⾏分配擔保,所以必須選擇標記-清除標記-整理演算法進⾏垃圾收集。

因為有了分代收集理論,所以就有了了“Minor GC(新⽣代GC)”、“Major GC(⽼年代GC)”、“Full GC(全域性GC)”這樣的回收型別的劃分

4.4、垃圾收集器

4.4.1、Serial收集器

Serial收集器是最基礎、歷史最悠久的收集器,曾經(在JDK 1.3.1之前)是HotSpot虛擬機器新生代收集器的唯一選擇。這個收集器是一個單執行緒工作的收集器,但它的“單線 程”的意義並不僅僅是說明它只會使用一個處理器或一條收集執行緒去完成垃圾收集工作,更重要的是強調在它進行垃圾收集時,必須暫停其他所有工作執行緒,直到它收集結束。

Serial/Serial Old收 集器的執行過程如下:

image-20210213213230637

4.4.2、ParNew收集器

ParNew收集器實質上是Serial收集器的多執行緒並行版本,除了同時使用多條執行緒進行垃圾收集之外,其餘的行為包括Serial收集器可用的所有控制引數(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集演算法、Stop The World、物件分配規則、回收策略等都與Serial收集器完全一致,在實現上這兩種收集器也共用了相當多的程式碼。

ParNew收集器的工作過程如圖所示:

image-20210213213606136

4.4.3、Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同樣是基於標記-複製演算法實現的收集器,也是能夠並行收集的多執行緒收集器

Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。由於與吞吐量關係密切,Parallel Scavenge收集器也經常被稱作“吞吐量優先收集器”。

Parallel Scavenge收集器提供了兩個引數用於精確控制吞吐量,分別是控制最大垃圾收集停頓時間的-XX:MaxGCPauseMillis引數以及直接設定吞吐量大小的-XX:GCTimeRatio引數。

4.4.4、Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同樣是一個單執行緒收集器,使用標記-整理演算法。這個收集器的主要意義也是供客戶端模式下的HotSpot虛擬機器使用。如果在服務端模式下,它也可能有兩種用途:一種是在JDK 5以及之前的版本中與Parallel Scavenge收集器搭配使用,另外一種就是作為CMS 收集器發生失敗時的後備預案,在併發收集發生Concurrent Mode Failure時使用。這兩點都將在後面的內容中繼續講解。

Serial Old收集器的工作過程如圖所示。

image-20210213214008232

4.4.5、Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支援多執行緒併發收集,基於標記-整理演算法實現。這個收集器是直到JDK 6時才開始提供的。Parallel Old收集器的工作過程如圖所示。

image-20210213214130222

4.4.6、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標的收集器。目前很大一部分的Java應用集中在網際網路網站或者基於瀏覽器的B/S系統的服務端上,這類應用通常都會較為關注服務的響應速度,希望系統停頓時間儘可能短,以給使用者帶來良好的互動體驗。CMS收集器就非常符合這類應用的需求。

Concurrent Mark Sweep收集器執行過程如圖:

image-20210213214323197

4.4.7、Garbage First收集器

G1是一款主要面向服務端應用的垃圾收集器,是目前垃圾回收器的前沿成果。HotSpot開發團隊最初賦予它的期望是(在比較長期的)未來可以替換掉JDK 5中釋出的CMS收集器。現在這個期望目標已經實現過半了,JDK 9釋出之日,G1宣告取代Parallel Scavenge加Parallel Old組合,成為服務端模式下的預設垃圾收集器。

G1收集器執行過程如圖:

image-20210213214532807

5、JVM類載入

Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別,這個過程被稱作虛擬機器的類載入機制。

5.1、類載入過程

一個型別從被載入到虛擬機器記憶體中開始,到解除安裝出記憶體為止,它的整個生命週期將會經歷載入 (Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化 (Initialization)、使用(Using)和解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱為連線(Linking)。

過程如下圖:

image-20210214122059420

**載入 **:

“載入”(Loading)階段是整個“類載入”(Class Loading)過程中的一個階段,在載入階段,Java虛擬機器需要完成以下三件事情:

  • 1)通過一個類的全限定名來獲取定義此類的二進位制位元組流。

  • 2)將這個位元組流所代表的靜態儲存結構轉化為堆的執行時資料結構。

  • 3)在記憶體中生成一個代表這個類的java.lang.Class物件,作為方法區這個類的各種資料的訪問入口。

驗證:

驗證是連線階段的第一步,這一階段的目的是確保Class檔案的位元組流中包含的資訊符合《Java虛擬機器規範》的全部約束要求,保證這些資訊被當作程式碼執行後不會危害虛擬機器自身的安全。

驗證階段大致上會完成四個階段的檢驗動作:檔案格式驗證後設資料驗證位元組碼驗證符號引用驗證

準備:

準備階段是正式為類中定義的變數(即靜態變數,被static修飾的變數)分配記憶體並設定類變數初始值的階段。

解析

解析階段是Java虛擬機器將常量池內的符號引用替換為直接引用的過程。

5.2、類載入器

Java虛擬機器設計團隊有意把類載入階段中的“通過一個類的全限定名來獲取描述該類的二進位制位元組流”這個動作放到Java虛擬機器外部去實現,以便讓應用程式自己決定如何去獲取所需的類。實現這個動作的程式碼被稱為“類載入器”(Class Loader)。

5.2.1、類與類載入器

類載入器雖然只用於實現類的載入動作,但它在Java程式中起到的作用卻遠超類載入階段。對於任意一個類,都必須由載入它的類載入器和這個類本身一起共同確立其在Java虛擬機器中的唯一性,每一個類載入器,都擁有一個獨立的類名稱空間。這句話可以表達得更通俗一些:比較兩個類是否“相等”,只有在這兩個類是由同一個類載入器載入的前提下才有意義,否則,即使這兩個類來源於同一個Class檔案,被同一個Java虛擬機器載入,只要載入它們的類載入器不同,那這兩個類就必定不相等。

5.2.2、雙親委派模型

JVM 中內建了三個重要的 ClassLoader,啟動類載入器(Bootstrap ClassLoader),這個類載入器使用C++語言實現,是虛擬機器自身的一部分,其他所有

的類載入器,這些類載入器都由Java語言實現,獨立存在於虛擬機器外部,並且全都繼承自抽象類java.lang.ClassLoader。

  • 啟動類載入器(Bootstrap Class Loader): 這個類載入器負責載入存放在 <JAVA_HOME>\lib目錄,或者被-Xbootclasspath引數所指定的路徑中存放的,而且是Java虛擬機器能夠識別的(按照檔名識別,如rt.jar、tools.jar,名字不符合的類庫即使放在lib目錄中也不會被載入)類庫載入到虛擬機器的記憶體中。
  • 擴充套件類載入器(Extension Class Loader):這個類載入器是在類sun.misc.Launcher$ExtClassLoader 中以Java程式碼的形式實現的。它負責載入<JAVA_HOME>\lib\ext目錄中,或者被java.ext.dirs系統變數所指定的路徑中所有的類庫。
  • 應用程式類載入器(Application Class Loader):這個類載入器由 sun.misc.Launcher$AppClassLoader來實現。由於應用程式類載入器是ClassLoader類中的getSystem-ClassLoader()方法的返回值,所以有些場合中也稱它為“系統類載入器”。它負責載入使用者類路徑 (ClassPath)上所有的類庫,開發者同樣可以直接在程式碼中使用這個類載入器。

雙親委派模型: 如果一個類載入器收到了類載入的請求,它首先不會自己去嘗試載入這個類,而是把這個請求委派給父類載入器去完成,每一個層次的類載入器都是如此,因此所有的載入請求最終都應該傳送到最頂層的啟動類載入器中,只有當父載入器反饋自己無法完成這個載入請求(它的搜尋範圍中沒有找到所需的類)時,子載入器才會嘗試自己去完成載入。

image-20210214124729757

為什麼要使用雙親委派模型來組織類載入器之間的關係呢?一個顯而易見的好處就是Java中的類隨著它的類載入器一起具備了一種帶有優先順序的層次關係。例如類java.lang.Object,它存放在rt.jar之中,無論哪一個類載入器要載入這個類,最終都是委派給處於模型最頂端的啟動類載入器進行載入,因此Object類在程式的各種類載入器環境中都能夠保證是同一個類。反之,如果沒有使用雙親委派模型,都由各個類載入器自行去載入的話,如果使用者自己也編寫了一個名為java.lang.Object的類,並放在程式的 ClassPath中,那系統中就會出現多個不同的Object類,Java型別體系中最基礎的行為也就無從保證,應用程式將會變得一片混亂。

5.2.3、破壞雙親委派模型

過雙親委派模型並不是一個具有強制性約束的模型,而是Java設計者推薦給開發者們的類載入器實現方式。在Java的世界中大部分的類載入器都遵循這個模型,但也有例外的情況,直到Java模組化出現為止,雙親委派模型主要出現過3次較大規模“被破壞”的情況。

  • 第一次破壞:向前相容

    JDK1.2釋出之前,相容之前的程式碼。

  • 第二次破壞:載入SPI介面實現類

第二次被破壞是這個模型自身的缺陷導致的。雙親委派模型很好的解決了各個類載入器的基礎類的統一問題(越基礎的類由越上層的載入器進行載入),基礎類之所以稱為“基礎”,是因為它們總是作為被使用者程式碼呼叫的API, 但沒有絕對,如果基礎類呼叫會使用者的程式碼怎麼辦呢?

這不是沒有可能的。一個典型的例子就是JNDI服務,JNDI現在已經是Java的標準服務,它的程式碼由啟動類載入器去載入(在JDK1.3時就放進去的rt.jar),但它需要呼叫由獨立廠商實現並部署在應用程式的ClassPath下的JNDI介面提供者(SPI, Service Provider Interface)的程式碼,但啟動類載入器不可能“認識“這些程式碼啊。因為這些類不在rt.jar中,但是啟動類載入器又需要載入。怎麼辦呢?

為了解決這個問題,Java設計團隊只好引入了一個不太優雅的設計:執行緒上下文類載入器(Thread Context ClassLoader)。這個類載入器可以通過java.lang.Thread類的setContextClassLoader方法進行設定。如果建立執行緒時還未設定,它將會從父執行緒中繼承一個,如果在應用程式的全域性範圍內都沒有設定過多的話,那這個類載入器預設即使應用程式類載入器。

有了執行緒上下文載入器,JNDI服務使用這個執行緒上下文載入器去載入所需要的SPI程式碼,也就是父類載入器請求子類載入器去完成類載入的動作,這種行為實際上就是打通了雙親委派模型的層次結構來逆向使用類載入器,實際上已經違背了雙親委派模型的一般性原則。但這無可奈何,Java中所有涉及SPI的載入動作基本勝都採用這種方式。例如JNDI,JDBC,JCE,JAXB,JBI等。

  • 第三次破壞:熱部署

雙親委派模型的第三次“被破壞”是由於使用者對程式的動態性的追求導致的。為了實現熱插拔,熱部署,模組化,意思是新增一個功能或減去一個功能不用重啟,只需要把這模組連同類載入器一起換掉就實現了程式碼的熱替換。例如OSGi的出現。在OSGi環境下,類載入器不再是雙親委派模型中的樹狀結構,而是進一步發展為網狀結構。

如果我們自己想定義一個類載入器,破壞雙親委派模型,只需要重寫重寫其中的loadClass方法,使其不進行雙親委派即可。

5.2.4、Tomcat類載入器架構

Tomcat是主流的Java Web伺服器之一,為了實現一些特殊的功能需求,自定義了一些類載入器。

Tomcat類載入器如下:

image-20210214131305597

Tomcat實際上也是破壞了雙親委派模型的。

Tomact是web容器,可能需要部署多個應用程式。不同的應用程式可能會依賴同一個第三方類庫的不同版本,但是不同版本的類庫中某一個類的全路徑名可能是一樣的。如多個應用都要依賴hollis.jar,但是A應用需要依賴1.0.0版本,但是B應用需要依賴1.0.1版本。這兩個版本中都有一個類是com.hollis.Test.class。如果採用預設的雙親委派類載入機制,那麼無法載入多個相同的類。

所以,Tomcat破壞雙親委派原則,提供隔離的機制,為每個web容器單獨提供一個WebAppClassLoader載入器。

Tomcat的類載入機制:為了實現隔離性,優先載入 Web 應用自己定義的類,所以沒有遵照雙親委派的約定,每一個應用自己的類載入器——WebAppClassLoader負責載入本身的目錄下的class檔案,載入不到時再交給CommonClassLoader載入,這和雙親委派剛好相反。

6、JVM故障處理

6.1、基礎故障處理工具

6.1.1、jps:虛擬機器程式狀況工具

jps(JVM Process Status Tool),它的功能與 ps 命令類似,可以列出正在執行的虛擬機器程式,並顯示虛擬機器執行主類(Main Class,main()函式所在的類)

名稱以及這些程式的本地虛擬機器唯一 ID ( Local Virtual Machine Identifier,LVMID),類似於 ps -ef | grep java 的功能。

命令格式

jps [ options ] [ hostid ]

 options:選項、引數,不同的引數可以輸出需要的資訊

 hostid:遠端檢視

選項列表 描述
-q 只輸出程式 ID,忽略主類資訊
-l 輸出主類全名,或者執行 JAR 包則輸出路徑
-m 輸出虛擬機器程式啟動時傳遞給主類 main()函式的引數
-v 輸出虛擬機器程式啟動時的 JVM 引數

6.1.2、jstat:虛擬機器統計資訊監視工具

jstat(JVM Statistics Monitoring Tool),用於監視虛擬機器各種執行狀態資訊。它可以檢視本地或者遠端虛擬機器程式中,類載入、記憶體、垃圾收集、即時編譯等執行時資料。

命令格式

jstat -

  • vmid:如果是檢視遠端機器,需要按照此格式:

[protocol:][//]lvmid[@hostname[:port]/servername]

  • interval 和 count,表示查詢間隔和次數,比如每隔 1000 毫秒查詢一次程式 ID 的gc 收集情況,每次查詢 5 次。jstat -gc 111552 1000 5

選項列表:

選項列表 描述
-class 監視類載入、解除安裝數量、總空間以及類裝載所耗費時長
-gc 監視 Java 堆情況,包括 Eden 區、2 個 Survivor 區、老年代、永久代或者 jdk1.8 元空間等,容量、已用空間、垃圾收集時間合計等資訊
-gccapacity 監視內容與-gc 基本一致,但輸出主要關注 Java 堆各個區域使用到的最大、最小空間
-gcutil 監視內容與-gc 基本相同,但輸出主要關注已使用空間佔總空間的百分比
-gccause 與 -gcutil 功能一樣,但是會額外輸出導致上一次垃圾收集產生的原因
-gcnew 監視新生代垃圾收集情況
-gcnewcapacity 監視內容與 -gcnew 基本相同,輸出主要關注使用到的最大、最小空間
-gcold 監視老年代垃圾收集情況
-gcoldcapacity 監視內容與 -gcold 基本相同,輸出主要關注使用到的最大、最小空間
-compiler 輸出即時編譯器編譯過的方法、耗時等資訊
-printcompilation 輸出已經被即時編譯的方法

6.1.3、jinfo:Java配置資訊工具

jinfo(Configuration Info for Java),實時檢視和調整 JVM 的各項引數。在上面講到 jps -v 指令時,可以看到它把虛擬機器啟動時顯式的引數列表都列印

出來了,但如果想更加清晰的看具體的一個引數或者想知道未被顯式指定的引數時,就可以通過 jinfo -flag 來查詢了。

命令格式

jinfo [ option ] pid

6.1.4、jmap:Java記憶體映像工具

jmap(Memory Map for Java),用於生成堆轉儲快照(heapdump 檔案)。

jmap 的作用除了獲取堆轉儲快照,還可以查詢 finalize 執行佇列、Java 堆和

方法區的詳細資訊。

命令格式

jmap [ option ] pid

 option:選項引數

 pid:需要列印配置資訊的程式 ID

 executable:產生核心 dump 的 Java 可執行檔案

 core:需要列印配置資訊的核心檔案

 server-id:可選的唯一 id,如果相同的遠端主機上執行了多臺除錯伺服器,用此選

項引數標識伺服器

 remote server IP or hostname: 遠端除錯伺服器的 IP 地址或主機名

選項 描述
-dump 生成 Java 堆轉儲快照。
-finalizerinfo 顯示在 F-Queue 中等待 Finalizer 執行緒執行 finalize 方法的物件。Linux平臺
-heap 顯示 Java 堆詳細資訊,比如:用了哪種回收器、引數配置、分代情況。Linux 平臺
-histo 顯示堆中物件統計資訊,包括類、例項數量、合計容量
-permstat 顯示永久代記憶體狀態,jdk1.7,永久代
-F 當虛擬機器程式對 -dump 選項沒有響應式,可以強制生成快照。Linux平臺

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

jhat(JVM Heap Analysis Tool),與 jmap 配合使用,用於分析 jmap 生成的堆轉儲快照。

jhat 內建了一個小型的 http/web 伺服器,可以把堆轉儲快照分析的結果,展示在瀏覽器中檢視。不過用途不大,基本大家都會使用其他第三方工具。

命令格式

jhat [-stack ] [-refs ] [-port ] [-baseline ] [-

debug ] [-version] [-h|-help]

6.1.6、jstack:Java堆疊跟蹤工具

jstack(Stack Trace for Java),用於生成虛擬機器當前時刻的執行緒快照(threaddump、javacore)。

執行緒快照就是當前虛擬機器內每一條執行緒正在執行的方法堆疊的集合,生成執行緒快照的目的通常是定位執行緒出現長時間停頓的原因,如:執行緒死鎖、死迴圈、請求

外部資源耗時較長導致掛起等。

執行緒出現聽頓時通過 jstack 來檢視各個執行緒的呼叫堆疊,就可以獲得沒有響應的執行緒在搞什麼鬼。

命令格式

jstack [ option ] vmid

選項引數:

選項 描述
-F 當正常輸出的請求不被響應時,強制輸出執行緒堆疊
-l 除了堆疊外,顯示關於鎖的附加資訊
-m 如果呼叫的是本地方法的話,可以顯示 c/c++的堆疊

6.2、視覺化故障處理工具

JDK中除了附帶大量的命令列工具外,還提供了幾個功能整合度更高的視覺化工具,使用者可以使用這些視覺化工具以更加便捷的方式進行程式故障診斷和除錯工作。這類工具主要包括JConsole、 JHSDB、VisualVM和JMC四個。

6.2.1、JHSDB:基於服務性代理的除錯工具

JDK中提供了JCMD和JHSDB兩個整合式的多功能工具箱,它們不僅整合了所有 基礎工具所能提供的專項功能,而且由於有著“後發優勢”,能夠做得往往比之前的老工具們更好、更強大。

JHSDB是一款基於服務性代理(Serviceability Agent,SA)實現的程式外除錯工具。

使用以下命令進入JHSDB的圖形化模式,並使其附加程式11180:

jhsdb hsdb --pid 11180

命令開啟的JHSDB的介面:

image-20210214140518407

6.2.2、JConsole:Java監視與管理控制檯

JConsole(Java Monitoring and Management Console)是一款基於JMX(Java Manage-ment Extensions)的視覺化監視、管理工具。它的主要功能是通過JMX的MBean(Managed Bean)對系統進 行資訊收集和引數動態調整。

JConsole連線頁面 :

image-20210214140713141

通過JDK/bin目錄下的jconsole.exe啟動JCon-sole後,會自動搜尋出本機執行的所有虛擬機器程式

image-20210214140905009

6.2.3、VisualVM:多合-故障處理工具

VisualVM(All-in-One Java Troubleshooting Tool)是功能最強大的執行監視和故障處理程式之一, 曾經在很長一段時間內是Oracle官方主力發展的虛擬機器故障處理工具。

它除了常規的執行監視、故障處理外,還可以做效能分析等工作。因為它的通用性很強,對應用程式影響較小,所以可以直接接入到生產環境中。

VisualVM的外掛可以手工進行安裝,在網站上下載nbm包後,點選“工具->外掛->已下載”選單,然後在彈出對話方塊中指定nbm包路徑便可完成安裝。

VisualVM外掛頁籤:

image-20210214141116343

6.2.4、Java Mission Control:可持續線上的監控工具

JMC最初是BEA公司的產品,因此並沒有像VisualVM那樣一開始就基於自家的Net-Beans平臺來開發,而是選擇了由IBM捐贈的Eclipse RCP作為基礎框架,現在的JMC不僅可以下載到獨立程式,更常見的是作為Eclipse的外掛來使用。JMC與虛擬機器之間同樣採取JMX協議進行通訊,JMC一方面作為 JMX控制檯,顯示來自虛擬機器MBean提供的資料;另一方面作為JFR的分析工具,展示來自JFR的資料。

JMC的主介面如圖:

image-20210214141410375


本文是作者結合一些常見面試題學習周志朋老師《深入理解Java虛擬機器:JVM高階特性與最佳實踐》的整理。這本書是非常經典的JVM書籍,也是一部七百多頁的大部頭,強烈建議有空仔細研讀這本書籍,來學習更多JVM的特性和細節。



參考:

【1】:周志朋編著 《深入理解Java虛擬機器:JVM高階特性與最佳實踐(第3版》

【2】:JavaGuide 搞定大廠jvm面試

【3】:小傅哥編著 《Java面經手冊》

【4】:Java記憶體管理-JVM記憶體模型以及JDK7和JDK8記憶體模型對比總結(三)

相關文章