1. 程式人生 > >jvm整體架構圖文詳解

jvm整體架構圖文詳解

今天學習了jvm三大組成部分(jvm類載入器,jvm記憶體結構,jvm執行引擎)的記憶體結構,現在把學習筆記總結記錄一下,當作複習吧。

1.jvm的概念

JVM(虛擬機器):指以軟體的方式模擬具有完整硬體系統功能、執行在一個完全隔離環境中的完整計算機系統 ,是物理機的軟體實現。

jvm和VMware,Virtual Box等虛擬機器一樣,都是執行在作業系統之上的計算機系統。

首先我們來看看jvm的整體架構的劃分:

大家在留意記憶體結構的時候是不是發現,本地方法棧,程式計算器和java棧顏色一樣的,而方法區和堆卻不一樣呢,其實是這樣子,在一個java程序中可能有很多正在執行的java執行緒,那麼在每一個java執行緒中都會獨立開闢本地方法棧,程式計算器,和Java棧的,而方法區和堆並不是獨立開闢的,他們之間是可以共享的。

記憶體結構每個組成模組的具體概念

本地方法棧(執行緒私有)登記native方法,在Execution Engine執行時載入本地方法庫

程式計數器執行緒私有就是一個指標,指向方法區中的方法位元組碼(用來儲存指向下一條指令的地址,也即將要執行的指令程式碼),由執行引擎讀取下一條指令,是一個非常小的記憶體空間,幾乎可以忽略不記。

方法區(執行緒共享)類的所有欄位和方法位元組碼,以及一些特殊方法如建構函式,介面程式碼也在此定義。簡單說,所有定義的方法的資訊都儲存在該區域,靜態變數+常量+類資訊(構造方法/介面定義)+執行時常量池都存在方法區中,雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它卻有一個別名叫做

Non-Heap(非堆),目的應該是與 Java 堆區分開來。

Java棧(執行緒私有 Java執行緒執行方法的記憶體模型一個執行緒對應一個棧,每個方法在執行的同時都會建立一個棧幀(用於儲存區域性變量表,運算元棧,動態連結,方法出口等資訊不存在垃圾回收問題,只要執行緒一結束該棧就釋放,生命週期和執行緒一致

 JVM對該區域規範了兩種異常:

1) 執行緒請求的棧深度大於虛擬機器棧所允許的深度,將丟擲StackOverFlowError異常

2) 若虛擬機器棧可動態擴充套件,當無法申請到足夠記憶體空間時將丟擲OutOfMemoryError,通過jvm引數Xss

指定棧空間,空間大小決定函式呼叫的深度

(執行緒共享):虛擬機器啟動時建立,用於存放物件例項,幾乎所有的物件(包含常量池)都在堆上分配記憶體,當物件無法再該空間申請到記憶體時將丟擲OutOfMemoryError異常。同時也是垃圾收集器管理的主要區域。可通過 -Xmx –Xms 引數來分別指定最大堆和最小堆

接下我們通過具體的程式碼結合畫圖解析記憶體結構中每一個模組

我們結合上面的一個很簡單的程式碼來看看記憶體結構是怎麼工作的

首先我們通過doc命令編譯該java程式碼會產生一個class檔案,不過我們一般都不會去研究位元組碼檔案的(也有專門的文件去解讀的),這沒有意義,但我們又想了解他們工作原理呢,這時候我們可以藉助java本身的指令反編譯class檔案

這樣子我們就可以結合jvm指令集(網上可以下載),下面就是反編譯class檔案產生的檔案

那麼加入有個java執行緒1執行math.class這個檔案

那麼,該執行緒就會開闢自己獨自的java棧,本地方法區和程式計算器如圖:

而且在執行main方法的時候,main方法就會進入java棧,當執行到math.math()的時候,math()方法也會入棧,在每一個方法入棧的時候java棧都是獨立開闢一個新的記憶體給每一個方法,這個新開闢的記憶體塊成為棧幀,而且棧幀中也會存在很多模組,主要四個是,區域性變量表,運算元棧,動態連結,方法出口。

那麼我們可以結合jvm的指令詳細看看其中執行的原理(我們主要看math方法):

下面的我已經插好的jvm指令的解釋:(0,1,2...這是地址的簡稱而已,實際上是32位或者64位樣子存在的)

那麼當執行0這句程式碼的時候是表示1進入運算元棧

 當執行0這句程式碼的時候是表示1 出棧而且進去區域性變量表

那麼執行2和3是一樣的道理,執行完就是如下;:

那麼執行4和5呢,從區域性變數1和2中裝載int型別值進去運算元棧中,如下:

那麼執行  6: iadd        //iadd 執行int型別的加法,右如下:會棧中的兩個數會出棧進行加法運輸算然後就是把得到的新資料重新入棧

執行 7: bipush        12  //bipush 將一個8位帶符號整數壓入棧 常量12入棧

