自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

17張圖帶你了解,JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)

原創(chuàng) 精選
開發(fā) 前端
JVM 會把Java的字節(jié)碼加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)內(nèi),這個(gè)內(nèi)存區(qū)域分為:方法區(qū)、堆、虛擬機(jī)棧、本地方法棧以及程序計(jì)數(shù)器。

開篇

眾所周知,Java程序的執(zhí)行需要依賴于JVM(Java 虛擬機(jī))。JVM 會將Java源代碼編譯成字節(jié)碼文件,然后使用類加載器將其加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)中執(zhí)行,垃圾收集器也會針對運(yùn)行時(shí)數(shù)據(jù)區(qū)進(jìn)行對象回收的工作。今天就來說說JVM的運(yùn)行時(shí)數(shù)據(jù)區(qū)。

運(yùn)行時(shí)數(shù)據(jù)區(qū)概述

在計(jì)算機(jī)世界中,內(nèi)存是十分重要的系統(tǒng)資源,它承載著操作系統(tǒng)和應(yīng)用程序?qū)崟r(shí)運(yùn)行的責(zé)任。JVM內(nèi)存布局規(guī)定了Java在運(yùn)行過程中內(nèi)存申請、分配、管理的策略,從而保證了JVM的高效穩(wěn)定運(yùn)行。

Java虛擬機(jī)在執(zhí)行Java程序的過程中,會將涉及到的數(shù)據(jù)劃分到不同的內(nèi)存區(qū)域去管理,在這些數(shù)據(jù)區(qū)域,有些是隨著虛擬機(jī)啟動而創(chuàng)建,虛擬機(jī)關(guān)閉而銷毀。還有一部分是隨著線程生命周期創(chuàng)建銷毀的。這部分區(qū)域就是接下來要講的Java虛擬機(jī)的運(yùn)行時(shí)數(shù)據(jù)區(qū)。

圖1 運(yùn)行時(shí)數(shù)據(jù)區(qū)

如圖1所示,紅色的部分就是運(yùn)行時(shí)數(shù)據(jù)區(qū),它包括:方法區(qū)、堆、虛擬機(jī)棧、本地方法棧以及程序計(jì)數(shù)器五個(gè)部分。

圖1中標(biāo)注為黃色的方法區(qū)和堆是線程間共享的,也就是說它們會隨著虛擬機(jī)啟動而創(chuàng)建,隨著虛擬機(jī)退出而銷毀。橙色部分為每個(gè)線程單獨(dú)享有的,即它們與線程是一一對應(yīng)的,會隨著線程開始和結(jié)束而創(chuàng)建和銷毀。在HotSpot JVM中,每個(gè)線程都與操作系統(tǒng)的本地線程直接映射,例如:有一個(gè)Java線程準(zhǔn)備好執(zhí)行時(shí),就有一個(gè)操作系統(tǒng)的本地線程被創(chuàng)建并且與Java 線程對應(yīng),當(dāng)Java線程執(zhí)行終止后,本地線程也會被回收。同時(shí)操作系統(tǒng)負(fù)責(zé)線程調(diào)度,及分配對應(yīng)的CPU執(zhí)行線程,一旦操作系統(tǒng)的本地線程初始化成功,它就會調(diào)用Java線程中的的run()方法去執(zhí)行Java線程。

褐色部分的執(zhí)行引擎就負(fù)責(zé)讀取指令并且交由CPU執(zhí)行,它包括解釋器、JIT(即時(shí)編譯器),GC(垃圾回收器)。而另外一個(gè)褐色的本地庫接口會提供Java程序調(diào)用的native方法。

另外,運(yùn)行時(shí)數(shù)據(jù)區(qū)的劃分也隨著JDK的發(fā)展不斷變遷,如圖2 所示, JDK 1.6、JDK 1.7、JDK 1.8 的內(nèi)存劃分都會有所不同。

圖2 運(yùn)行時(shí)數(shù)據(jù)區(qū)的變遷

