Java基礎(chǔ):Java虛擬機(jī)(JVM)

Java基礎(chǔ):Java虛擬機(jī)(JVM)

當(dāng)我們第一次學(xué)習(xí)Java時(shí)這些原理上的東西就會被提到,但是很少有真正去學(xué)習(xí)。今天開始從頭過一遍Java,打算從JVM開始。

1. JVM是什么

2. JRE和JDK

3. JVM結(jié)構(gòu)

3.1. 程序計(jì)數(shù)器(PC, Program Counter)

3.2. Java虛擬機(jī)棧(Stack,Java Virtual Mechine Stacks)

3.3. 本地方法棧(Native Stack)

3.4. Java 堆(Heap, Garbage Collection Heap)

3.5. 方法區(qū)(Method Area)

3.6. 代碼緩存(Code Cache)

3.7. 類信息(Class Data)

3.8. 運(yùn)行時(shí)常量池(Run-Time Constant Pool)

3.9. 直接內(nèi)存(Direct Memory)

4. Java垃圾回收

5. JVM線程與原生線程的關(guān)系

6. 參考文章

1. JVM是什么

JVM是Java Virtual Mechine的縮寫。它是一種基于計(jì)算設(shè)備的規(guī)范,是一臺虛擬機(jī),即虛構(gòu)的計(jì)算機(jī)。

JVM屏蔽了具體操作系統(tǒng)平臺的信息(顯然,就像是我們在電腦上開了個(gè)虛擬機(jī)一樣),當(dāng)然,JVM執(zhí)行字節(jié)碼時(shí)實(shí)際上還是要解釋成具體操作平臺的機(jī)器指令的。

通過JVM,Java實(shí)現(xiàn)了平臺無關(guān)性,Java語言在不同平臺運(yùn)行時(shí)不需要重新編譯,只需要在該平臺上部署JVM就可以了。因而能實(shí)現(xiàn)一次編譯多處運(yùn)行。(就像是你的虛擬機(jī)也可以在任何安了VMWare的系統(tǒng)上運(yùn)行)

2. JRE和JDK

JRE:Java Runtime Environment,也就是JVM的運(yùn)行平臺,聯(lián)系平時(shí)用的虛擬機(jī),大概可以理解成JRE=虛擬機(jī)平臺+虛擬機(jī)本體(JVM)。類似于你電腦上的VMWare+適用于VMWare的Ubuntu虛擬機(jī)。這樣我們也就明白了JVM到底是個(gè)什么。

JDK:Java Develop Kit,Java的開發(fā)工具包,JDK本體也是Java程序,因此運(yùn)行依賴于JRE,由于需要保持JDK的獨(dú)立性與完整性,JDK的安裝目錄下通常也附有JRE。目前Oracle提供的Windows下的JDK安裝工具會同時(shí)安裝一個(gè)正常的JRE和隸屬于JDK目錄下的JRE。

3. JVM結(jié)構(gòu)

JVM主要包括:程序計(jì)數(shù)器(Program Counter),Java堆(Heap),Java虛擬機(jī)棧(Stack),本地方法棧(Native Stack),方法區(qū)(Method Area)

詳細(xì)的結(jié)構(gòu)如下:

現(xiàn)在我來分別介紹一下每一部分的功能。

3.1. 程序計(jì)數(shù)器(PC, Program Counter)

是一個(gè)寄存器,可以看作是代碼行號指示器,類似于實(shí)際計(jì)算機(jī)里的PC,用于指示,跳轉(zhuǎn)下一條需要執(zhí)行的命令。Java的基礎(chǔ)操作以及異常處理等都十分依賴PC。

JVM多線程是通過線程輪流切換并分配處理器執(zhí)行時(shí)間的方式來實(shí)現(xiàn)的。在一個(gè)確定的時(shí)刻,一個(gè)處理器(或者說多核處理器的一個(gè)內(nèi)核)只會執(zhí)行一條線程中的命令。因此,為了正常的切換線程,每個(gè)線程都會有一個(gè)獨(dú)立的PC,各線程的PC不會互相影響。這個(gè)私有的PC所占的這塊內(nèi)存即是線程的“私有內(nèi)存”。

如果線程在執(zhí)行的是Java方法,那么PC記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址。如果正在執(zhí)行的不是Java方法即Native方法,那么PC的值為undefined。

PC的內(nèi)存區(qū)域是唯一的沒有規(guī)定任何OutOfMemoryError的Java虛擬機(jī)規(guī)范中的區(qū)域。

3.2. Java虛擬機(jī)棧(Stack,Java Virtual Mechine Stacks)

同PC一樣(從工作流程圖里我們可以看到,實(shí)際上,PC也是存在于JVM Stack上的),也是線程私有的,生命周期與線程相同。虛擬機(jī)棧描述Java方法執(zhí)行的內(nèi)存模型,每個(gè)方法被執(zhí)行時(shí)都會創(chuàng)建一個(gè)棧幀(Stack Frame),棧幀會利用局部變量數(shù)組存儲局部變量(Local Variables),操作棧(Operand Stack),方法出口(Return Value),動態(tài)連接(Current Class Constant Pool Reference)等信息。

