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

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

先上圖:html

JVM必備知識

一、JVM基本概念

1.一、JVM是什麼

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

JVM具有着計算機的基本運算方式,它主要負責把 Java 程序生成的字節碼文件,解釋成具體系統平臺上的機器指令,讓其在各個平臺運行。c++

JVM是運行在操做系統上的,它與硬件沒有直接的交互。程序員

固然,嚴格來講JVM也是虛擬機規範,有不少不一樣的實現,Sun/OracleJDK和OpenJDK中的默認Java虛擬機是HotSpot虛擬機,是目前使用範圍最廣的Java虛擬機,通常講到的JVM默認指的就是HotSpot虛擬機。

1.二、Java程序運行過程

咱們都知道 Java 源文件,經過編譯器,可以生產相應的.Class 文件,也就是字節碼文件,而字節碼文件又經過 Java 虛擬機中的解釋器,編譯成特定機器上的機器碼 。web

也就是以下:面試

image-20210213164039026

每一種平臺的解釋器是不一樣的,可是實現的虛擬機是相同的,這也就是 Java 爲何可以跨平臺的緣由了 ,當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啓動就會存在多個虛擬機實例。程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。算法

1.三、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

二、JVM內存區域

Java虛擬機在執行Java程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域。根據《Java虛擬機規範》的規定,Java虛擬機所管理的內存將會包括如下幾個運行時數據區域:

image-20210213172256916

固然,實際上,爲了更好的適應 CPU 性能提高,最大限度提高JVM 運行效率,JDK中各個版本對JVM進行了一些迭代,示意圖以下:

image-20210213172547779

JDK1.六、JDK1.七、JDK1.8 JVM 內存模型主要有如下差別:

  • JDK 1.6:有永久代,靜態變量存放在永久代上。
  • JDK 1.7:有永久代,但已經把字符串常量池、靜態變量,存放在堆上。逐漸的減小永久代的使用。
  • JDK 1.8:無永久代,運行時常量池、類常量池,都保存在元數據區,也就是常說的元空間。但字符串常量池仍然存放在堆上。

2.一、程序計數器

一塊較小的內存空間, 是當前線程所執行的字節碼的行號指示器,每條線程都要有一個獨立的程序計數器,這類內存也稱爲「線程私有」的內存。

正在執行 java 方法的話,計數器記錄的是虛擬機字節碼指令的地址(當前指令的地址)。若是仍是 Native 方法,則爲空。

這個內存區域是惟一一個在虛擬機中沒有規定任何 OutOfMemoryError 狀況的區域。

2.二、Java虛擬機棧

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

虛擬機棧描述的是Java方法執行的線程內存模型:每一個方法被執行的時候,Java虛擬機都 會同步建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態鏈接、方法出口等信息。每個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。

棧示意圖1

局部變量表存放了編譯期可知的各類Java虛擬機基本數據類型、對象引用(reference類型,它並不等同於對象自己,多是一個指向對象起始址的引用指針,也多是指向一個表明對象的句柄或者其餘與此對象相關的位置)和returnAddress 類型(指向了一條字節碼指令的地址)。

image-20210214142724540

2.三、本地方法棧

本地方法棧(Native Method Stacks)與虛擬機棧所發揮的做用是很是類似的,其區別只是虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native) 方法服務。

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

與虛擬機棧同樣,本地方法棧也會在棧深度溢出或者棧擴展失敗時分別拋出StackOverflowError和OutOfMemoryError異常。

2.四、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.五、方法區(JDK1.8移除)

方法區(Method Area)與Java堆同樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載 的類型信息、常量、靜態變量、即時編譯器編譯後的代碼緩存等數據。

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

在JDK 6的 時候HotSpot開發團隊就有放棄永久代,逐步改成採用本地內存(Native Memory)來實現方法區的計劃了,到了JDK 7的HotSpot,已經把本來放在永久代的字符串常量池、靜態變量等移出,而到了 JDK 8,終於徹底廢棄了永久代的概念,改用在本地內存中實現的元空間(Meta- space)來代替,把JDK 7中永久代還剩餘的內容(主要是類型信息)所有移到元空間中。

若是方法區沒法知足新的內存分配需求時,將拋出OutOfMemoryError異常。

2.六、運行時常量池

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

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

2.七、直接內存

直接內存(Direct Memory)並非虛擬機運行時數據區的一部分。

顯然,本機直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,則確定仍是會受到本機總內存(包括物理內存、SWAP分區或者分頁文件)大小以及處理器尋址空間的限制。

