高手詳細(xì)介紹JVM是什么?
首先這里澄清兩個(gè)概念:JVM實(shí)例和JVM執(zhí)行引擎實(shí)例,JVM實(shí)例對(duì)應(yīng)了一個(gè)獨(dú)立運(yùn)行的Java程序,而JVM執(zhí)行引擎實(shí)例則對(duì)應(yīng)了屬于用戶運(yùn)行程序的線程;也就是JVM實(shí)例是進(jìn)程級(jí)別,而執(zhí)行引擎是線程級(jí)別的。
JVM是什么?—JVM的生命周期
JVM實(shí)例的誕生:當(dāng)啟動(dòng)一個(gè)Java程序時(shí),一個(gè)JVM實(shí)例就產(chǎn)生了,任何一個(gè)擁有publicstaticvoidmain(String[]args)函數(shù)的class都可以作為JVM實(shí)例運(yùn)行的起點(diǎn),既然如此,那么JVM如何知道是運(yùn)行classA的main而不是運(yùn)行classB的main呢?這就需要顯式的告訴JVM類名,也就是我們平時(shí)運(yùn)行Java程序命令的由來(lái),如JavaclassAhelloworld,這里Java是告訴os運(yùn)行SunJava2SDK的Java虛擬機(jī),而classA則指出了運(yùn)行JVM所需要的類名。
JVM實(shí)例的運(yùn)行:main()作為該程序初始線程的起點(diǎn),任何其他線程均由該線程啟動(dòng)。JVM內(nèi)部有兩種線程:守護(hù)線程和非守護(hù)線程,main()屬于非守護(hù)線程,守護(hù)線程通常由JVM自己使用,Java程序也可以標(biāo)明自己創(chuàng)建的線程是守護(hù)線程。JVM實(shí)例的消亡:當(dāng)程序中的所有非守護(hù)線程都終止時(shí),JVM才退出;若安全管理器允許,程序也可以使用Runtime類或者System.exit()來(lái)退出。
JVM是什么?—JVM的體系結(jié)構(gòu)
粗略分來(lái),JVM的內(nèi)部體系結(jié)構(gòu)分為三部分,分別是:類裝載器(ClassLoader)子系統(tǒng),運(yùn)行時(shí)數(shù)據(jù)區(qū),和執(zhí)行引擎。下面將先介紹類裝載器,然后是執(zhí)行引擎,最后是運(yùn)行時(shí)數(shù)據(jù)區(qū)
1,類裝載器,顧名思義,就是用來(lái)裝載.class文件的。JVM的兩種類裝載器包括:?jiǎn)?dòng)類裝載器和用戶自定義類裝載器,啟動(dòng)類裝載器是JVM實(shí)現(xiàn)的一部分,用戶自定義類裝載器則是Java程序的一部分,必須是ClassLoader類的子類。(下面所述情況是針對(duì)SunJDK1.2)
動(dòng)類裝載器:只在系統(tǒng)類(JavaAPI的類文件)的安裝路徑查找要裝入的類
用戶自定義類裝載器:
系統(tǒng)類裝載器:在JVM啟動(dòng)時(shí)創(chuàng)建,用來(lái)在CLASSPATH目錄下查找要裝入的類其他用戶自定義類裝載器:這里有必要先說(shuō)一下ClassLoader類的幾個(gè)方法,了解它們對(duì)于了解自定義類裝載器如何裝載.class文件至關(guān)重要。
- protectedfinalClassdefineClass(Stringname,bytedata[],intoffset,intlength)
- protectedfinalClassdefineClass(Stringname,bytedata[],intoffset,intlength,ProtectionDomainprotectionDomain);protectedfinalClassfindSystemClass(Stringname)
- protectedfinalvoidresolveClass(Classc)
defineClass用來(lái)將二進(jìn)制class文件(新類型)導(dǎo)入到方法區(qū),也就是這里指的類是用戶自定義的類(也就是負(fù)責(zé)裝載類)
findSystemClass通過(guò)類型的全限定名,先通過(guò)系統(tǒng)類裝載器或者啟動(dòng)類裝載器來(lái)裝載,并返回Class對(duì)象。
ResolveClass:讓類裝載器進(jìn)行連接動(dòng)作(包括驗(yàn)證,分配內(nèi)存初始化,將類型中的符號(hào)引用解析為直接引用),這里涉及到Java命名空間的問(wèn)題,JVM保證被一個(gè)類裝載器裝載的類所引用的所有類都被這個(gè)類裝載器裝載,同一個(gè)類裝載器裝載的類之間可以相互訪問(wèn),但是不同類裝載器裝載的類看不見對(duì)方,從而實(shí)現(xiàn)了有效的屏蔽。
2,執(zhí)行引擎:它或者在執(zhí)行字節(jié)碼,或者執(zhí)行本地方法
要說(shuō)執(zhí)行引擎,就不得不的指令集,每一條指令包含一個(gè)單字節(jié)的操作碼,后面跟0個(gè)或者多個(gè)操作數(shù)。
(一)指令集以棧為設(shè)計(jì)中心,而非以寄存器為中心這種指令集設(shè)計(jì)如何滿足Java體系的要求:
平臺(tái)無(wú)關(guān)性:以棧為中心使得在只有很少register的機(jī)器上實(shí)現(xiàn)Java更便利compiler一般采用stack向連接優(yōu)化器傳遞編譯的中間結(jié)果,若指令集以stack為基礎(chǔ),則有利于運(yùn)行時(shí)進(jìn)行的優(yōu)化工作與執(zhí)行即時(shí)編譯或者自適應(yīng)優(yōu)化的執(zhí)行引擎結(jié)合,通俗的說(shuō)就是使編譯和運(yùn)行用的數(shù)據(jù)結(jié)構(gòu)統(tǒng)一,更有利于優(yōu)化的開展。
網(wǎng)絡(luò)移動(dòng)性:class文件的緊湊性。
安全性:指令集中絕大部分操作碼都指明了操作的類型。(在裝載的時(shí)候使用數(shù)據(jù)流分析期進(jìn)行一次性驗(yàn)證,而非在執(zhí)行每條指令的時(shí)候進(jìn)行驗(yàn)證,有利于提高執(zhí)行速度)。
(二)執(zhí)行技術(shù)
主要的執(zhí)行技術(shù)有:解釋,即時(shí)編譯,自適應(yīng)優(yōu)化、芯片級(jí)直接執(zhí)行其中解釋屬于第一代JVM,即時(shí)編譯JIT屬于第二代JVM,自適應(yīng)優(yōu)化(目前Sun的HotspotJVM采用這種技術(shù))則吸取第一代JVM和第二代JVM的經(jīng)驗(yàn),采用兩者結(jié)合的方式
自適應(yīng)優(yōu)化:開始對(duì)所有的代碼都采取解釋執(zhí)行的方式,并監(jiān)視代碼執(zhí)行情況,然后對(duì)那些經(jīng)常調(diào)用的方法啟動(dòng)一個(gè)后臺(tái)線程,將其編譯為本地代碼,并進(jìn)行仔細(xì)優(yōu)化。若方法不再頻繁使用,則取消編譯過(guò)的代碼,仍對(duì)其進(jìn)行解釋執(zhí)行。
3,運(yùn)行時(shí)數(shù)據(jù)區(qū):主要包括:方法區(qū),堆,Java棧,PC寄存器,本地方法棧
(1)方法區(qū)和堆由所有線程共享
堆:存放所有程序在運(yùn)行時(shí)創(chuàng)建的對(duì)象
方法區(qū):當(dāng)JVM的類裝載器加載.class文件,并進(jìn)行解析,把解析的類型信息放入方法區(qū)。
(2)Java棧和PC寄存器由線程獨(dú)享,在新線程創(chuàng)建時(shí)間里
(3)本地方法棧:存儲(chǔ)本地方法調(diào)用的狀態(tài)
上邊總體介紹了運(yùn)行時(shí)數(shù)據(jù)區(qū)的主要內(nèi)容,下邊進(jìn)行詳細(xì)介紹,要介紹數(shù)據(jù)區(qū),就不得不說(shuō)明JVM中的數(shù)據(jù)類型。
JVM中的數(shù)據(jù)類型:JVM中基本的數(shù)據(jù)單元是word,而word的長(zhǎng)度由JVM具體的實(shí)現(xiàn)者來(lái)決定
數(shù)據(jù)類型包括基本類型和引用類型,
(1)基本類型包括:數(shù)值類型(包括除boolean外的所有的Java基本數(shù)據(jù)類型),boolean(在JVM中使用int來(lái)表示,0表示false,其他int值均表示true)和returnAddress(JVM的內(nèi)部類型,用來(lái)實(shí)現(xiàn)finally子句)。
(2)引用類型包括:數(shù)組類型,類類型,接口類型
前邊講述了JVM中數(shù)據(jù)的表示,下面讓我們輸入到JVM的數(shù)據(jù)區(qū)
首先來(lái)看方法區(qū):
上邊已經(jīng)提到,方法區(qū)主要用來(lái)存儲(chǔ)JVM從class文件中提取的類型信息,那么類型信息是如何存儲(chǔ)的呢?眾所周知,Java使用的是大端序(big?endian:即低字節(jié)的數(shù)據(jù)存儲(chǔ)在高位內(nèi)存上,如對(duì)于1234,12是高位數(shù)據(jù),34為低位數(shù)據(jù),則Java中的存儲(chǔ)格式應(yīng)該為12存在內(nèi)存的低地址,34存在內(nèi)存的高地址,x86中的存儲(chǔ)格式與之相反)來(lái)存儲(chǔ)數(shù)據(jù),這實(shí)際上是在class文件中數(shù)據(jù)的存儲(chǔ)格式,但是當(dāng)數(shù)據(jù)倒入到方法區(qū)中時(shí),JVM可以以任何方式來(lái)存儲(chǔ)它。
類型信息:包括class的全限定名,class的直接父類,類類型還是接口類型,類的修飾符(public,等),所有直接父接口的列表,Class對(duì)象提供了訪問(wèn)這些信息的窗口(可通過(guò)Class.forName(“”)或instance.getClass()獲得),下面是Class的方法,相信大家看了會(huì)恍然大悟,(原來(lái)如此J)
getName(),getSuperClass(),isInterface(),getInterfaces(),getClassLoader();
static變量作為類型信息的一部分保存
指向ClassLoader類的引用:在動(dòng)態(tài)連接時(shí)裝載該類中引用的其他類
指向Class類的引用:必然的,上邊已述
該類型的常量池:包括直接常量(String,integer和floatpoint常量)以及對(duì)其他類型、字段和方法的符號(hào)引用(注意:這里的常量池并不是普通意義上的存儲(chǔ)常量的地方,這些符號(hào)引用可能是我們?cè)诰幊讨兴佑|到的變量),由于這些符號(hào)引用,使得常量池成為Java程序動(dòng)態(tài)連接中至關(guān)重要的部分
字段信息:普通意義上的類型中聲明的字段
方法信息:類型中各個(gè)方法的信息
編譯期常量:指用final聲明或者用編譯時(shí)已知的值初始化的類變量
class將所有的常量復(fù)制至其常量池或者其字節(jié)碼流中。
方法表:一個(gè)數(shù)組,包括所有它的實(shí)例可能調(diào)用的實(shí)例方法的直接引用(包括從父類中繼承來(lái)的)
除此之外,若某個(gè)類不是抽象和本地的,還要保存方法的字節(jié)碼,操作數(shù)棧和該方法的棧幀,異常表。
舉例:
- classLava{
- privateintspeed=5;
- voidflow(){}
- classVolcano{
- publicstaticvoidmain(String[]args){
- Lavalava=newLava();
- lava.flow();
- }
- }
運(yùn)行命令JavaVolcano;
(1)JVM找到Volcano.class倒入,并提取相應(yīng)的類型信息到方法區(qū)。通過(guò)執(zhí)行方法區(qū)中的字節(jié)碼,JVM執(zhí)行main()方法,(執(zhí)行時(shí)會(huì)一直保存指向Vocano類的常量池的指針)
(2)Main()中第一條指令告訴JVM需為列在常量池第一項(xiàng)的類分配內(nèi)存(此處再次說(shuō)明了常量池并非只存儲(chǔ)常量信息),然后JVM找到常量池的第一項(xiàng),發(fā)現(xiàn)是對(duì)Lava類的符號(hào)引用,則檢查方法區(qū),看Lava類是否裝載,結(jié)果是還未裝載,則查找“Lava.class”,將類型信息寫入方法區(qū),并將方法區(qū)Lava類信息的指針來(lái)替換Volcano原常量池中的符號(hào)引用,即用直接引用來(lái)替換符號(hào)引用。
(3)JVM看到new關(guān)鍵字,準(zhǔn)備為L(zhǎng)ava分配內(nèi)存,根據(jù)Volcano的常量池的第一項(xiàng)找到Lava在方法區(qū)的位置,并分析需要多少對(duì)空間,確定后,在堆上分配空間,并將speed變量初始為0,并將lava對(duì)象的引用壓到棧中
(4)調(diào)用lava的flow()方法
好了,大致了解了方法區(qū)的內(nèi)容后,讓我們來(lái)看看堆
Java對(duì)象的堆實(shí)現(xiàn):
Java對(duì)象主要由實(shí)例變量(包括自己所屬的類和其父類聲明的)以及指向方法區(qū)中類數(shù)據(jù)的指針,指向方法表的指針,對(duì)象鎖(非必需),等待集合(非必需),GC相關(guān)的數(shù)據(jù)(非必需)(主要視GC算法而定,如對(duì)于標(biāo)記并清除算法,需要標(biāo)記對(duì)象是否被引用,以及是否已調(diào)用finalize()方法)。
那么為什么Java對(duì)象中要有指向類數(shù)據(jù)的指針呢?我們從幾個(gè)方面來(lái)考慮
首先:當(dāng)程序中將一個(gè)對(duì)象引用轉(zhuǎn)為另一個(gè)類型時(shí),如何檢查轉(zhuǎn)換是否允許?需用到類數(shù)據(jù)
其次:動(dòng)態(tài)綁定時(shí),并不是需要引用類型,而是需要運(yùn)行時(shí)類型,
這里的迷惑是:為什么類數(shù)據(jù)中保存的是實(shí)際類型,而非引用類型?這個(gè)問(wèn)題先留下來(lái),我想在后續(xù)的讀書筆記中應(yīng)該能明白
指向方法表的指針:這里和C++的VTBL是類似的,有利于提高方法調(diào)用的效率
對(duì)象鎖:用來(lái)實(shí)現(xiàn)多個(gè)線程對(duì)共享數(shù)據(jù)的互斥訪問(wèn)
等待集合:用來(lái)讓多個(gè)線程為完成共同目標(biāo)而協(xié)調(diào)功過(guò)。(注意Object類中的wait(),notify(),notifyAll()方法)。
Java數(shù)組的堆實(shí)現(xiàn):數(shù)組也擁有一個(gè)和他們的類相關(guān)聯(lián)的Class實(shí)例,具有相同dimension和type的數(shù)組是同一個(gè)類的實(shí)例。數(shù)組類名的表示:如[[LJava/lang/Object表示Object[][],[I表示int[],[[[B表示byte[][][]
至此,堆已大致介紹完畢,下面來(lái)介紹程序計(jì)數(shù)器和Java棧
程序計(jì)數(shù)器:為每個(gè)線程獨(dú)有,在線程啟動(dòng)時(shí)創(chuàng)建,
若thread執(zhí)行Java方法,則PC保存下一條執(zhí)行指令的地址。
若thread執(zhí)行native方法,則Pc的值為undefined
Java棧:Java棧以幀為單位保存線程的運(yùn)行狀態(tài),Java棧只有兩種操作,幀的壓棧和出棧。
每個(gè)幀代表一個(gè)方法,Java方法有兩種返回方式,return和拋出異常,兩種方式都會(huì)導(dǎo)致該方法對(duì)應(yīng)的幀出棧和釋放內(nèi)存。
幀的組成:局部變量區(qū)(包括方法參數(shù)和局部變量,對(duì)于instance方法,還要首先保存this類型,其中方法參數(shù)按照聲明順序嚴(yán)格放置,局部變量可以任意放置),操作數(shù)棧,幀數(shù)據(jù)區(qū)(用來(lái)幫助支持常量池的解析,正常方法返回和異常處理)。
本地方法棧:依賴于本地方法的實(shí)現(xiàn),如某個(gè)JVM實(shí)現(xiàn)的本地方法借口使用C連接模型,則本地方法棧就是C棧,可以說(shuō)某線程在調(diào)用本地方法時(shí),就進(jìn)入了一個(gè)不受JVM限制的領(lǐng)域,也就是JVM可以利用本地方法來(lái)動(dòng)態(tài)擴(kuò)展本身。
相信大家都明白JVM是什么了吧。
【編輯推薦】