1. 程式人生 > >深入詳細講解JVM原理

深入詳細講解JVM原理

一、JVM體系結構:

     類裝載器ClassLoader:用來裝載.class檔案

     執行引擎:執行位元組碼,或者執行本地方法

     執行時資料區方法區、堆、Java棧、程式計數器、本地方法棧

JVM把描述類資料的位元組碼.Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的java型別,這就是虛擬機器的類載入機制

二、JVM原理:

JVM是java的核心和基礎,在java編譯器和os平臺之間的虛擬處理器。它是一種利用軟體方法實現的抽象的計算機基於下層的作業系統和硬體平臺,可以在上面執行java的位元組碼程式。java編譯器只要面向JVM,生成JVM能理解的程式碼或位元組碼檔案。Java原始檔經編譯成位元組碼程式,通過JVM將每一條指令翻譯成不同平臺機器碼,通過特定平臺執行


三、JVM執行程式的過程:

1、載入.class檔案

2、管理並分配記憶體

3、執行垃圾收集

四步完成JVM環境:

1、建立JVM裝載環境和配置

2、裝載JVM.dll

3、初始化JVM.dll並掛界到JNIENV(JNI呼叫介面)例項

4、呼叫JNIEnv例項裝載並處理class類。

四、JVM的生命週期:

JVM例項和JVM執行引擎例項:

(1)JVM例項對應了一個獨立執行的java程式——程序級別

         一個執行時的Java虛擬機器(JVM)負責執行一個Java程式。

         當啟動一個Java程式時,一個虛擬機器例項誕生;當程式關閉退出,這個虛擬機器例項也就隨之消亡。

         如果在同一臺計算機上同時執行多個Java程式,將得到多個Java虛擬機器例項,每個Java程式都運行於它自己的Java虛擬機器例項中。

(2)JVM執行引擎例項則對應了屬於執行程式的執行緒——執行緒級別

JVM的生命週期:

(1)JVM例項的誕生

當啟動一個Java程式時,一個JVM例項就產生了,任何一個擁有public static void main(String[] args)函式的class都可以作為JVM例項執行的起點。

(2)JVM例項的執行

main()作為該程式初始執行緒的起點,任何其他執行緒均由該執行緒啟動。JVM內部有兩種執行緒:守護執行緒和非守護執行緒main()屬於非守護執行緒,守護執行緒通常由JVM自己使用,java程式也可以標明自己建立的執行緒是守護執行緒。

(3)JVM例項的消亡

當程式中的所有非守護執行緒都終止時,

JVM才退出;若安全管理器允許,程式也可以使用java.lang.Runtime類或者java.lang.System.exit()來退出。

五、類裝載器:

類從被載入到虛擬機器記憶體中開始,到卸載出記憶體為止,它的生命週期包括了:載入(Loading)、驗證(Verification)、準備(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、解除安裝(Unloading)七個階段,其中驗證、準備、解析三個部分統稱連結


1.載入:(重點)
載入階段是“類載入機制”中的一個階段,這個階段通常也被稱作“裝載”,主要完成:
1.通過“類全名”來獲取定義此類的二進位制位元組流

2.將位元組流所代表的靜態儲存結構轉換為方法區的執行時資料結構

3.在java堆中生成一個代表這個類的java.lang.Class物件,作為方法區這些資料的訪問入口

相對於類載入過程的其他階段,載入階段(準備地說,是載入階段中獲取類的二進位制位元組流的動作)是開發期可控性最強的階段,因為載入階段可以使用系統提供的類載入器(ClassLoader)來完成,也可以由使用者自定義的類載入器完成,開發人員可以通過定義自己的類載入器去控制位元組流的獲取方式。

載入階段完成後,虛擬機器外部的二進位制位元組流就按照虛擬機器所需的格式儲存在方法區之中,方法區中的資料儲存格式有虛擬機器實現自行定義,虛擬機器並未規定此區域的具體資料結構。然後在java堆中例項化一個java.lang.Class類的物件,這個物件作為程式訪問方法區中的這些型別資料的外部介面。

2.驗證:(瞭解)

驗證是連結階段的第一步,這一步主要的目的是確保class檔案的位元組流中包含的資訊符合當前虛擬機器的要求,並且不會危害虛擬機器自身安全。
驗證階段主要包括四個檢驗過程:檔案格式驗證、元資料驗證、位元組碼驗證和符號引用驗證。

3.準備:(瞭解)

準備階段是正式為類變數分配記憶體並設定類變數初始值的階段,這些記憶體都將在方法區中進行分配。這個階段中有兩個容易產生混淆的知識點,首先是這時候進行記憶體分配的僅包括類變數(static 修飾的變數),而不包括例項變數,例項變數將會在物件例項化時隨著物件一起分配在java堆中。其次是這裡所說的初始值“通常情況”下是資料型別的零值,假設一個類變數定義為:

public static int value  = 12;

那麼變數value在準備階段過後的初始值為0而不是12,因為這時候尚未開始執行任何java方法,而把value賦值為123的putstatic指令是程式被編譯後,存放於類構造器<clinit>()方法之中,所以把value賦值為12的動作將在初始化階段才會被執行。

上面所說的“通常情況”下初始值是零值,那相對於一些特殊的情況,如果類欄位的欄位屬性表中存在ConstantValue屬性,那在準備階段變數value就會被初始化為ConstantValue屬性所指定的值,建設上面類變數value定義為:

public static final int value = 123;

編譯時javac將會為value生成ConstantValue屬性,在準備階段虛擬機器就會根據ConstantValue的設定將value設定為123。

4.解析:(瞭解)
解析階段是虛擬機器常量池內的符號引用替換為直接引用的過程。
符號引用:符號引用是一組符號來描述所引用的目標物件,符號可以是任何形式的字面量,只要使用時能無歧義地定位到目標即可。符號引用與虛擬機器實現的記憶體佈局無關,引用的目標物件並不一定已經載入到記憶體中。

直接引用:直接引用可以是直接指向目標物件的指標、相對偏移量或是一個能間接定位到目標的控制代碼。直接引用是與虛擬機器記憶體佈局實現相關的,同一個符號引用在不同虛擬機器例項上翻譯出來的直接引用一般不會相同,如果有了直接引用,那引用的目標必定已經在記憶體中存在。

解析的動作主要針對類或介面、欄位、類方法、介面方法四類符號引用進行。分別對應編譯後常量池內的CONSTANT_Class_Info、CONSTANT_Fieldref_Info、CONSTANT_Methodef_Info、CONSTANT_InterfaceMethoder_Info四種常量型別。

1.類、介面的解析

2.欄位解析

3.類方法解析

4.介面方法解析

5.初始化:(瞭解)

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

類載入器的任務是根據一個類的全限定名來讀取此類的二進位制位元組流到JVM中,然後轉換為一個與目標類對應的java.lang.Class物件例項,在虛擬機器提供了4種類載入器,啟動(Bootstrap ClassLoader)類載入器、擴充套件(Extension ClassLoader)類載入器、應用程式(Application ClassLoader)類載入器、自定義(User ClassLoader)類載入器

1.啟動類載入器:這個類載入器負責放在<JAVA_HOME>\lib目錄中的,或者被-Xbootclasspath引數所指定的路徑中的,並且是虛擬機器識別的類庫。使用者無法直接使用。

2.擴充套件類載入器:這個類載入器由sun.misc.Launcher$AppClassLoader實現。它負責<JAVA_HOME>\lib\ext目錄中的,或者被java.ext.dirs系統變數所指定的路徑中的所有類庫。使用者可以直接使用。

3.應用程式類載入器:這個類由sun.misc.Launcher$AppClassLoader實現。是ClassLoader中getSystemClassLoader()方法的返回值。它負責使用者路徑(ClassPath)所指定的類庫。使用者可以直接使用。如果使用者沒有自己定義類載入器,預設使用這個。

4.自定義載入器:使用者自己定義的類載入器。

六、執行引擎:

執行引擎負責具體的程式碼呼叫及執行過程。就目前而言,所有的執行引擎的基本一致:

  1. 輸入:位元組碼檔案
  2. 處理:位元組碼解析
  3. 輸出:執行結果。

物理機的執行引擎是由硬體實現的,和物理機的執行過程不同的是虛擬機器的執行引擎由於自己實現的。

類裝載器裝載負責裝載編譯後的位元組碼,並載入到執行時資料區(Runtime Data Area),然後執行引擎執行會執行這些位元組碼。通過類裝載器裝載的,被分配到JVM的執行時資料區的位元組碼會被執行引擎執行。執行引擎以指令為單位讀取Java位元組碼。它就像一個CPU一樣,一條一條地執行機器指令。每個位元組碼指令都由一個1位元組的操作碼和附加的運算元組成。執行引擎取得一個操作碼,然後根據運算元來執行任務,完成後就繼續執行下一條操作碼。不過Java位元組碼是用一種人類可以讀懂的語言編寫的,而不是用機器可以直接執行的語言。因此,執行引擎必須把位元組碼轉換成可以直接被JVM執行的語言。位元組碼可以通過以下兩種方式轉換成合適的語言。

  • 直譯器:一條一條地讀取,解釋並且執行位元組碼指令。因為它一條一條地解釋和執行指令,所以它可以很快地解釋位元組碼,但是執行起來會比較慢。這是解釋執行的語言的一個缺點。位元組碼這種“語言”基本來說是解釋執行的。
  • 即時(Just-In-Time)編譯器:即時編譯器被引入用來彌補直譯器的缺點。執行引擎首先按照解釋執行的方式來執行,然後在合適的時候,即時編譯器把整段位元組碼編譯成原生代碼。然後,執行引擎就沒有必要再去解釋執行方法了,它可以直接通過原生代碼去執行它。執行原生代碼比一條一條進行解釋執行的速度快很多。編譯後的程式碼可以執行的很快,因為原生代碼是儲存在快取裡的。

七、執行時資料區:

包括:方法區——執行緒共享

          ——執行緒共享

          Java棧(虛擬機器棧)——非執行緒共享

          程式計數器——非執行緒共享

          本地方法棧——非執行緒共享

JVM執行時會分配好方法區和堆,而JVM每遇到一個執行緒,就為其分配一個程式計數器、Java棧、本地方法棧,當執行緒終止時,三者(程式計數器、Java棧、本地方法棧)所佔用的記憶體空間也會釋放掉。

程式計數器、Java棧、本地方法棧的生命週期與所屬執行緒相同,而方法區和堆的生命週期與JAVA程式執行生命週期相同,所以gc只發生線上程共享的區域(大部分發生在Heap上)。

7.1、方法區:

有時候也稱為永久代(Permanent Generation),在方法區中,儲存了每個類的資訊(包括類的名稱、修飾符、方法資訊、欄位資訊)、類中靜態變數、類中定義為final型別的常量、類中的Field資訊、類中的方法資訊以及編譯器編譯後的程式碼等當開發人員在程式中通過Class物件中的getName、isInterface等方法來獲取資訊時,這些資料都來源於方法區域,同時方法區域也是全域性共享的,在一定的條件下它也會被GC,在這裡進行的GC主要是方法區裡的常量池和型別的解除安裝。當方法區域需要使用的記憶體超過其允許的大小時,會丟擲OutOfMemory的錯誤資訊。

在方法區中有一個非常重要的部分就是執行時常量池用於存放靜態編譯產生的字面量和符號引用。執行時生成的常量也會存在這個常量池中,比如String的intern方法它是每一個類或介面的常量池的執行時表示形式,在類和介面被載入到JVM後,對應的執行時常量池就被創建出來。

7.2、堆:

Java中的堆是用來儲存物件例項以及陣列(當然,陣列引用是存放在Java棧中的)。堆是被所有執行緒共享的,因此在其上進行物件記憶體的分配均需要進行加鎖,這也導致了new物件的開銷是比較大的。在JVM中只有一個堆。堆是Java垃圾收集器管理的主要區域,Java的垃圾回收機制會自動進行處理。

Sun Hotspot JVM為了提升物件記憶體分配的效率,對於所建立的執行緒都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據執行的情況計算而得,在TLAB上分配物件時不需要加鎖,因此JVM在給執行緒的物件分配記憶體時會盡量的在TLAB上分配,在這種情況下JVM中分配物件記憶體的效能和C基本是一樣高效的,但如果物件過大的話則仍然是直接使用堆空間分配

堆空間分為老年代年輕代剛建立的物件存放在年輕代,而老年代中存放生命週期長久的例項物件。年輕代中又被分為Eden區兩個Survivor區(From Space和To Space)。新的物件分配是首先放在Eden區,Survivor區作為Eden區和Old區的緩衝,在Survivor區的物件經歷若干次GC仍然存活的,就會被轉移到老年代。 當一個物件大於eden區而小於old區(老年代)的時候會直接扔到old區。 而當物件大於old區時,會直接丟擲OutOfMemoryError(OOM)

7.3、Java棧:

Java棧也稱作虛擬機器棧(Java Vitual Machine Stack),也就是我們常常所說的棧。JVM棧是執行緒私有的,每個執行緒建立的同時都會建立自己的JVM棧,互不干擾。

Java棧是Java方法執行的記憶體模型。Java棧中存放的是一個個的棧幀每個棧幀對應一個被呼叫的方法,在棧幀中包括區域性變量表(Local Variables)、運算元棧(Operand Stack)、指向當前方法所屬的類的執行時常量池的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些額外的附加資訊當執行緒執行一個方法時,就會隨之建立一個對應的棧幀,並將建立的棧幀壓棧。當方法執行完畢之後,便會將棧幀出棧。因此可知,執行緒當前執行的方法所對應的棧幀必定位於Java棧的頂部。

區域性變量表:用來儲存方法中的區域性變數(包括在方法中宣告的非靜態變數以及函式形參)。對於基本資料型別的變數,則直接儲存它的值,對於引用型別的變數,則存的是指向物件的引用。區域性變量表的大小在編譯期就可以確定其大小了,因此在程式執行期間區域性變量表的大小是不會改變的

運算元棧:棧最典型的一個應用就是用來對錶達式求值。在一個執行緒執行方法的過程中,實際上就是不斷執行語句的過程,而歸根到底就是進行計算的過程。因此可以這麼說,程式中的所有計算過程都是在藉助於運算元棧來完成的。

指向執行時常量池的引用:因為在方法執行的過程中有可能需要用到類中的常量,所以必須要有一個引用指向執行時常量。

方法返回地址:當一個方法執行完畢之後,要返回之前呼叫它的地方,因此在棧幀中必須儲存一個方法返回地址。

7.4、程式計數器:

程式計數器(Program Counter Register),也有稱作為PC暫存器。

由於在JVM中,多執行緒是通過執行緒輪流切換來獲得CPU執行時間的,因此,在任一具體時刻,一個CPU的核心只會執行一條執行緒中的指令,因此,為了能夠使得每個執行緒都線上程切換後能夠恢復在切換之前的程式執行位置,每個執行緒都需要有自己獨立的程式計數器,並且不能互相被幹擾,否則就會影響到程式的正常執行次序。因此,可以這麼說,程式計數器是每個執行緒所私有的

在JVM規範中規定,如果執行緒執行的是非native(本地)方法,則程式計數器中儲存的是當前需要執行的指令的地址;如果執行緒執行的是native方法,則程式計數器中的值是undefined。

由於程式計數器中儲存的資料所佔空間的大小不會隨程式的執行而發生改變,因此,對於程式計數器是不會發生記憶體溢位現象(OutOfMemory)的。

7.5、本地方法棧:

JVM採用本地方法堆疊來支援native方法的執行,此區域用於儲存每個native方法呼叫的狀態。本地方法棧與Java棧的作用和原理非常相似。區別只不過是Java棧是為執行Java方法服務的,而本地方法棧則是為執行本地方法(Native Method)服務的在JVM規範中,並沒有對本地方法棧的具體實現方法以及資料結構作強制規定,虛擬機器可以自由實現它。在HotSopt虛擬機器中直接就把本地方法棧和Java棧合二為一。

八、JVM各區域潛在異常

8.1、程式計數器

此區域是JVM規範中唯一一個不存在OOM(OutOfMemory)的區域。

8.2、Java棧(區域性變數空間)

(1)StackOverflowError :棧深度大於虛擬機器所允許的深度。

(2)OOM :如果虛擬機器棧可以動態擴充套件(當前大部分Java虛擬機器都可以動態擴充套件,只不過Java虛擬機器規範中的也允許固定長度的虛擬機器棧),如果擴充套件是無法申請到足夠的記憶體。

8.3、本地方法棧

(1)StackOverflowError :棧深度大於虛擬機器所允許的深度。

(2)OOM

8.4、堆

OOM: 堆無法擴充套件時。