如圖2 所示,在JDK 1.8 中加入了元數(shù)據(jù)區(qū)的概念,將原來保存在方法區(qū)中的運(yùn)行時(shí)常量池和類常量池都包括其中。

虛擬機(jī)棧

上面介紹了JVM 運(yùn)行時(shí)數(shù)據(jù)區(qū)的概念和組成,接下來一次介紹每個(gè)組成部分,首先從虛擬機(jī)棧開始。

每個(gè)Java線程都會對應(yīng)一個(gè)虛擬機(jī)棧,換句話說多個(gè)線程就對應(yīng)多個(gè)虛擬機(jī)棧。上面講過了虛擬機(jī)棧是線程私有,虛擬機(jī)棧中包含多個(gè)棧幀(Stack Frame),每一個(gè)棧幀是為方法執(zhí)行而創(chuàng)建的,棧幀中描述的是Java方法執(zhí)行的內(nèi)存模型。每個(gè)方法從調(diào)用開始直到完成的全過程都對應(yīng)著一個(gè)棧幀。棧幀是用來管理Java程序的運(yùn)行,并保存方法的局部變量、部分結(jié)果、并參與方法的調(diào)用與返回。在活動線程中,只有一個(gè)棧幀是處于活躍狀態(tài)的,也就是說只有位于棧頂?shù)臈攀怯行У?,稱為當(dāng)前棧幀,與這個(gè)棧幀相關(guān)聯(lián)的方法稱為當(dāng)前方法。執(zhí)行引擎運(yùn)行的所有字節(jié)碼指令都只針對當(dāng)前棧幀進(jìn)行操作。

如圖3 所示,每個(gè)Java 方法都會對應(yīng)一個(gè)棧幀,左邊的四個(gè)方法就對應(yīng)了四個(gè)棧幀,從下往上依次是方法調(diào)用的順序,最終方法1 會調(diào)用方法4, 此時(shí)正在執(zhí)行方法4 ,它對應(yīng)的棧幀4 就是“當(dāng)前棧幀”,就是出于活躍狀態(tài)的,其包含了局部變量表、操作數(shù)棧、動態(tài)鏈接以及返回地址等信息。

圖3 棧幀結(jié)構(gòu)

局部變量表

它定義為數(shù)字?jǐn)?shù)組,主要用于存儲方法參數(shù)和定義在方體內(nèi)的局部變量,包含基本數(shù)據(jù)類型,對象引用,以及returnAddress類型。它建立在線程的棧上,是線程的私有數(shù)據(jù),因此不存在數(shù)據(jù)的安全問題。

局部變量表所需的容量在編譯期間確定,在運(yùn)行期間是不改變其容量。方法嵌套調(diào)用的次數(shù)由棧的容量來決定,例如圖3就進(jìn)行了4個(gè)方法的嵌套,也就是說棧越大,方法嵌套調(diào)用次數(shù)越多。對一個(gè)函數(shù)而言,它的參數(shù)和局部變量越多,對應(yīng)的棧幀就越大。因此,函數(shù)調(diào)用就會占用更多的??臻g。局部變量表中的變量只在當(dāng)前方法調(diào)用中有效。在方法執(zhí)行時(shí),虛擬機(jī)通過使用局部變量表完成參數(shù)值到參數(shù)變量列表的傳遞。當(dāng)方法調(diào)用結(jié)束后,隨著方法棧幀的銷毀,局部變量表也會隨之銷毀。

操作數(shù)棧

它是一個(gè)后進(jìn)先出的棧,在方法執(zhí)行的過程中,根據(jù)字節(jié)碼指令、往棧中寫入或取出數(shù)據(jù),即入棧/出棧。字節(jié)碼指令將值壓入操作棧,其余的字節(jié)碼指令將操作數(shù)取出棧,進(jìn)行操作之后再將結(jié)果壓入棧。操作包括:復(fù)制、交換、求和等。

這樣講比較抽象,來看一個(gè)具體的例子。

如圖4 所示,生成一個(gè)testAdd 方法,給變量i和j 分別賦值為1 和2 ,然后讓其相加并且把結(jié)果賦值給k。

