【java虛擬機系列】java虛擬機系列之JVM總述

我們知道java之所以能夠快速崛起一個重要的原因就是其跨平臺性,而跨平臺就是通過java虛擬機來完成的,java虛擬機屬於java底層的知識範疇,即使你不瞭解也不會影響絕大部分人從事的java應用層的開發,但是如果你瞭解JVM的底層知識,你就能更加了解java這門語言的本質,從而對你應用層的java程序的性能優化起到很大的幫助(主要是指內存方面的)另外像一些要求比較高的公司可能在面試的時候會問一些JVM相關的知識,如博主在阿里巴巴內推一面的時候就被問到過JVM的內存區域模型(要知道博主目前可是大三學生啊,就被問道了JVM的知識,而且面試官告訴我這些屬於java的基礎知識委屈),這也說明了JVM知識的重要性。

本博客將從JVM的整個體系方面來總的敘述一下JVM,具體各部分的詳細講解後續將通過【java虛擬機系列】博客的方式不斷補充完善,敬請期待。

一java類型的生命週期:

java中的一個類的生命週期可以概括爲三個階段:開始階段,使用階段,結束階段:

開始階段:開始階段包括三個步驟:

裝載:把二進制形式的java類型讀入jvm中。
1)通過該類型的完全限定名,產生一個代表該類型的二進制數據流;
2)解析這個二進制數據流爲方法區內的內部數據結構;
3)創建一個表示該類型的java.lang.Class類的實例;
連接:把已讀入的類型數據合併到虛擬機的運行時狀態中。
1)驗證:確保java類型數據格式正確並且適用於jvm使用;
2)準備:爲該類型分配內存;
3)解析:把常量池中的符號引用轉換爲直接引用;
初始化:每個類和接口在首次主動使用時初始化。爲類變量賦予正確的初始值;
1)如果類存在直接超類,且直接超類沒有被初始化,先初始化直接超類;
2)如果類存在初始化方法,就執行此方法;

只有六種活動被認爲是主動使用:
1)、創建類的新實例
2)、調用類中聲明的靜態方法
3)、操作類或者接口中聲明的非常量靜態字段
4)、調用Java API中特定的反射方法
5)、初始化一個類的子類
6)、指定一個類作爲jvm啓動時的初始化類


使用階段:實例化

實例化途徑
明確實例化一個類的四種途徑:
1)、new操作符;
2)、調用Class或者Java.lang.reflect.Constructor對象的newInstance()方法;
3)、調用任何現有對象的Clone()方法;
4)、通過java.io.ObjectInputStream類的getObject()方法反序列化;
隱含實例化的幾種途徑:
1)、保存命令行參數的String對象;
2)、和類裝載相關,jvm裝載的每一個類型,會暗中實例化一個Class對象來代表這個類型;
3)、和類裝載相關,當jvm裝載了在常量池中包含CONSTANT_String_info入口類的時候,會創建新的String對象的實例來表示這些常量字符串;
4)、通過執行包含字符串連接操作符的表達式產生對象;
實例化步驟
1)、在堆中爲保存對象的實例變量分配內存;
2)、爲實例變量初始化爲默認的初始值;
3)、爲實例變量賦正確的初始值,有三種技術完成賦值:
  a)、如果對象是clone() 創建的,jvm把原實例變量中的值拷貝到新對象中;
  b)、如果是通過ObjectInputStream類的readObject()調用反序列化的,jvm從輸入流中讀取的值來初始化實例變量;
  c)、jvm調用對象的實例化方法把對象的實例變量初始化爲正確的初始值;
垃圾收集和對象終結
jvm實現必須具有某種自動堆存儲管理策略,大部分是使用垃圾收集器。如果類聲明瞭 void finalize()方法,垃圾收集器在釋放實例內存前會執行這個方法。
垃圾收集器自動調用的finalize()方法拋出的任何異常都將被忽略。


結束階段:

從jvm中卸載類型
很多情況,jvm中類的生命週期和對象的生命週期很相似。jvm如何判斷動態裝載的類型是否仍然被程序使用,其判斷方式和判斷對象是否仍然被使用很相似。


如果程序不再引用某類型,那麼類型就是不可觸及的,就可以被卸載。

使用啓動類裝載器裝載的類型永遠都是可觸及的,所以永遠不會被卸載。只有使用用戶定義的類裝載器裝載的類型纔會變成不可觸及,纔會被卸載。


二java程序的執行過程:


從這個框圖很容易大體上了解java程序工作原理。首先,你寫好java代碼,保存到硬盤當中。然後你在命令行中輸入

