JVM對Java程序來講就至關於操做系統,JVM包括一套指令、一組寄存器、棧、堆以及垃圾回收等,全部的Java程序都運行在JVM中,經過JVM能夠實現各類計算機上的功能,JVM的好處在於它屏蔽了底層實現方式,Java程序能夠運行在任何平臺上,不須要寫多份代碼。
1. JVM體系結構
java
首先,Java程序通過Compiler編譯生成Class文件,啓動程序後就產生JVM實例,類加載器負責將Class字節碼加載到JVM中,將類的靜態信息、方法信息等加載到方法區中,對象分配在Java對上,程序執行過程和局部變量則存放到Java棧中,而後執行引擎基於棧執行程序,寄存器記錄程序執行的位置,程序執行的同時垃圾回收器回收不被引用的對象。
2. JVM生命週期數組
- 啓動。啓動一個Java程序時,一個JVM實例就產生了,任何一個擁有public static void main(String[] args)函數的class均可以做爲JVM實例運行的起點;
- 運行。main()做爲該程序初始線程的起點,任何其餘線程均由該線程啓動。JVM內部有兩種線程:守護線程和非守護線程,main()屬於非守護線程,守護線程一般由JVM本身使用,java程序也能夠標明本身建立的線程是守護線程;
- 消亡。當程序中的全部非守護線程都終止時,JVM才退出;若安全管理器容許,程序也可使用Runtime類或者System.exit()來退出。
3. 類加載器:
類加載器的做用就是將Class文件加載到JVM中。
ClassLoader兩種裝載Class的方式:緩存
- 隱式:運行過程當中,碰到new方式生成對象時,隱式調用ClassLoader到JVM
- 顯式:經過Class.forName()動態加載
類加載器結構:
類的加載過程採用雙親委派模型(Parent Delegation Model),這種機制能更好的保證 Java 平臺的安全。該模型要求除了頂層的Bootstrap class loader啓動類加載器外,其他的類加載器都應當有本身的父類加載器。子類加載器和父類加載器不是以繼承(Inheritance)的關係來實現,而是經過組合(Composition)關係來複用父加載器的代碼。
雙親委派模型的工做過程爲:安全
- 當前ClassLoader首先從本身已經加載的類中查詢是否此類已經加載,若是已經加載則直接返回原來已經加載的類。每一個類加載器都有本身的加載緩存,當一個類被加載了之後就會放入緩存,等下次加載的時候就能夠直接返回了。
- 當前ClassLoader的緩存中沒有找到被加載的類的時候,委託父類加載器去加載,父類加載器採用一樣的策略,首先查看本身的緩存,而後委託父類的父類去加載,一直到Bootstrap ClassLoader。
- 當全部的父類加載器都沒有加載的時候,再由當前的類加載器加載,並將其放入它本身的緩存中,以便下次有加載請求的時候直接返回。
使用這種模型來組織類加載器之間的關係主要是爲了安全性,避免用戶本身編寫的類動態替換Java的一些核心類,好比String,同時也避免了重複加載,由於JVM中區分不一樣類,不只僅是根據類名,相同的Class文件被不一樣的ClassLoader加載就是不一樣的兩個類,若是相互轉型的話會拋java.lang.ClassCaseException。
4. 運行時數據:
Class文件加載到JVM後就會產生如下幾種數據:數據結構
- PC寄存器
PC寄存器是用於存儲每一個線程下一步將執行的JVM指令,如該方法爲native的,則PC寄存器中不存儲任何信息。
- Java棧
Java棧是線程私有的,每一個線程建立的同時都會建立Java棧,Java棧中存放的爲當前線程棧幀(Stack Frame),棧幀適用於支持虛擬機對方法調用和方法執行的數據結構,棧幀隨着方法的調用而建立,隨着方法的結束而銷燬,棧幀中包括局部變量表(八中基本類型和非基本類型引用),操做數棧,動態連接,返回地址和額外信息。
- Java堆
它是用來存儲對象實例以及數組值的區域,能夠認爲Java中全部經過new建立的對象的內存都在此分配,Heap中的對象的內存須要等待GC進行回收。
堆是JVM中全部線程共享的,所以在其上進行對象內存的分配均須要進行加鎖,這也致使了new對象的開銷是比較大的;
Sun Hotspot JVM爲了提高對象內存分配的效率,對於所建立的線程都會分配一塊獨立的空間TLAB(Thread Local Allocation Buffer),其大小由JVM根據運行的狀況計算而得,在TLAB上分配對象時不須要加鎖,所以JVM在給線程的對象分配內存時會盡可能的在TLAB上分配,在這種狀況下JVM中分配對象內存的性能和C基本是同樣高效的,但若是對象過大的話則仍然是直接使用堆空間分配,TLAB僅做用於新生代的Eden Space。
- 方法區域:
在Sun JDK中這塊區域對應的爲PermanetGeneration,又稱爲持久代。方法區域存放了所加載的類的信息(名稱、修飾符等)、類中的靜態變量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息,當開發人員在程序中經過Class對象中的getName、isInterface等方法來獲取信息時,這些數據都來源於方法區域,同時方法區域也是全局共享的,在必定的條件下它也會被GC,當方法區域須要使用的內存超過其容許的大小時,會拋出OutOfMemory的錯誤信息。
- 運行時常量池:
存放的爲類中的固定的常量信息、方法和Field的引用信息等,其空間從方法區域中分配。
- 本地方法棧:
JVM採用本地方法堆棧來支持native方法的執行,此區域用於存儲每一個native方法調用的狀態。
5. 執行引擎:
執行引擎是JVM的核心部分,ClassLoader將Class加載到JVM後,執行引擎的做用就是解析JVM字節碼指令,獲得執行結果。Sun HotSpot是基於棧的執行引擎,執行的過程就是棧幀壓棧和出棧的過程,計數器則用於記錄指令執行到的位置。
併發
- 局部變量表
一組變量值存儲空間,用於存放方法參數和方法內部定義的局部變量。在Java程序被編譯成Class文件時,就在方法的Code屬性的max_locals數據項中肯定了改方法所需分配的最大局部變量表的容器。 包含類型:boolean、byte、char、short、int、float、reference或returnAddress類型八種類型。
- 操做棧
一個後入先出棧。同局部變量表同樣,操做數棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks數據項之中,操做數棧的深度都不會超過在max_stacks數據項中設定的最大值。當一個方法剛剛開始執行的時候,這個方法的操做數棧是空的,在方法的執行過程當中,會有各類字節碼指令向操做數棧中寫入和提取內容,也就是入棧出棧操做。 操做數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配,在編譯程序代碼的時候,編譯器要嚴格保證這一點,在類校驗階段的數據流分析中還要再次驗證這一點。
- 動態連接
每一個棧幀都包含着一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用的是爲了支持方法調用過程當中的動態鏈接。 在Class文件中存在着大量的符號引用,字節碼中的方法調用指令就以常量池中指向方法的符號引用爲參數。這些符號引用一部分在類加載階段第一次使用階段的時候轉換爲直接引用,這種轉換稱爲靜態解析。另一部分將在每次的運行期間轉化爲直接引用,這部分稱爲動態轉換。
- 返回地址
當一個方法被執行後,有兩種方式能夠退出這個方法:
a) 第一種方式是執行引擎遇到任意一個方法返回的字節碼指令,這時候可能會有返回值傳遞給上層的方法調用者(調用當前方法的方法稱爲調用者),是否有返回值和返回值的類型將遇到何種方法返回指令來決定,這種退出方法的方式稱爲正常完成出口。
b) 另一種退出方式是:在方法執行過程當中遇到異常,而且這個異常沒有在方法體內獲得處理,不管是JVM內部產生的異常,仍是代碼中使用athrow字節碼指令產生的異常,只要在本方法的異常表中沒有搜索到匹配的異常處理器,就會致使方法退出。這種方式被稱爲異常退出出口。此方式不會給上層調用者產生任何返回值。
不管採用哪種退出方式,在方法退出後,都會返回到方法被調用的位置,程序才能繼續執行。方法返回時可能要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態。通常來講,方法正常退出以後,調用者的PC計數器的值就能夠做爲返回地址。棧幀中極可能會保存這個計數器值,而方法異常退出後,返回地址就要經過異常處理器表來肯定,棧幀通常不保存這部分信息。
- 方法調用
a) 解析調用時是靜態的過程,在編譯期間就徹底肯定目標方法。
在Class文件中,全部方法調用中的目標方法都是常量池中的符號引用,在類加載的解析階段,會將一部分符號引用轉爲直接引用,也就是在編譯階段就可以肯定惟一的目標方法,這類方法的調用成爲解析調用。此類方法主要包括靜態方法和私有方法兩大類,前者與類型直接關聯,後者在外部不可訪問,所以決定了他們都不可能經過繼承或者別的方式重寫該方法,符合這兩類的方法主要有如下幾種:靜態方法、私有方法、實例構造器、父類方法。虛擬機中提供瞭如下幾條方法調用指令:
invokestatic:調用靜態方法,解析階段肯定惟一方法版本
invokespecial:調用<init>方法、私有及父類方法,解析階段肯定惟一方法版本
invokevirtual:調用全部虛方法
invokeinterface:調用接口方法
invokedynamic:動態解析出須要調用的方法,而後執行
b) 分派調用則便可能是靜態,也多是動態的,根據分派標準能夠分爲單分派和多分派。兩兩組合有造成了靜態單分派、靜態多分派、動態單分派、動態多分派。分派調用更多的體如今多態上。
靜態分派:全部依賴靜態類型來定位方法執行版本的分派成爲靜態分派,發生在編譯階段,典型應用是方法重載。
動態分派:在運行期間根據實際類型來肯定方法執行版本的分派成爲動態分派,發生在程序運行期間,典型的應用是方法的重寫。
單分派:根據一個參數對目標方法進行選擇。
多分派:根據多於一個參數對目標方法進行選擇。
執行引擎工做過程:函數
public class Main {
public static void main(String[] args) {
int a = 0;
int b = 1;
int c = a + b;
}
}
public class piggy.Main {
public piggy.Main();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: iconst_0 // 常量0入棧
1: istore_1 // 變量a複製0加入變量表
2: iconst_1 // 常量1入棧
3: istore_2 // 變量b複製1加入變量表
4: iload_1 // 變量a入棧
5: iload_2 // 變量b入棧
6: iadd // 棧頂a,b相加
7: istore_3 // 將a,b和複製c加入變量表
8: return
}
6. 垃圾回收:
Class被ClassLoader加載到JVM後會產生各類數據,有些數據通過一段時間後就再也不會被使用,GC將內存中再也不被使用的對象進行回收,GC中用於回收的方法稱爲收集器,因爲GC須要消耗一些資源和時間,Java在對對象的生命週期特徵進行分析後,按照新生代、舊生代的方式來對對象進行收集,以儘量的縮短GC對應用形成的暫停。
垃圾回收按照如下三個方面進行分類:性能
- 回收策略
a) 引用計數
b) 標記清除
c) 複製
d) 標記整理
- 分區方法
a) 增量收集
b) 分代收集
- 系統線程
a) 串行收集
b) 並行收集
c) 併發收集
JVM堆內存採用分代方法存儲對象,分爲年輕帶、年老代和永久代,不一樣代的回收方式也不相同,年輕代通常採用並行方式收集稱爲Young GC,年老代採用併發的方式收集稱爲Full GC,其中主要耗時的是Full GC,由於Full GC會暫停全部線程,因此要經過對JVM進行一些設置,從而減小Full GC的頻率和事件。操作系統