一. 虛擬機
虛擬機是模擬執行某種指令集體系結構(ISA)的軟件,是對操作系統和硬件的一種抽象。
圖1 計算機系統中的抽象
計算機系統的這種抽象類(lèi)似于面向對象編程(OOP)中的針對接口編程泛型(或者是依賴(lài)倒轉原則),通過(guò)一層抽象提取底層實(shí)現中共性的部分,底層實(shí)現這個(gè)抽
象并完成自己個(gè)性的部分。也就是說(shuō)通過(guò)一個(gè)抽象層次來(lái)隔離底層的不同實(shí)現。虛擬機規范定義了這個(gè)虛擬機要完成的功能(也就是接口),底層的操作系統和硬件
利用自己提供的功能來(lái)實(shí)現虛擬機需要完成的功能(實(shí)現)。通過(guò)運行在虛擬機之上,Java才具有很好跨平臺特性。
圖2 jvm實(shí)現,32bit+Win Jvm,32bit+Linux Jvm, 64bit+Win Jvm
二. Java虛擬機
Java虛擬機(JVM)是由Java虛擬機規范定義的,其上運行的是字節碼指令集。這種字節碼指令集包含一個(gè)字節的操作碼(opcode),零至多個(gè)操
作數(oprand),虛擬機規范明確定義了每種字節碼指令完成的功能是什么以及需要多少個(gè)操作數。Java虛擬機上運行的class文件,這個(gè)文件中包
含字節碼指令流以及類(lèi)定義的信息,所以Java虛擬機規范還定義了class文件的格式(精確到每個(gè)字節)。所以實(shí)現Java虛擬機的兩個(gè)要素是字節碼指
令集和class文件格式,Java虛擬機的實(shí)現者只要以正確方式讀取class文件中的每一條字節碼指令,并按照要求實(shí)現字節碼指令的功能就可以實(shí)現
JVM。
目前常用的商用JVM主要有:Sun
HotSpot,BEA JRocket以及IBM
J9。其中由于BEA和Sun已經(jīng)被Oracle收購,所以Oracle擁有當今世界上最流行的兩個(gè)JVM,并有傳言說(shuō)Oracle將在Java8時(shí)將兩
個(gè)虛擬機合并,各取所需,取長(cháng)補短,打造一個(gè)更加精湛的JVM。HotSpot會(huì )以解釋+即時(shí)編譯執行代碼,HotSpot在解釋執行字節碼的時(shí)候,會(huì )探
測熱點(diǎn)(hotspot)代碼,然后將這部分代碼編譯為本地代碼,之后將直接運行本地代碼,而不是解釋?zhuān)@樣會(huì )有效提高虛擬機性能。JRocket主要是
定位于服務(wù)器應用,所以不關(guān)注虛擬機的啟動(dòng)速度,它會(huì )將所有代碼即時(shí)編譯為本地代碼執行,JRocket的垃圾收集器具有很高的收集效率。J9定位與
HotSpot類(lèi)似,專(zhuān)注于桌面應用和服務(wù)器應用,主要是針對IBM的各種Java產(chǎn)品。
三. Java語(yǔ)言與Java虛擬機
我們知道Java源代碼,即.java文件,通過(guò)javac編譯為.class文件。.class文件可以運行在JVM上,JVM底層會(huì )通過(guò)字節碼解釋器
或者即時(shí)編譯器(JIT
Compiler)執行.class文件中的字節碼指令。JVM是運行在操作系統之上的,操作系統又通過(guò)指令集調用底層硬件服務(wù)執行其上的各種軟件。
圖3 Java語(yǔ)言在JVM上的執行
從圖3中,可以看到Java是運行在JVM之上的。但是Java語(yǔ)言和JVM沒(méi)有必然的聯(lián)系。Java語(yǔ)言并不是只能運行在JVM之上,只要實(shí)現了相應的
編譯器Java語(yǔ)言就可以運行在任何平臺之上(比如J++),也可以被編譯為本地代碼直接運行在操作系統之上,比如,Linux上的GCJ(GNU
Compiler for
Java)就可以把Java語(yǔ)言編譯為本地代碼直接執行。同樣的,JVM上也不是只能執行Java語(yǔ)言,只要實(shí)現了適當的編譯器,將其他語(yǔ)言編譯為JVM
上的字節碼,就可以在JVM上運行。比如,JRuby,Jython以及Groovy等其他JVM語(yǔ)言,都會(huì )通過(guò)相應的編譯器或是解釋器轉化
為.class,然后再JVM上運行。由于JVM并不關(guān)心.class文件是由Java、JRuby、Jython等轉化而來(lái),只要這個(gè)文件結構正確并能
通過(guò)class文件校驗。因此,由于.class文件屏蔽了Java、JRuby等上層語(yǔ)言的差異,所以Java、Groovy等可以相互調用。
四. Java虛擬機體系結構
如圖,概念上講,JVM由類(lèi)加載器子系統,運行時(shí)數據區,執行引擎以及本地方法接口組成。
圖4 JVM體系結構
1. 類(lèi)加載器子系統主要用
于定位類(lèi)定義的二進(jìn)制信息,然后將這些信息解析并加載至虛擬機,轉化為虛擬機內部的類(lèi)型信息的數據結構。類(lèi)加載器子系統還承擔著(zhù)安全性的責任,并且是
JVM的動(dòng)態(tài)鏈接和動(dòng)態(tài)加載的基礎。將二進(jìn)制信息=>類(lèi)型信息的數據結構,中間需要經(jīng)過(guò)很多步驟。首先類(lèi)加載器是JVM安全沙箱的第一道防線(xiàn),能夠
防止非信任類(lèi)破壞虛擬機。每一個(gè)被加載的class文件需要經(jīng)過(guò)四次校驗才能被加載。校驗通過(guò)后,類(lèi)加載器的命名空間和運行時(shí)包的特性能夠防止非信任類(lèi)偽
裝成信任類(lèi)來(lái)破壞虛擬機。類(lèi)加載器在方法區構造具有這個(gè)類(lèi)的信息的數據結構后,會(huì )在堆上創(chuàng )建一個(gè)Class對象作為訪(fǎng)問(wèn)這個(gè)數據結構的接口。同時(shí),類(lèi)加載
還需要初始化類(lèi)的靜態(tài)數據,也就是調用類(lèi)的<clinit>方法。以上就是一個(gè)類(lèi)的加載、鏈接及初始化的過(guò)程。
2. 運行時(shí)數據區是JVM運行時(shí)的內存空間的組織,邏輯上又劃分為多個(gè)區,這些區的生命周期和它是否線(xiàn)程共享有關(guān),它們分別是:
堆:用于存
放對象或數組實(shí)例,也就是運行期間new出來(lái)的對象。堆的生命周期與JVM相同,并且在線(xiàn)程之間共享訪(fǎng)問(wèn)。由于多線(xiàn)程并發(fā)訪(fǎng)問(wèn),所以需要考慮線(xiàn)程安全的問(wèn)
題,有兩種方法。第一種是,加鎖進(jìn)行互斥訪(fǎng)問(wèn)。第二種是線(xiàn)程本地分配緩沖(Thread Local Allocate Buffer,
TLAB),在線(xiàn)程創(chuàng )建時(shí)預先給每個(gè)線(xiàn)程分配一塊區域,這塊區域是線(xiàn)程私有的,對其他線(xiàn)程是不可見(jiàn),也就不會(huì )被共享。JVM規范規定在申請不到足夠的內存
時(shí),堆會(huì )拋出OutOfMemoryException。
方法區:存
放類(lèi)型信息和運行時(shí)常量池(Runtime Constant
Pool)。每個(gè)被類(lèi)加載器加載的類(lèi)都會(huì )在方法區中形成一個(gè)與子對應的類(lèi)型信息的數據結構,包括:這個(gè)類(lèi)的類(lèi)名、直接超類(lèi)、實(shí)現的接口列表、字段列表、方
法列表等。運行時(shí)常量池是class文件中的常量池列表(Constant Pool
List)在運行時(shí)的一種體現,其中存儲各種基本數據類(lèi)型及String類(lèi)型的常量以及其他類(lèi)、方法、字段的符號引用。方法區的生命周期與JVM相同,被
多個(gè)線(xiàn)程共享,所以要考慮并發(fā)訪(fǎng)問(wèn)的安全性的問(wèn)題。JVM規范規定在需要的內存得不到滿(mǎn)足的情況下,方法區會(huì )拋出
OutOfMemoryException。
PC(Program Counter):
線(xiàn)程私有的,生命周期與線(xiàn)程相同,是對CPU中PC的一種模擬。如果線(xiàn)程正在執行的是Java方法,則該線(xiàn)程的PC中存放的下一條字節碼指令的地址。在進(jìn)
行Java方法的調用和返回時(shí),需要更新PC以保存當前方法(Current
Method)正在執行的字節碼指令的地址。PC是JVM規范中唯一沒(méi)有規定會(huì )拋出異常的存儲區。
JVM棧:
線(xiàn)程私有,生命周期與線(xiàn)程相同,是對傳統語(yǔ)言(比如C)中的方法調用棧的一種模擬。JVM棧中存放棧幀(Frame)用于進(jìn)行方法調用和返回、存儲局部變
量以及計算的中間結果。JVM規范規定??梢?huà)伋鰞煞N異常:(1)StackOverflowException,在棧的深度大于某個(gè)規定值的情況下拋
出。(2)OutOfMemoryException,在為新棧幀分配內存或者是為線(xiàn)程分配棧的內存時(shí),申請不到足夠的內存的情況下拋出。
JVM棧中存放的是棧幀,每個(gè)棧幀對應著(zhù)一次方法調用。每一時(shí)刻,JVM線(xiàn)程只能執行一個(gè)方法(Current
Method),該方法的棧幀是JVM棧的棧頂的元素(叫做當前棧幀,Current
Frame),當調用一個(gè)方法時(shí),會(huì )初始化一個(gè)棧幀壓入JVM棧;當方法調用返回或者拋出異常沒(méi)有被處理的情況下,JVM棧會(huì )彈出該方法對應的棧幀。每一
個(gè)棧幀中存放局部變量表(Local Variable Table)、操作數棧(Oprand
Stack)以及其他棧幀信息。棧幀的大小在編譯時(shí)就確定了,編譯器會(huì )把局部變量表和操作數棧的大小記錄在class文件中method_info的屬性
表中。局部變量表類(lèi)似于數組存放局部變量和方法參數。由于JVM采用的是基于棧的指令集體系結構,而不是基于寄存器,所以JVM上的所有計算都是在操作數
棧上進(jìn)行的(比如,算術(shù)運算、方法調用、內存訪(fǎng)問(wèn)等)。
本地方法棧:用于支持本地方法調用,拋出的異常與JVM棧相同。
3. 執行引擎用于執行JVM字節碼指令,主要由兩種實(shí)現方式:(1)將輸入的字節碼指令在加載時(shí)或執行時(shí)
翻譯成另外一種虛擬機指令;(2)將輸入的字節碼指令在加載時(shí)或執行時(shí)翻譯成宿主主機本地CPU的指令集。這兩種方式對應著(zhù)字節碼的解釋執行和即時(shí)編譯。
比如在HotSpot VM中執行引擎的實(shí)現是一種解釋-編譯的層次結構:
(1)解釋執行:解釋執行字節碼,并以方法為單位收集“熱點(diǎn)(HotSpot)代碼”的信息,將“熱點(diǎn)代碼”執行C0編譯。
(2)C0編譯:將收集的“熱點(diǎn)代碼”編譯成本地代碼,并進(jìn)行一些簡(jiǎn)單的優(yōu)化。繼續收集運行時(shí)信息,將一些頻繁執行的本地代碼進(jìn)行C1編譯。
(3)C1編譯:將C0階段的本地代碼,進(jìn)行一些比較激進(jìn)的優(yōu)化。如果某些優(yōu)化導致本地代碼執行失敗,此時(shí)JVM會(huì )退化到解釋執行字節碼階段。
4. 自動(dòng)內存管理用于管理運行時(shí)數據區的分配和釋放。和C和C++相比,Java不需要程序員主動(dòng)的管理
內存(在new出對象后,不需要顯示的delete),這樣JVM就需要承擔內存管理這個(gè)任務(wù)。內存管理的重點(diǎn)主要是在申請內存(new對象、類(lèi)加載和初
始化、啟動(dòng)線(xiàn)程時(shí)初始化棧等)得不到滿(mǎn)足時(shí),JVM可以自動(dòng)回收那些不再存活的對象所占用的內存,也就是經(jīng)常聽(tīng)到的垃圾收集。在回收過(guò)程中還要保證處理內
存空間的碎片,以提高空間利用率?;厥者^(guò)程主要有兩個(gè)關(guān)鍵點(diǎn),標記存活對象和回收內存的算法。
標記存活對象主要有引用計算和根搜索法兩種。
(1)引用計數,是一種很普遍的方法,在python、lua等一些腳本語(yǔ)言中都是使用這種算法。每個(gè)對象持有一個(gè)計數器,標記這個(gè)對象被引用的次數。
進(jìn)行垃圾收集時(shí),那些引用計數為0的對象就是“死”對象,需要被收集。引用計數的一個(gè)缺點(diǎn)就是它沒(méi)有辦法處理循環(huán)引用的情況(A->B,
B->A)。
(2)根搜索,HotSpot虛擬機采用這種算法標記存活對象。把方法區、JVM棧中的所有的引用組成的集合作為搜索的根,從這個(gè)集合開(kāi)始遍歷直到結
束。其中被遍歷到的對象是存活對象;那些沒(méi)有被遍歷到的對象需要被垃圾收集。這樣可以有效的避免循環(huán)引用的情況。
回收內存的算法主要有:
(1)復制算法,將內存分成兩個(gè)部分,每一時(shí)刻只是用其中的一個(gè)。進(jìn)行回收時(shí),將所有存活的對象依次復制到另一個(gè)部分(依次復制避免了內存碎片的產(chǎn)
生),接下來(lái)只用這一個(gè)部分。復制算法需要在兩個(gè)內存區域來(lái)回復制,有一定的復制開(kāi)銷(xiāo)和空間開(kāi)銷(xiāo)(每一時(shí)刻只使用一個(gè)區域),但是可以很好的解決內存碎片
的問(wèn)題,適用于對象頻繁創(chuàng )建并且生命周期短的情況。
(2)標記清掃,先進(jìn)行存活對象標記,回收時(shí)將“死”對象占用的內存直接釋放掉,會(huì )產(chǎn)生大量的內存碎片。
(3)標記整理,標記階段與標記清掃算法一樣,回收階段釋放“死”對象的內存后,還需要進(jìn)行對象的移動(dòng)使得所有對象依次在內存中排列,避免了內存碎片的產(chǎn)生。標記整理與復制算法相反,適用于對象創(chuàng )建不頻繁,生命周期長(cháng)得情況。
(4)按代收集,將內存按照對象生命周期的不同劃分為多個(gè)部分,每個(gè)部分采用不同的收集算法。目前,大部分商業(yè)虛擬機都是采用這種算法。比如,在
HotSpot中,內存被劃分為:新生代(New)、老年代(Old)和永久代(Perm)。新生代采用復制算法,老年代和永久代采用標記整理算法。內存
分配、回收的策略是,對象首先在新生代分配,如果新生代內存不滿(mǎn)足要求,則觸發(fā)一次新生代內存的垃圾收集(Young GC,或者是Minor
GC)。Young
GC會(huì )導致部分新生代的對象被移動(dòng)至老年代,一部分是因為新生代內存不足以放下所有的對象;另一部分是因為這些對象的年齡(每個(gè)對象都保存著(zhù)這個(gè)對象被垃
圾收集的次數,表示它的年齡。存儲在對象頭的age屬性中)大到足以晉升到老年代。當新生代的對象進(jìn)入老年代,而老年代的內存不滿(mǎn)足要求時(shí),則會(huì )觸發(fā)一次
整個(gè)新生代和老年代的垃圾收集(Full GC, 或者是Major GC)。
在JVM中有多個(gè)后臺線(xiàn)程用于完成自動(dòng)內存管理,對于CPU來(lái)說(shuō)這些后臺線(xiàn)程和用戶(hù)線(xiàn)程是一樣的,都需要占用系統的資源。在GC線(xiàn)程進(jìn)行垃圾收集時(shí)必須
執行“Stop the
World”這一操作,也就是暫停所有的用戶(hù)線(xiàn)程。這就導致對于實(shí)時(shí)性要求比較高的系統,JVM的垃圾收集可能是一個(gè)短板。但是在JDK1.5,Sun提
供了CMS(Concurrent Mark and
Sweep)垃圾收集器,通過(guò)GC線(xiàn)程和用戶(hù)線(xiàn)程并發(fā)執行減少GC時(shí)間,提高了JVM的實(shí)時(shí)性。在JVM的各種應用中,gc調優(yōu)是一個(gè)關(guān)鍵的部分,主要目
標是減少GC的次數并且降低每次GC的時(shí)間。關(guān)于這部分內容,后續的JVM內存管理會(huì )詳細討論。
五. JVM執行程序的流程
在命令行執行"java Main"就會(huì )開(kāi)啟一個(gè)JVM實(shí)例,我們可以通過(guò)jps,jstat等JVM工具觀(guān)察JVM的運行狀態(tài),下面以運行com.ntes.money.Main這個(gè)類(lèi)為例來(lái)描述一下JVM執行一個(gè)程序的流程。
當在命令行執行"java -Xmx=12m -Xms=12m -Dname=value com.ntes.money.Main"這個(gè)命令時(shí),JVM的執行流程是,
(1)加載JVM,主要是加載動(dòng)態(tài)鏈接庫,windows下是jvm.dll,Linux下是libjvm.so;
(2)設置JVM啟動(dòng)參數,比如命令中的-Xmx=12m -Xms=12m用于設置堆大小。
(3)初始化JVM。
(4)調用類(lèi)加載器子系統,加載com.ntes.money.Main。這里給出的是自定義類(lèi),根據類(lèi)加載器雙親委派鏈,最后是由系統默認類(lèi)加載器
(Classpath類(lèi)加載器)進(jìn)行加載。首先,根據全路徑類(lèi)型轉化為文件路徑com/ntes/money/Main.class,然后讀取
Main.class中的二進(jìn)制信息、解析、加載,在方法區中形成Main類(lèi)對應的數據結構。這里可能拋出
ClassNotFoundException,有兩種原因。一是文件路徑com/ntes/money/Main.class不存在;二是com
/ntes/money/Main.class文件路徑存在,但是Main.class文件中存儲的不是Main類(lèi)的信息,比如是Main1,Main2
等其他類(lèi)的信息。這種情況下,會(huì )拋出NoClassDefFoundError,然后導致ClassNotFoundException。
(5)在方法區com.ntes.money.Main類(lèi)對應的數據結構中,根據方法描述符及訪(fǎng)問(wèn)標志,查找main方法。這里的描述符,包括了方法的
方法名、參數、返回值,也就是public static void
main(String[])。如果找不到對應的main方法,會(huì )拋出NoSuchMethodError: main異常。
(6)通過(guò)本地方法(JNI)執行main方法。
六. 小結
這里,主要是對JVM體系結構的一個(gè)概述,后續打算會(huì )討論一下JVM的內存管理(垃圾收集部分),Java源碼的編譯以及字節碼指令的執行與即時(shí)編譯。由于我也是在不斷的學(xué)習中,所以文章中可能有錯誤和理解不對的地方,請大家及時(shí)指出。