局部變量數(shù)組存儲了編譯可知的八個(gè)基本類型(int, boolean, char, short, byte, long, float, double),對象引用(根據(jù)不同的虛擬機(jī)實(shí)現(xiàn)可能是引用地址的指針或者一個(gè)handle),returnAddress類型。64位的long和double會占用兩個(gè)Slot,其余類型會占用一個(gè)Slot。在編譯期間,局部變量所需的空間就會完成分配,動態(tài)運(yùn)行期間不會改變所需的空間。

操作棧在執(zhí)行字節(jié)碼指令時(shí)會被用到,這種方式類似于原生的CPU寄存器,大部分JVM把時(shí)間花費(fèi)在操作棧的花費(fèi)上,操作棧和局部變量數(shù)組會頻繁的交換數(shù)據(jù)。

動態(tài)連接控制著運(yùn)行時(shí)常量池和棧幀的連接。所有方法和類的引用都會被當(dāng)作符號的引用存在常量池中。符號引用是實(shí)際上并不指向物理內(nèi)存地址的邏輯引用。JVM 可以選擇符號引用解析的時(shí)機(jī),一種是當(dāng)類文件加載并校驗(yàn)通過后,這種解析方式被稱為饑餓方式。另外一種是符號引用在第一次使用的時(shí)候被解析,這種解析方式稱為惰性方式。無論如何 ,JVM 必須要在第一次使用符號引用時(shí)完成解析并拋出可能發(fā)生的解析錯(cuò)誤。綁定是將對象域、方法、類的符號引用替換為直接引用的過程。綁定只會發(fā)生一次。一旦綁定,符號引用會被完全替換。如果一個(gè)類的符號引用還沒有被解析,那么就會載入這個(gè)類。每個(gè)直接引用都被存儲為相對于存儲結(jié)構(gòu)(與運(yùn)行時(shí)變量或方法的位置相關(guān)聯(lián)的)偏移量。

對Java虛擬機(jī)棧這個(gè)區(qū)域,Java虛擬機(jī)規(guī)范規(guī)定了兩種異常:

線程請求的棧深度大于虛擬機(jī)所允許的深度,拋出StackOverFlow異常。

對于支持動態(tài)擴(kuò)展的虛擬機(jī),當(dāng)擴(kuò)展無法申請到足夠的內(nèi)存時(shí)會拋出OutOfMemory異常。

3.3. 本地方法棧(Native Stack)

本地方法棧如其名字,和Java Virtual Machine Stack其實(shí)極為類似,只是執(zhí)行的是Native方法,為Native方法服務(wù)。在JVM規(guī)范中,沒有對它的實(shí)現(xiàn)做具體規(guī)定。

3.4. Java 堆(Heap, Garbage Collection Heap)

Java堆是被所有線程共享的一塊區(qū)域,在虛擬機(jī)啟動時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實(shí)例,幾乎所有的對象實(shí)例都在這里分配內(nèi)存(隨著技術(shù)的發(fā)展,已不絕對)。

Java堆是垃圾收集器管理的主要區(qū)域,因而也被稱為GC堆。收集器采用分代回收法,GC堆可以分為新生代(Yong Generation)和老生代(Old Generation)。新生代包括Eden Space和Survivor Space。但無論哪個(gè)區(qū)域,如何劃分,存儲的都是Java對象實(shí)例,進(jìn)一步的劃分是為了更好的回收內(nèi)存或快速的分配內(nèi)存。

根據(jù)Java虛擬機(jī)規(guī)范,堆所在的物理內(nèi)存區(qū)間可以是不連續(xù)的,只要邏輯連續(xù)就可以。實(shí)現(xiàn)時(shí)既可以是固定大小,也可以是可擴(kuò)展的。如果堆無法擴(kuò)展時(shí),就會拋出OutOfMemoryError。

3.5. 方法區(qū)(Method Area)

方法區(qū)和Java堆類似,也屬于各線程共享的內(nèi)存區(qū)域。用于存儲已被虛擬機(jī)加載的類信息,常量,靜態(tài)變量,即時(shí)編譯器編譯后的代碼數(shù)據(jù)等。它屬于非堆區(qū)(Non Heap),和Java堆區(qū)分開。對于存在永久代(Permanent)概念的虛擬機(jī)(HotSpot)而言,方法區(qū)存在于永久代。Java虛擬機(jī)規(guī)范對方法區(qū)的規(guī)定很寬松,甚至可以不實(shí)現(xiàn)GC。不過并非進(jìn)入方法區(qū)的數(shù)據(jù)就會永久存在了,這塊區(qū)域的內(nèi)存回收主要為常量池的回收和類型的卸載。這個(gè)區(qū)域的回收處理不善也會導(dǎo)致嚴(yán)重的內(nèi)存泄漏。

當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時(shí)也會拋出OutOfMemoryError。

3.6. 代碼緩存(Code Cache)

用于編譯和存儲那些被 JIT 編譯器編譯成原生代碼的方法。

3.7. 類信息(Class Data)

類信息存儲在方法區(qū),其主要構(gòu)成為運(yùn)行時(shí)常量池(Run-Time Constant Pool)和方法(Method Code)。