圖4 操作數(shù)棧代碼

使用jclasslib反編譯上面的代碼得到圖5 的結(jié)果。

圖5 jclasslib反編譯結(jié)果

如圖6 所示,當(dāng)執(zhí)行地址 0 的時(shí)候操作指令為bipush,此時(shí)程序寄存器的地址顯示為0 ,bipush 命令將 1 壓入到操作數(shù)棧的頂部。

圖6

如圖7 所示,當(dāng)指令地址到2 的時(shí)候,程序寄存器顯示為2, 此時(shí)執(zhí)行istore_1 的指令,將棧頂?shù)臄?shù)字1 保存到局部變量表中。

圖7

如圖8所示,指令地址執(zhí)行到3 的時(shí)候,程序寄存器為3 , bipush指令把2 壓入到操作數(shù)棧的頂部。

圖8

在指令地址為5 的時(shí)候,程序寄存器的值為5, istore_2指令將操作數(shù)棧中的2 保存到局部變量表中的2 的位置。

圖9

如圖10所示,指令地址為6 的時(shí)候,執(zhí)行iload_1 指令獲取局部變量表中 位置為1 的值,也就是1 并且把它放到操作數(shù)棧的頂部。

圖10

如圖11所示,指令地址為7 的時(shí)候,執(zhí)行iload_2 指令,從局部變量表2 的位置取出值2 放到操作數(shù)棧的頂部。

圖11

如圖12 所示,在指令地址為8 時(shí),執(zhí)行iadd 指令,將操作數(shù)棧的兩個(gè)數(shù)字1和2 相加結(jié)果為3,并且將其放到操作數(shù)棧的頂部。

圖12

如圖13 所示,接著執(zhí)行指令地址 9 , istore_3 執(zhí)行之后將操作數(shù)棧頂?shù)? 保存到局部變量表3 的位置,完成相加的操作,最后通過指令地址10 中的return指令返回方法。

圖13

動態(tài)鏈接

在介紹動態(tài)鏈接之前先說說靜態(tài)鏈接,即字節(jié)碼文件被裝載進(jìn)JVM內(nèi)部時(shí),如果被調(diào)用的目標(biāo)方法在編譯期可知,且運(yùn)行期間保持不變時(shí)。這種情況下將調(diào)用方法的符號引用轉(zhuǎn)換為直接引用的過程稱之為靜態(tài)鏈接。但是,如果被調(diào)用方法在編譯期間無法被確定下來,只能在程序運(yùn)行時(shí)將調(diào)用方法的符號引用轉(zhuǎn)換為直接引用,由于這種引用轉(zhuǎn)換的過程具備動態(tài)性,被稱為動態(tài)鏈接。

如圖14所示,上面是反編譯的字節(jié)碼部分,對應(yīng)的#3、#6、#5等等就是符號引用,下面的Constant pool就是常量池。在Java源文件被編譯成字節(jié)碼文件時(shí),所有的變量和方法引用都作為符號引用保存在class文件的常量池中。例如在指令第9行會執(zhí)行invokevirtual的指令,對應(yīng)的符號引用就是#7,所對應(yīng)常量池中的#7 就是Methodref,也就是方法引用,這里對應(yīng)的方法是com.itcast.java.DynamicLinkTest中的methodA方法。

圖14 從字節(jié)碼到常量池中的方法引用

如圖15所示,當(dāng)字節(jié)碼文件被加載后,字節(jié)碼文件中的一些數(shù)據(jù),如類型信息、域信息、方法信息等,就會被放置到方法區(qū)中。而棧幀中的當(dāng)前類常量池引用(Current Class Constant Pool Reference)保存的是方法符號引用,真正的方法引用放在了方法區(qū)(Method Area)中的方法引用(method reference)中了,這個(gè)方法引用是為了支持代碼的動態(tài)鏈接。動態(tài)鏈接就是將符號引用轉(zhuǎn)化為直接引用。

圖15 棧幀中的當(dāng)前類常量池引用對應(yīng)方法區(qū)中的方法引用