元空間從虛擬機 Java 堆中轉移到本地內存,默認狀況下,元空間的大小僅受本地內存的限制。jdk1.8 之前版本的 class 和 JAR 包數據存儲在 PermGen 下面 ,PermGen 大小是固定的,並且項目之間沒法共用,公有的 class,因此比較容易出現 OOM 異常。

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

三、JVM中的對象

上面已經瞭解Java虛擬機的運行時數據區域,咱們接下來更進一步瞭解這些虛擬機內存中數據的其餘細節,譬如它們是如何建立、如何佈局以及如何訪問的。以最經常使用的虛擬機HotSpot和最經常使用的內存區域Java堆爲例,瞭解一下HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。

3.一、對象的建立

Java對象建立的大概過程以下:

image-20210214114717010

  • 類加載檢查: 虛擬機遇到⼀條 new 指令時,⾸先將去檢查這個指令的參數是否能在常量池中定位到這個類的符號引⽤,而且檢查這個符號引⽤表明的類是否已被加載過、解析和初始化過。若是沒有,那必須先執⾏相應的類加載過程。
  • 分配內存: 在類加載檢查經過後,接下來虛擬機將爲新⽣對象分配內存。對象所需的內存⼤⼩在類加載完成後即可肯定,爲對象分配空間的任務等同於把⼀塊肯定⼤⼩的內存從 Java 堆中劃分出來。分配⽅式有 指針碰撞空閒列表 兩種,選擇那種分配⽅式由 Java 堆是否規整決定,⽽Java堆是否規整⼜由所採⽤的垃圾收集器是否帶有壓縮整理功能決定。
內存分配的兩種⽅式:選擇以上兩種⽅式中的哪⼀種,取決於 Java 堆內存是否規整。⽽ Java 堆內存是否規整,取決於 GC收集器的算法是"標記-清除",仍是"標記-整理"(也稱做"標記-壓縮"),值得注意的是,複製算法內存也是規整的。

image-20210214115130021

  • 初始化零值: 內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),這⼀步操做保證了對象的實例字段在 Java 代碼中能夠不賦初始值就直接使⽤,程序能訪問到這些字段的數據類型所對應的零值。
  • 設置對象頭: 初始化零值完成以後,虛擬機要對對象進⾏必要的設置,例如這個對象是那個類的實例、如何才能找到類的元數據信息、對象的哈希嗎、對象的 GC 分代年齡等信息。 這些信息存放在對象頭中。 另外,根據虛擬機當前運⾏狀態的不一樣,如是否啓⽤偏向鎖等,對象頭會有不一樣的設置⽅式。
  • 執⾏ init ⽅法: 在上⾯⼯做都完成以後,從虛擬機的視⻆來看,⼀個新的對象已經產⽣了,但從Java 程序的視⻆來看,對象建立纔剛開始, <init> ⽅法尚未執⾏,全部的字段都還爲零。因此⼀般來講,執⾏ new 指令以後會接着執⾏ <init> ⽅法,把對象按照程序員的意願進⾏初始化,這樣⼀個真正可⽤的對象纔算徹底產⽣出來。

3.二、對象的內存佈局

在HotSpot虛擬機裏,對象在堆內存中的存儲佈局能夠劃分爲三個部分:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

img

HotSpot虛擬機對象的對象頭部分包括兩類信息。第一類是用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機(未開啓壓縮指針)中分別爲32個比特和64個比特,官方稱它爲「Mark Word」。

3.三、對象的訪問定位

建⽴對象就是爲了使⽤對象,咱們的Java程序經過棧上的 reference 數據來操做堆上的具體對象。對象的訪問⽅式有虛擬機實現⽽定,⽬前主流的訪問⽅式有使⽤句柄和直接指針兩種:

  • 句柄: 若是使⽤句柄的話,那麼Java堆中將會劃分出⼀塊內存來做爲句柄池,reference 中存儲的就是對象的句柄地址,⽽句柄中包含了對象實例數據與類型數據各⾃的具體地址信息。

image-20210214120115895

  • 直接指針: 若是使⽤直接指針訪問,那麼 Java 堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,⽽reference 中存儲的直接就是對象的地址。

image-20210214120227426

四、GC垃圾回收

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

  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

4.一、如何判斷對象須要回收?

4.1.一、引用計數法

引用計數法的算法:

  • 在對象中添加一個引用計數器,每當有一個地方引用它時,計數器值就加一;
  • 當引用失效時,計數器值就減一;
  • 任什麼時候刻計數器爲零的對象就是不可 能再被使用的。

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