一個(gè)編譯后的類文件包括以下結(jié)構(gòu):

結(jié)構(gòu)解釋

magic, minor_version, major_version類文件的版本信息和用于編譯這個(gè)類的 JDK 版本。

constant_pool類似于符號表,盡管它包含更多數(shù)據(jù)。下面有更多的詳細(xì)描述。

access_flags提供這個(gè)類的描述符列表。

this_class提供這個(gè)類全名的常量池(constant_pool)索引,比如org/jamesdbloom/foo/Bar。

super_class提供這個(gè)類的父類符號引用的常量池索引。

interfaces指向常量池的索引數(shù)組,提供那些被實(shí)現(xiàn)的接口的符號引用。

fields提供每個(gè)字段完整描述的常量池索引數(shù)組。

methods指向constant_pool的索引數(shù)組,用于表示每個(gè)方法簽名的完整描述。如果這個(gè)方法不是抽象方法也不是 native 方法,那么就會顯示這個(gè)函數(shù)的字節(jié)碼。

attributes不同值的數(shù)組,表示這個(gè)類的附加信息,包括 RetentionPolicy.CLASS 和 RetentionPolicy.RUNTIME 注解。

3.8. 運(yùn)行時(shí)常量池(Run-Time Constant Pool)

運(yùn)行時(shí)常量池是方法區(qū)的一部分。Class文件中有類的版本,字段,方法,接口等描述信息和用于存放編譯期生成的各種字面量和符號引用。這部分內(nèi)容將在類加載后存放到方法區(qū)的運(yùn)行時(shí)常量池中。Java虛擬機(jī)規(guī)范對Class的細(xì)節(jié)有著嚴(yán)苛的要求而對運(yùn)行時(shí)常量池的實(shí)現(xiàn)不做要求。一般來說除了翻譯的Class,翻譯出來的直接引用也會存在運(yùn)行時(shí)常量池中。

運(yùn)行時(shí)常量池具備動態(tài)性,即運(yùn)行時(shí)也可將新的常量放入池中。比如String類的intern()方法。

常量池?zé)o法申請到足夠的內(nèi)存分配時(shí)也會拋出OutOfMemoryError。

3.9. 直接內(nèi)存(Direct Memory)

直接內(nèi)存并不在Java虛擬機(jī)規(guī)范中,不是Java的一部分,但是也被頻繁使用并可能導(dǎo)致OutOfMemoryError。Native函數(shù)庫可以直接分配堆外內(nèi)存,通過存儲在Java堆里的DirectDataBuffer對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣做在一些場景中可以顯著提高性能。

直接內(nèi)存是堆外內(nèi)存,自然不受Java堆大小的限制,但是可能受實(shí)體機(jī)內(nèi)存大小的限制。如果內(nèi)存各部分總和大于實(shí)體機(jī)的內(nèi)存時(shí),也會報(bào)出OutOfMemoryError。

4. Java垃圾回收

將內(nèi)存中不再被使用的對象進(jìn)行回收,GC中用于回收的方法稱為收集器,由于GC需要消耗一些資源和時(shí)間,Java在對對象的生命周期特征進(jìn)行分析后,按照新生代、舊生代的方式來對對象進(jìn)行收集,以盡可能的縮短GC對應(yīng)用造成的暫停。

不同的對象引用類型, GC會采用不同的方法進(jìn)行回收,JVM對象的引用分為了四種類型:

強(qiáng)引用:默認(rèn)情況下,對象采用的均為強(qiáng)引用(這個(gè)對象的實(shí)例沒有其他對象引用,GC時(shí)才會被回收)。

軟引用:軟引用是Java中提供的一種比較適合于緩存場景的應(yīng)用(只有在內(nèi)存不夠用的情況下才會被GC)。

弱引用:在GC時(shí)一定會被GC回收。

虛引用:由于虛引用只是用來得知對象是否被GC。

5. JVM線程與原生線程的關(guān)系

JVM允許一個(gè)程序使用多個(gè)并發(fā)線程,Hotspot JVM中Java的線程與原生操作系統(tǒng)的線程是直接映射關(guān)系。即當(dāng)線程本地存儲、緩沖區(qū)分配、同步對象、棧、程序計(jì)數(shù)器等準(zhǔn)備好以后,就會創(chuàng)建一個(gè)操作系統(tǒng)原生線程。Java 線程結(jié)束,原生線程隨之被回收。操作系統(tǒng)負(fù)責(zé)調(diào)度所有線程,并把它們分配到任何可用的 CPU 上。當(dāng)原生線程初始化完畢,就會調(diào)用 Java 線程的 run() 方法。run() 返回時(shí),被處理未捕獲異常,原生線程將確認(rèn)由于它的結(jié)束是否要終止 JVM 進(jìn)程(比如這個(gè)線程是最后一個(gè)非守護(hù)線程)。當(dāng)線程結(jié)束時(shí),會釋放原生線程和 Java 線程的所有資源。

6. 參考文章

深入理解JVM——JVM內(nèi)存模式

JVM內(nèi)幕——Java虛擬機(jī)詳解

JVM介紹

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容