javac ClassName.java

此時,你的java代碼就被編譯成字節碼(.class).如果你是在Eclipse IDE或者其他開發工具中,你保存代碼的時候,開發工具已經幫你完成了上述的編譯工作,因此你可以在對應的目錄下看到class文件。此時的class文件依然是保存在硬盤中,因此,當你在命令行中運行

java ClassName

就完成了上面紅色方框中的工作。JRE的來加載器從硬盤中讀取class文件,載入到系統分配給JVM的內存區域--運行數據區(Runtime Data Areas). 然後執行引擎解釋或者編譯類文件,轉化成特定CPU的機器碼,CPU執行機器碼,至此完成整個過程。

因此從上述過程中可以看到java程序執行的過程最最重要的過程就是上述紅色方框中的內容,而這部分內容就屬於java體系結構的內容。下面我們來看一下JVM體系結構。

二JVM體系結構:我們先l來看一張圖:


下面我們來逐個介紹:

1類裝載器子系統

類裝載器子系統與java對象的生命週期息息相關,關於java對象的生命週期請參考上述相關內容,類裝載器子系統是一個層級結構:用圖示表示如下:


--Bootstrap class loader:

當運行java虛擬機時,這個類加載器被創建,它加載一些基本的java API,包括Object這個類。需要注意的是,這個類加載器不是用java語言寫的,而是用C/C++寫的。

--Extension class loader:

這個加載器加載出了基本API之外的一些拓展類,包括一些與安全性能相關的類。

--System Class Loader:

它加載應用程序中的類,也就是在你的classpath中配置的類。

--User-Defined Class Loader:

這是開發人員通過拓展ClassLoader類定義的自定義加載器,加載程序員定義的一些類。

委派模式(Delegation Mode)

仔細看上面的層次結構,當JVM加載一個類的時候,下層的加載器會將將任務委託給上一層類加載器,上一層加載檢查它的命名空間中是否已經加載這個類,如果已經加載,直接使用這個類。如果沒有加載,繼續往上委託直到頂部。檢查完了之後,按照相反的順序進行加載,如果Bootstrap加載器找不到這個類,則往下委託,直到找到類文件。對於某個特定的類加載器來說,一個Java類只能被載入一次,也就是說在Java虛擬機中,類的完整標識是(classLoader,package,className)。一個類可以被不同的類加載器加載。用圖示表示如下:


舉個具體的例子來說明,現在假如有一個自己定義的類MyClass需要加載,如果不指定的話,一般交App(System)加載。接到任務後,System檢查自己的庫裏是否已經有這個類,發現沒有之後委託給Extension,Extension進行同樣的檢查,發現還是沒有繼續往上委託,最頂層的Boots發現自己庫裏也沒有,於是根據它的路徑(Java 核心類庫,如java.lang)嘗試去加載,沒找到這個MaClass類,於是只好往下委託給Extension,Extension到自己的路徑(JAVA_HOME/jre/lib/ext)中找,還是沒找到,繼續往下,此時System加載器到classpath路徑尋找,找到了,於是加載到Java虛擬機。
現在假設我們將這個類放到JAVA_HOME/jre/lib/ext這個路徑中去(相當於交給Extension加載器加載),按照同樣的規則,最後由Extension加載器加載MyClass類,看到了吧,同一個類被兩次加載到JVM,但是每次都是由不同的ClassLoader完成。

可見性限制
下層的加載器能夠看到上層加載器中的類,反之則不行,也就是是說委託只能從下到上。

不允許卸載類
類加載器可以加載一個類,但是它不能卸載一個類。但是類加載器可以被刪除或者被創建

當類加載完成之後,該類的生命週期就開始了,JVM將完成該類的裝載,連接,與初始化。

裝載:

Loading:將文件系統中的Class文件載入到JVM內存(運行數據區域)

連接:
Verifying:檢查載入的類文件是否符合Java規範和虛擬機規範。
Preparing:爲這個類分配所需要的內存,確定這個類的屬性、方法等所需的數據結構。(Prepare a data structure that assigns the memory required by classes andindicates the fields, methods, and interfaces defined in the class.)
Resolving:將該類常量池中的符號引用都改變爲直接引用。

初始化:

Initialing:初始化類的局部變量,爲靜態域賦值,同時執行靜態初始化塊。


2運行時數據區