執行 9: imul                //imul 執行int型別的乘法(不知道大家有沒有發現上面執行7的地址現在就跳到了9,那麼8哪去了,其實壓入棧的12就是佔了一個地址碼),其實四則運算都是一樣的,都是把其中參加運算的資料出棧然後計算得到新的資料,把新的資料入棧

執行:istore_3    //istore_3 將int型別值存入區域性變數3

11: iload_3            //iload_3 從區域性變數3中裝載int型別值

12: ireturn            //ireturn 從方法中返回int型別的資料,還會返回當時進去math()的時候地址,這樣子就可以回覆主方法中原來執行的程式碼

到此,math()就全部執行成功了。

是不是很疑惑程式計算器和動態連結是怎麼用的呢,其實呢,程式計算器就比如一個指標,開始的時候它是0,然後隨著上面指令的執行,它就會指向相對應執行的指令。如圖:執行12: ireturn            //ireturn 從方法中返回int型別的資料的時候

那麼什麼動態連結呢,其實很好理解,就是多型的應用,如我們定義一個集合 Map<> map = new HashMap<>();這Map是一個介面,我們用map.put()的時候,實際是呼叫了Map的實現類,HashMap<>,所以我們去找動態去找HashMap,這就是動態連線。

那麼本地方法棧又是怎麼解析呢

舉個例子,例如我們new Thread.start(); 並且呼叫start方法,但其實不是馬上執行,它還要讓執行一個start0的方法讓cpu準備好了才會執行看圖:

那麼我們再看看start0的方法:

這 start0看起是不是很像一個介面啦,沒有實現,其實它就是一個介面,是本地方法介面,它的底層實現是通過c語言實現的,java就是通過本地介面的方法區呼叫c語言寫的系統(不過現在本地方法使用很少了,很少java和C語言混合使用)

 讓剛剛那個new Thread.start();中start()方法就會就去本地方法棧的,當前本地方法棧也是有棧幀的,然後執行引擎就會呼叫本地方法介面,本質呼叫本地C語言實驗的類庫。

那麼接下來看看堆,像new出來的物件都是放在堆裡面的,當然裡面還有其他的,就在這次說了

++方法區的互動關係

HotSpot是使用指標的方式來訪問物件,Java堆中會存放訪問類元資料的地址,reference儲存的就直接是物件的地址

(執行緒共享):虛擬機器啟動時建立,用於存放物件例項,幾乎所有的物件(包含常量池)都在堆上分配記憶體,當物件無法再該空間申請到記憶體時將丟擲OutOfMemoryError異常。同時也是垃圾收集器管理的主要區域。可通過 -Xmx –Xms 引數來分別指定最大堆和最小堆

新生區

類誕生、成長、消亡的區域,一個類在這裡產生,應用,最後被垃圾回收器收集,結束生命。

新生區分為兩部分: 伊甸區(Eden space)和倖存者區(Survivor pace) ,所有的類都是在伊甸區被new出來的。倖存區有兩個: 0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程式又需要建立物件,JVM的垃圾回收器將對伊甸園區進行垃圾回收(Minor GC),將伊甸園區中的不再被其他物件所引用的物件進行銷燬。然後將伊甸園中的剩餘物件移動到倖存 0區。若倖存 0區也滿了,再對該區進行垃圾回收,然後移動到1區。那如果1區也滿了呢?

老年區

新生區經過多次GC仍然存活的物件移動到老年區。若老年區也滿了,那麼這個時候將產生MajorGCFullGC,進行老年區的記憶體清理。若老年區執行了Full GC之後發現依然無法進行物件的儲存,就會產生OOM異常“OutOfMemoryError

元資料區元資料區取代了永久代(jdk1.8以前),本質和永久代類似,都是JVM規範中方法區的實現,區別在於元資料區並不在虛擬機器中,而是使用本地實體記憶體,永久代在虛擬機器中,永久代邏輯結構上屬於堆,但是物理上不屬於堆,堆大小=新生代+老年代。元資料區也有可能發生OutOfMemory異常。

Jdk1.6及之前: 有永久代, 常量池在方法區

Jdk1.7:       有永久代,但已經逐步“去永久代”,常量池在堆

Jdk1.8及之後: 無永久代,常量池在元空間

元資料區的動態擴充套件,預設XX:MetaspaceSize值為21MB的高水位線。一旦觸及則Full GC將被觸發並解除安裝沒有用的類(類對應的類載入器不再存活),然後高水位線將會重置。新的高水位線的值取決於GC後釋放的元空間。如果釋放的空間少,這個高水位線則上升。如果釋放空間過多,則高水位線下降。

為什麼jdk1.8用元資料區取代了永久代?

官方解釋:移除永久代是為融合HotSpot JVMJRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代

執行引擎:讀取執行時資料區的Java位元組碼並逐個執行

JIT:在Java程式語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的位元組碼(包括需要被解釋的指令的程式)轉換成可以直接傳送給處理器的指令的程式。