JVM之所以這么設(shè)計(jì)是因?yàn)樽止?jié)碼文件需要數(shù)據(jù)支持的量會很大,因此不能直接將這些數(shù)據(jù)存放到字節(jié)碼中。針對方法的引用創(chuàng)建符號引用,這個(gè)符號引用放在棧幀的常量池引用中,而實(shí)際的方法和符號引用的對照表卻放在方法區(qū)的常量池中,這樣字節(jié)碼就可以通過常量池中的對照關(guān)系找到引用的方法,并且也不會增加棧幀的容量。

方法返回地址

當(dāng)一個(gè)方法開始執(zhí)行后,可以通過兩種方式退出該方法。第一種是執(zhí)行引擎遇到方法返回的字節(jié)碼指令,此時(shí)返回值會傳遞到上層調(diào)用者,這種方式稱為正常完成出口。另外一種退出方式是在方法執(zhí)行中遇到異常,這個(gè)異常在方法體內(nèi)沒有得到處理,就會導(dǎo)致方法退出,這種方式稱為異常完成出口。由于是異常退出,就不會給上層調(diào)用者任何返回值。無論采取上面那種退出方式,方法都會到處調(diào)用它的位置,程序才能繼續(xù)執(zhí)行。方法在返回的時(shí)候需要在棧幀中保存一些信息,用來恢復(fù)調(diào)用該方法的上層方法的執(zhí)行狀態(tài)。這里可以通過方法調(diào)用者的程序計(jì)數(shù)器存放返回地址,如果是正常退出方法,上層方法會從程序計(jì)數(shù)器中保存的地址繼續(xù)執(zhí)行接下來的步驟。如果是異常退出的情況,返回地址就需要異常處理器來確定了。

程序計(jì)數(shù)器

有了上面虛擬機(jī)棧的講解,對于程序計(jì)數(shù)器的理解會相對簡單點(diǎn)。記得在虛擬機(jī)棧中的操作數(shù)棧的例子中,提到了使用程序計(jì)數(shù)器記錄操作指令的地址。程序計(jì)數(shù)器就是一塊較小的內(nèi)存空間,它是當(dāng)前線程執(zhí)行的字節(jié)碼的行號(操作指令的地址)指示器。在棧幀中字節(jié)碼解釋器就是通過改變計(jì)數(shù)器的值來選去下一條要執(zhí)行的字節(jié)碼指令的,例如:分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等。

上面講虛擬機(jī)棧的時(shí)候提到過,多個(gè)執(zhí)行的Java線程就是多個(gè)虛擬機(jī)棧,每個(gè)棧中存在多個(gè)棧幀,在一個(gè)時(shí)刻只有一個(gè)棧幀執(zhí)行,也就是當(dāng)前棧幀。也就是說在一個(gè)時(shí)刻一個(gè)處理只會對一個(gè)線程中的一個(gè)幀棧執(zhí)行一條指令,而每個(gè)棧幀都會維護(hù)一個(gè)屬于自己的程序計(jì)數(shù)器,這個(gè)計(jì)數(shù)器就是來記錄指令執(zhí)行的地址的。每個(gè)線程的計(jì)數(shù)器不會相互影響,這也保證了在Java 多線程進(jìn)行切換的時(shí)候,每個(gè)線程都能夠保證正確的指令地址被讀取。

如圖 16所示,在invokevirtual的框圖中存在多個(gè)線程,每個(gè)線程就是一個(gè)虛擬機(jī)棧,每個(gè)線程中包含多個(gè)Frame 也就是棧幀,針對每個(gè)線程都會維護(hù)一個(gè)PC Registers也就是程序寄存器,它會記錄指令地址信息,從而讓方法實(shí)現(xiàn):跳轉(zhuǎn)、分支、循環(huán)、異常處理和線程恢復(fù)的功能。

圖16 程序計(jì)數(shù)器

本地方法棧

本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧為虛擬機(jī)所使用到的Native方法服務(wù)。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