Runtime Data Areas:當運行一個JVM示例時,系統將分配給它一塊內存區域(這塊內存區域的大小可以設置的),這一內存區域由JVM自己來管理。從這一塊內存中分出一塊用來存儲一些運行數據,例如創建的對象,傳遞給方法的參數,局部變量,返回值等等。這塊就稱爲運行數據區域。運行數據區域可以劃分爲6大塊:Java棧、程序計數寄存器(PC寄存器)、本地方法棧(Native Method Stack)、Java堆、方法區域、運行常量池(Runtime Constant Pool)。運行常量池本應該屬於方法區,但是由於其重要性,JVM規範將其獨立出來說明。其中,前面3各區域(PC寄存器、Java棧、本地方法棧)是每個線程獨自擁有的,後三者則是整個JVM實例中的所有線程共有的。這六大塊如下圖所示:


PC計數器:

每一個線程都擁有一個PC計數器,當線程啓動(start)時,PC計數器被創建,這個計數器存放當前正在被執行的字節碼指令(JVM指令)的地址。

Java棧:

同樣的,Java棧也是每個線程單獨擁有,線程啓動時創建。這個棧中存放着一系列的棧幀(Stack Frame),JVM只能進行壓入(Push)和彈出(Pop)棧幀這兩種操作。每當調用一個方法時,JVM就往棧裏壓入一個棧幀,方法結束返回時彈出棧幀。如果方法執行時出現異常,可以調用printStackTrace等方法來查看棧的情況。棧的示意圖如下:

OK。現在我們再來詳細看看每一個棧幀中都放着什麼東西。從示意圖很容易看出,每個棧幀包含三個部分:本地變量數組,操作數棧,方法所屬類的常量池引用。
》局部(本地)變量數組:
局部(本地)變量數組中,從0開始按順序存放方法所屬對象的引用、傳遞給方法的參數、局部變量。舉個例子:

public void doSomething(int a, double b, Object o) {  
...  
}

這個方法的棧幀中的局部變量存儲的內容分別是:

0: this  
1: a  
2,3:b  
4:0

看仔細了,其中double類型的b需要兩個連續的索引。取值的時候,取出的是2這個索引中的值。如果是靜態方法,則數組第0個不存放this引用,而是直接存儲傳遞的參數。

操作數棧:
操作數棧中存放方法執行時的一些中間變量,JVM在執行方法時壓入或者彈出這些變量。其實,操作數棧是方法真正工作的地方,執行方法時,局部變量數組與操作數棧根據方法定義進行數據交換。

棧幀中數據引用:
除了局部變量數組和操作數棧之外,棧幀還需要一個常量池的引用。當JVM執行到需要常量池的數據時,就是通過這個引用來訪問常量池的。棧幀中的數據還要負責處理方法的返回和異常。如果通過return返回,則將該方法的棧幀從Java棧中彈出。如果方法有返回值,則將返回值壓入到調用該方法的方法的操作數棧中。另外,數據區中還保存中該方法可能的異常表的引用。

本地方法棧
當程序通過JNI(Java Native Interface)調用本地方法(如C或者C++代碼)時,就根據本地方法的語言類型建立相應的棧。

方法區域
方法區域是一個JVM實例中的所有線程共享的,當啓動一個JVM實例時,方法區域被創建。它用於存運行放常量池、有關域和方法的信息、靜態變量、類和方法的字節碼。不同的JVM實現方式在實現方法區域的時候會有所區別。Oracle的HotSpot稱之爲永久區域(Permanent Area)或者永久代(Permanent Generation)。

運行常量池
這個區域存放類和接口的常量,除此之外,它還存放方法和域的所有引用。當一個方法或者域被引用的時候,JVM就通過運行常量池中的這些引用來查找方法和域在內存中的的實際地址。

堆(Heap)
1)堆中存放的是程序創建的實例對象和數組。這個區域對JVM的性能影響很大。垃圾回收機制處理的正是這一塊內存區域。

2)一個java虛擬機實例只存在一個堆空間,因此虛擬機中的所有線程都共享這個堆;
3)一個java程序獨佔一個java虛擬機實例,因此每個java程序都有他自己的堆空間;


類加載器加載其實就是根據編譯後的Class文件,將java字節碼載入JVM內存,並完成對運行數據處於的初始化工作,供執行引擎執行,接下來就瞭解一下java執行引擎

3執行引擎

類加載器將字節碼載入內存之後,執行引擎以Java 字節碼指令爲單元,讀取Java字節碼。但是java字節碼機器是讀不懂的,因此還必須想辦法將字節碼轉化成平臺相關的機器碼。這個過程可以由解釋器來執行或即時編譯器(JIT Compiler)來完成。用圖示表示如下:



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章