位元組碼直譯器:需要使用哪句程式碼才編譯。

記憶體結構圖:全圖

類載入器深入解析

Java執行時編譯原始碼(.java)成位元組碼,由jre執行。jre由java虛擬機器(jvm)實現。Jvm分析位元組碼,後解釋並執行

那麼我們看看類載入過程是怎麼樣的(類載入的過程,如下圖)

類載入:類載入器將class檔案載入到虛擬機器的記憶體

載入:類載入過程的一個階段:通過一個類的完全限定查詢此類位元組碼檔案,並利用位元組碼檔案建立一個Class物件

驗證:目的在於確保Class檔案的位元組流中包含資訊符合當前虛擬機器要求,不會危害虛擬機器自身安全。主要包括四種驗證,檔案格式驗證,元資料驗證,位元組碼驗證,符號引用驗證。

準備:為類變數(即static修飾的欄位變數)分配記憶體並且設定該類變數的初始值即0(如static int i=5;這裡只將i初始化為0,至於5的值將在初始化時賦值),這裡不包含用final修飾的static,因為final在編譯的時候就會分配了,注意這裡不會為例項變數分配初始化,類變數會分配在方法區中,而例項變數是會隨著物件一起分配到Java堆中。

解析:主要將常量池中的符號引用替換為直接引用的過程。符號引用就是一組符號來描述目標,可以是任何字面量,而直接引用就是直接指向目標的指標、相對偏移量或一個間接定位到目標的控制代碼。有類或介面的解析,欄位解析,類方法解析,介面方法解析(這裡涉及到位元組碼變數的引用,如需更詳細瞭解

初始化:類載入最後階段,若該類具有超類,則對其進行初始化,執行靜態初始化器和靜態初始化成員變數(如前面只初始化了預設值的static變數將會在這個階段賦值,成員變數也將被初始化)。

類載入器種類

啟動類載入器(引導類載入器):負責載入JRE的核心類庫,如jre目標下的rt.jar,charsets.jar等

擴充套件類載入器:負責載入JRE擴充套件目錄ext中JAR類包

系統類載入器:負責載入ClassPath路徑下的類包

使用者自定義載入器:負責載入使用者自定義路徑下的類包

我們結合程式碼看看一看具體類載入器的種類。看如下程式碼他們的列印的類載入器是什麼。

 

 我們可以看到第一個載入的名字居然返回一個null,為什麼呢,是因為啟動類載入器是用C語言寫的,Java程式碼沒法獲得器名稱

sun.misc.Launcher$ExtClassLoader:擴充套件類載入器:負責載入JRE擴充套件目錄ext中JAR類包

sun.misc.Launcher$AppClassLoader:系統類載入器:負責載入ClassPath路徑下的類包(一般都是用寫的類)

那麼類載入器又是怎樣去載入類的呢(類載入機制

全盤負責委託機制當一個ClassLoader載入一個類時,除非顯示的使用另一個ClassLoader,否則該類所依賴和引用的類也由這個ClassLoader載入。舉個例子,例如我們定義了一個類A,是通過classload1來載入的,在A裡面引用了我們定義的B和C兩個,如果沒有強制指定其他的型別載入器,那麼B和C也是又classload1負責載入的

雙親委派機制:指先委託父類載入器尋找目標類,在找不到的情況下在自己的路徑中查詢並載入目標類。

我們看一下雙親委派機制的結構圖。如下

解析一下什麼交自定義載入器,這個不是jdk提供的,例如中間間有時候就會用得上自定義載入器,如tomcat就可以定義載入器

那麼我們為什麼要使用雙親委派機制呢?

其實,這樣子是為了避免java的核心類庫被噁心修改,這樣子雙親委派機制就有有兩大優勢

雙親委派模式優勢

沙箱安全機制:自己寫的String.class類不會被載入,這樣便可以防止核心API庫被隨意篡改

避免類的重複載入:當父親已經載入了該類時,就沒有必要子ClassLoader再   載入一次

這個我們可以用Java程式碼演示一下:

如我們自定義一個String類,而且包名也是java.lang下的,我們看看結果會怎樣

那麼它的執行結果會怎樣呢?

它居然報錯了,說沒有主方法,其實這正說明了,啟動類載入器把jdk中定義的String載入進記憶體了,而不是我們自定義的 String類,這避免了我們自定義jdk的類庫噁心修改。那麼我們也想用同樣的類名怎麼辦呢,修改包名就可以了

類載入過程

JVM載入jar包是否會將包裡的所有類全部載入進記憶體?

JVM對class檔案是按需載入(執行期間動態載入),非一次性載入,見示例(啟動需要加上引數:-verbose:class)

我們新增jvm的引數執行看看結果怎麼樣(新增引數的方式。這是IDEA的,不過eclipse也是大同小異)

輸出:

從這裡我們可以發現 VM對class檔案是按需載入(執行期間動態載入),非一次性載入

到這裡,jvm大體的架構就完畢了。