可是,在Java 領域,至少主流的Java虛擬機裏面都沒有選用引用計數算法來管理內存,主要緣由是,這個看似簡單的算法有不少例外狀況要考慮,例如在處理處理一些相互依賴、循環引用時很是複雜。

4.1.二、可達性分析算法

當前主流的商用程序語言(Java、C#,上溯至前面提到的古老的Lisp)的內存管理子系統,都是經過可達性分析(Reachability Analysis)算法來斷定對象是否存活的。這個算法的基本思路就是經過一系列稱爲「GC Roots」的根對象做爲起始節點集,從這些節點開始,根據引用關係向下搜索,搜索過程所走過的路徑稱爲「引用鏈」(Reference Chain),若是某個對象到GC Roots間沒有任何引用鏈相連, 或者用圖論的話來講就是從GC Roots到這個對象不可達時,則證實此對象是不可能再被使用的。


image-20210214121217760

GC Roots 包括;

  • 全局性引用,對方法區的靜態對象、常量對象的引用
  • 執行上下文,對 Java 方法棧幀中的局部對象引用、對 JNI handles 對象引用
  • 已啓動且未中止的 Java 線程

4.1.三、引用

不管是經過引用計數算法判斷對象的引用數量,仍是經過可達性分析算法判斷對象是否引用鏈可達,斷定對象是否存活都和「引用」離不開關係。

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

  • 強引用是最傳統的「引用」的定義,是指在程序代碼之中廣泛存在的引用賦值,即相似「Object obj=new Object()」這種引用關係。不管任何狀況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用是用來描述一些還有用,但非必須的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,若是此次回收尚未足夠的內存, 纔會拋出內存溢出異常。Java提供提供了SoftReference類來實現軟引用。
  • 弱引用也是用來描述那些非必須對象,可是它的強度比軟引用更弱一些,被弱引用關聯的對象只能生存到下一次垃圾收集發生爲止。當垃圾收集器開始工做,不管當前內存是否足夠,都會回收掉只被弱引用關聯的對象。Java提供了WeakReference類來實現弱引用。
  • 虛引用也稱爲「幽靈引用」或者「幻影引用」,它是最弱的一種引用關係。一個對象是否有虛引用的 存在,徹底不會對其生存時間構成影響,也沒法經過虛引用來取得一個對象實例。爲一個對象設置虛引用關聯的惟一目的只是爲了能在這個對象被收集器回收時收到一個系統通知。Java提供了PhantomReference類來實現虛引用。

4.二、垃圾收集算法

4.2.一、標記-清除算法

最先出現也是最基礎的垃圾收集算法是「標記-清除」(Mark-Sweep)算法,

算法分爲「標記」和「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後,統一回收掉全部被標記的對象,也能夠反過來,標記存活的對象,統一回

收全部未被標記的對象。標記過程就是對象是否屬於垃圾的斷定過程。

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

它的主要缺點有兩個:

  • 第一個是執行效率不穩定,若是Java堆中包含大量對象,並且其中大部分是須要被回收的,這時必須進行大量標記和清除的動做,致使標記和清除兩個過

程的執行效率都隨對象數量增加而下降;

  • 第二個是內存空間的碎片化問題,標記、清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使當之後在程序運行過程當中須要分配較大對象時沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

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

image-20210213205500815

4.2.二、標記-複製算法

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

它將可用內存按容量劃分爲大小相等的兩塊,每次只使用其中的一塊。當這一塊的內存用完了,就將還存活着的對象複製到另一塊上面,而後再把已使用過的內存空間一次清理掉。

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

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

image-20210213205852314

4.2.三、標記-整理算法

標記-整理算法的標記過程仍然與「標記-清除」算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向內存空間一端移動,而後直接清理掉邊界之外的內存。

「標記-整理」算法的示意圖如圖:

image-20210213210117656

4.三、分代收集理論

當前商業虛擬機的垃圾收集器,大多數都遵循了「分代收集」(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.一、Serial收集器

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

Serial/Serial Old收 集器的運行過程以下:

image-20210213213230637

4.4.二、ParNew收集器

ParNew收集器實質上是Serial收集器的多線程並行版本,除了同時使用多條線程進行垃圾收集以外,其他的行爲包括Serial收集器可用的全部控制參數(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、對象分配規則、回收策略等都與Serial收集器徹底一致,在實現上這兩種收集器也共用了至關多的代碼。

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

image-20210213213606136

4.4.三、Parallel Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它一樣是基於標記-複製算法實現的收集器,也是可以並行收集的多線程收集器

Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput)。因爲與吞吐量關係密切,Parallel Scavenge收集器也常常被稱做「吞吐量優先收集器」。

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

4.4.四、Serial Old收集器

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

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

image-20210213214008232

4.4.五、Parallel Old收集器

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

image-20210213214130222

4.4.六、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間爲目標的收集器。目前很大一部分的Java應用集中在互聯網網站或者基於瀏覽器的B/S系統的服務端上,這類應用一般都會較爲關注服務的響應速度,但願系統停頓時間儘量短,以給用戶帶來良好的交互體驗。CMS收集器就很是符合這類應用的需求。

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

image-20210213214323197

4.4.七、Garbage First收集器

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

G1收集器運行過程如圖:

image-20210213214532807

五、JVM類加載

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

5.一、類加載過程

一個類型從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期將會經歷加載 (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.二、類加載器

Java虛擬機設計團隊有意把類加載階段中的「經過一個類的全限定名來獲取描述該類的二進制字節流」這個動做放到Java虛擬機外部去實現,以便讓應用程序本身決定如何去獲取所需的類。實現這個動做的代碼被稱爲「類加載器」(Class Loader)。

5.2.一、類與類加載器

類加載器雖然只用於實現類的加載動做,但它在Java程序中起到的做用卻遠超類加載階段。對於任意一個類,都必須由加載它的類加載器和這個類自己一塊兒共同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。這句話能夠表達得更通俗一些:比較兩個類是否「相等」,只有在這兩個類是由同一個類加載器加載的前提下才有意義,不然,即便這兩個類來源於同一個Class文件,被同一個Java虛擬機加載,只要加載它們的類加載器不一樣,那這兩個類就一定不相等。

5.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.三、破壞雙親委派模型

過雙親委派模型並非一個具備強制性約束的模型,而是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.四、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加載,這和雙親委派恰好相反。

六、JVM故障處理

6.一、基礎故障處理工具

6.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.二、jstat:虛擬機統計信息監視工具

jstat(JVM Statistics Monitoring Tool),用於監視虛擬機各類運行狀態信息。它能夠查看本地或者遠程虛擬機進程中,類加載、內存、垃圾收集、即時編譯等運行時數據。

命令格式

jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]

  • 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.三、jinfo:Java配置信息工具

jinfo(Configuration Info for Java),實時查看和調整 JVM 的各項參數。在上面講到 jps -v 指令時,能夠看到它把虛擬機啓動時顯式的參數列表都打印

出來了,但若是想更加清晰的看具體的一個參數或者想知道未被顯式指定的參數時,就能夠經過 jinfo -flag 來查詢了。

命令格式

jinfo [ option ] pid

6.1.四、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.五、jhat:虛擬機堆轉儲快照分析工具

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

jhat 內置了一個小型的 http/web 服務器,能夠把堆轉儲快照分析的結果,展現在瀏覽器中查看。不過用途不大,基本你們都會使用其餘第三方工具。

命令格式

jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-

debug <int>] [-version] [-h|-help] <file>

6.1.六、jstack:Java堆棧跟蹤工具

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

線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的目的一般是定位線程出現長時間停頓的緣由,如:線程死鎖、死循環、請求

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

線程出現聽頓時經過 jstack 來查看各個線程的調用堆棧,就能夠得到沒有響應的線程在搞什麼鬼。

命令格式

jstack [ option ] vmid

選項參數:

選項 描述
-F 當正常輸出的請求不被響應時,強制輸出線程堆棧
-l 除了堆棧外,顯示關於鎖的附加信息
-m 若是調用的是本地方法的話,能夠顯示 c/c++的堆棧

6.二、可視化故障處理工具

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

6.2.一、JHSDB:基於服務性代理的調試工具

JDK中提供了JCMD和JHSDB兩個集成式的多功能工具箱,它們不只整合了全部 基礎工具所能提供的專項功能,並且因爲有着「後發優點」,可以作得每每比以前的老工具們更好、更強大。

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

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

jhsdb hsdb --pid 11180

命令打開的JHSDB的界面:

image-20210214140518407

6.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.三、VisualVM:多合-故障處理工具

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

它除了常規的運行監視、故障處理外,還能夠作性能分析等工做。由於它的通用性很強,對應用程序影響較小,因此能夠直接接入到生產環境中。

VisualVM的插件能夠手工進行安裝,在網站上下載nbm包後,點擊「工具->插件->已下載」菜單,而後在彈出對話框中指定nbm包路徑即可完成安裝。

VisualVM插件頁籤:

image-20210214141116343

6.2.四、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的特性和細節。



<big>參考:</big>

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

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

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

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

相關文章
相關標籤/搜索