說白了,本地方法(Native Method)就是一個(gè)Java調(diào)用非Java代碼的接口。 當(dāng)Java應(yīng)用需要與Java之外的環(huán)境交互時(shí)就需要使用本地方法,特別與底層系統(tǒng)、操作系統(tǒng)以及硬件打交道時(shí)就會用到本地方法。大家可以把本地方法理解為一種交流機(jī)制:它提供了一個(gè)對外的簡潔的接口,讓我們無需去了解Java應(yīng)用之外的細(xì)節(jié)。

那么JVM是如何使用Native Method的呢?當(dāng)一個(gè)類第一次被使用時(shí),類的字節(jié)碼會被加載到內(nèi)存,在字節(jié)碼的入口維持著該類所有方法描述符的list,包括:方法代碼來源,參數(shù),方法描述符(例如:public)等等。

如果方法描述符是native,同時(shí)描述符塊將有一個(gè)指向該方法實(shí)現(xiàn)的指針,而具體實(shí)現(xiàn)在DLL文件內(nèi),此時(shí)DLL文件會被操作系統(tǒng)加載到Java程序的地址空間里。當(dāng)一個(gè)帶有本地方法的類被加載時(shí),其相關(guān)的DLL并未被加載,因此指向方法實(shí)現(xiàn)的指針并不會被設(shè)置。當(dāng)本地方法被調(diào)用之前, DLL才會被加載,即通過調(diào)用java.system.loadLibrary()實(shí)現(xiàn)的。

堆和方法區(qū)

上面說的虛擬機(jī)棧、程序計(jì)數(shù)器和本地方法棧都是線程私有的,而接下來說的方法區(qū)和堆是線程共享的。這里把堆和方法區(qū)合起來說。

Java堆是Java虛擬機(jī)所管理內(nèi)存中最大的一塊,在虛擬機(jī)啟動時(shí)創(chuàng)建,被所有線程共享。Java對象實(shí)例以及數(shù)組都在堆上分配。堆的大小可以是固定的,也可以根據(jù)計(jì)算的需要進(jìn)行擴(kuò)展,如果不需要更大的堆,則可以收縮。堆的內(nèi)存不需要是連續(xù)的。Java虛擬機(jī)實(shí)現(xiàn)可以為程序員或用戶提供對堆初始大小的控制,如果可以動態(tài)擴(kuò)展或收縮堆,還可以控制堆的最大和最小大小。

Java堆是垃圾收集器管理的主要區(qū)域,所以也被稱為GC堆。從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java堆中還可以細(xì)分為:新生代和老年代;新生代再細(xì)分就是:Eden空間、From Survivor空間、ToSurvivor空間等。從內(nèi)存分配的角度來看,線程共享的Java堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB)。不論如何劃分,都與存放內(nèi)容無關(guān),無論哪個(gè)區(qū)域,存放的都仍然是對象實(shí)例;進(jìn)一步劃分的目的是為了更好的回收內(nèi)存,或者更快地分配內(nèi)存。

對于堆中垃圾回收的部分這里不展開說明,后面會有文章去介紹。

方法區(qū)

方法區(qū)和堆一樣是線程共享的內(nèi)存區(qū)域,它用來存放被虛擬機(jī)加載的類型信息、運(yùn)行時(shí)常量池、靜態(tài)變量、JIT代碼緩存、域信息、方法信息等。方法區(qū)(Method Area)與Java堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,有如下特點(diǎn):

  • 方法區(qū)在JVM啟動的時(shí)候被創(chuàng)建,并且它的實(shí)際的物理內(nèi)存空間和Java堆區(qū)一樣都可以是不連續(xù)的。
  • 方法區(qū)的大小,和堆空間一樣,可以選擇固定大小和可擴(kuò)展。
  • 方法區(qū)的大小決定了系統(tǒng)可以保存多少個(gè)類,如果系統(tǒng)定義了太多的類,導(dǎo)致方法區(qū)溢出,虛擬機(jī)就會拋出內(nèi)存溢出錯(cuò)誤:

java.lang.OutOfMemoryError:PermGenspace或者 java.lang.OutOfMemoryError: Metaspace。

  • 關(guān)閉JVM就會釋放這個(gè)區(qū)域的內(nèi)存。

這里把堆、方法區(qū)和虛擬機(jī)棧的關(guān)系整理一下。如圖17 所示,在右邊創(chuàng)建了AppMain 類,在運(yùn)行時(shí)JVM 會把AppMain的信息放入到方法區(qū),因?yàn)榉椒▍^(qū)會存放類型信息。同時(shí)main 的方法本身也會放入到方法區(qū)。接下來的new Sample(“測試1”)的語句中Sample的自定義對象會放到堆里面,而對應(yīng)的test1 應(yīng)用會放入到虛擬機(jī)棧中,對應(yīng)的test1.printName()方法的執(zhí)行會在虛擬機(jī)棧中的棧幀中通過指令執(zhí)行完成。另外下面的class Sample也是放到方法區(qū)中的,聲明的private name,其中name的引用放在虛擬機(jī)棧中,name對應(yīng)的對象放在堆中。對應(yīng)的printName方法是放在方法區(qū)中的。

圖17 棧、堆、方法區(qū)關(guān)系

總結(jié)

JVM 會把Java的字節(jié)碼加載到運(yùn)行時(shí)數(shù)據(jù)區(qū)內(nèi),這個(gè)內(nèi)存區(qū)域分為:方法區(qū)、堆、虛擬機(jī)棧、本地方法棧以及程序計(jì)數(shù)器。堆里面放對象,也是垃圾回收器要處理的對象;方法區(qū)放類型、方法描述、方法本體;程序計(jì)數(shù)器負(fù)責(zé)記錄虛擬機(jī)棧中指令執(zhí)行的地址;虛擬機(jī)棧對應(yīng)Java執(zhí)行的線程,對象的引用都保存在棧幀中,通過指令地址和指令執(zhí)行方法中的內(nèi)容;本地方法棧用來調(diào)用Java 之外的系統(tǒng)級別的接口。

譯者介紹

崔皓,51CTO社區(qū)編輯,資深架構(gòu)師,擁有18年的軟件開發(fā)和架構(gòu)經(jīng)驗(yàn),10年分布式架構(gòu)經(jīng)驗(yàn)。曾任惠普技術(shù)專家。樂于分享,撰寫了很多熱門技術(shù)文章,閱讀量超過60萬?!斗植际郊軜?gòu)原理與實(shí)踐》作者。

責(zé)任編輯:武曉燕 來源: 51CTO技術(shù)棧
相關(guān)推薦

2021-07-14 07:21:57

JVM運(yùn)行數(shù)據(jù)

2018-11-22 12:07:37

Java虛擬機(jī)結(jié)構(gòu)

2024-11-26 08:31:36

2022-01-17 22:09:50

JVM方法區(qū)數(shù)據(jù)

2020-06-28 07:39:44

Kafka分布式消息

2021-08-13 08:15:23

JVM 虛擬機(jī)Java

2017-12-07 18:02:01

Python新手運(yùn)行時(shí)錯(cuò)誤

2015-07-20 15:44:46

Swift框架MJExtension反射

2021-05-07 17:11:19

負(fù)載均衡運(yùn)維服務(wù)

2021-09-18 08:02:49

Go程序工具

2018-10-25 09:04:56

Java虛擬機(jī)JVM

2024-03-21 09:15:58

JS運(yùn)行的JavaScrip

2021-03-08 09:52:55

架構(gòu)運(yùn)維技術(shù)

2021-07-04 22:27:42

存儲BookKeeper系統(tǒng)

2018-12-13 09:27:31

后臺服務(wù)架構(gòu)

2019-07-12 09:30:12

DashboardDockerDNS

2021-09-11 15:38:23

容器運(yùn)行鏡像開放

2009-07-09 13:52:41

Inside JVM

2021-08-18 08:32:09

代碼運(yùn)行時(shí)間示波器

2024-03-20 10:46:00

云原生容器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號