App下載

詳談Java虛擬機(jī)中內(nèi)存區(qū)域的具體分配情況

猿友 2021-08-03 11:26:27 瀏覽數(shù) (2025)
反饋

眾所周知,Java是具有跨平臺性,也就是放在任何一個操作平臺上運行的。這是因為Java自身有一個虛擬機(jī),編寫代碼事先都會在Java虛擬機(jī)中進(jìn)行編譯操作。只要系統(tǒng)中安裝了Java虛擬機(jī),都可以運行Java程序。

在談 JVM 內(nèi)存區(qū)域劃分之前,我們先來看一下 Java 程序的具體執(zhí)行過程,我畫了一幅圖。

output_22_0.png

Java 源代碼文件經(jīng)過編譯器編譯后生成字節(jié)碼文件,然后交給 JVM 的類加載器,加載完畢后,交給執(zhí)行引擎執(zhí)行。在整個執(zhí)行的過程中,JVM 會用一塊空間來存儲程序執(zhí)行期間需要用到的數(shù)據(jù),這塊空間一般被稱為運行時數(shù)據(jù)區(qū),也就是常說的 JVM 內(nèi)存。

所以,當(dāng)我們在談 JVM 內(nèi)存區(qū)域劃分的時候,其實談的就是這塊空間——運行時數(shù)據(jù)區(qū)。

大家應(yīng)該對官方出品的《Java 虛擬機(jī)規(guī)范》有所了解吧?了解這個規(guī)范可以讓我們更深入地理解 JVM。該規(guī)范主要包含 6 個部分,分別是:

  • 第一章:引言
  • 第二章:Java 虛擬機(jī)結(jié)構(gòu)
  • 第三章:Java 虛擬機(jī)編譯
  • 第四章:Class 文件
  • 第五章:加載、鏈接和初始化
  • 第六章:Java 虛擬機(jī)指令集
  • 第七章:操作碼

根據(jù)第二章 Java 虛擬機(jī)結(jié)構(gòu)中的規(guī)定,運行時數(shù)據(jù)區(qū)可以分為以下幾個部分,見下圖。

output_22_0.png

01、程序計數(shù)器

程序計數(shù)器(Program Counter Register)所占的內(nèi)存空間不大,很小一塊,可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼指令的行號指示器。字節(jié)碼解釋器會在工作的時候改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,像分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等功能都需要依賴這個計數(shù)器來完成。

在 JVM 中,多線程是通過線程輪流切換來獲得 CPU 執(zhí)行時間的,因此,在任一具體時刻,一個 CPU 的內(nèi)核只會執(zhí)行一條線程中的指令,因此,為了線程切換后能恢復(fù)到正確的執(zhí)行位置,每個線程都需要有一個獨立的程序計數(shù)器,并且不能互相干擾,否則就會影響到程序的正常執(zhí)行次序。

也就是說,我們要求程序計數(shù)器是線程私有的。

《Java 虛擬機(jī)規(guī)范》中規(guī)定,如果線程執(zhí)行的是非本地(native)方法,則程序計數(shù)器中保存的是當(dāng)前需要執(zhí)行的指令地址;如果線程執(zhí)行的是本地方法,則程序計數(shù)器中的值是 undefined。

為什么本地方法在程序計數(shù)器中的值是 undefined 的?因為本地方法大多是通過 C/C++ 實現(xiàn)的,并未編譯成需要執(zhí)行的字節(jié)碼指令。

由于程序計數(shù)器中存儲的數(shù)據(jù)所占的空間不會隨程序的執(zhí)行而發(fā)生大小上的改變,因此,程序計數(shù)器是不會發(fā)生內(nèi)存溢出現(xiàn)象(OutOfMemory)的。

02、Java 虛擬機(jī)棧

Java 虛擬機(jī)棧中是一個個棧幀,每個棧幀對應(yīng)一個被調(diào)用的方法。當(dāng)線程執(zhí)行一個方法時,會創(chuàng)建一個對應(yīng)的棧幀,并將棧幀壓入棧中。當(dāng)方法執(zhí)行完畢后,將棧幀從棧中移除。遵循的是后進(jìn)先出的原則,所以線程當(dāng)前執(zhí)行的方法對應(yīng)的棧幀必定在 Java 虛擬機(jī)棧的頂部。

棧幀包含以下 5 個部分,見下圖。

output_22_0.png

1.局部變量表

顧名思義,就是用來存儲方法中的局部變量的,包括方法的參數(shù)。對于基本數(shù)據(jù)類型的變量,直接存儲變量的值;對于引用類型的變量,存儲的是對象的引用。局部變量表的大小在編譯期間就確定了,程序執(zhí)行期間,它的大小是不會改變的。

2.操作數(shù)棧

表達(dá)式的計算是在操作數(shù)棧中完成的。當(dāng)一個方法剛開始執(zhí)行的時候,這個方法的操作數(shù)棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內(nèi)容,也就是入棧/出棧操作。例如,在做算術(shù)運算的時候是通過操作數(shù)棧來進(jìn)行的,又或者在調(diào)用其他方法的時候是通過操作數(shù)棧來進(jìn)行參數(shù)傳遞的。

3.指向運行時常量池的引用

當(dāng)前方法所屬的類的運行時常量池的引用,引用其他的常量類或者使用字符串常量池中的字符串。

4.方法返回地址

方法執(zhí)行完(不論是正常執(zhí)行還是發(fā)生了異常)后需要返回到方法被調(diào)用的位置,程序才能繼續(xù)執(zhí)行,方法返回地址保存一些用來幫助恢復(fù)上層方法的執(zhí)行狀態(tài)的信息。

5.動態(tài)鏈接

每個棧幀都包含了一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)鏈接。

與程序計數(shù)器一樣,Java 虛擬機(jī)棧也是線程私有的,它的生命周期和線程相同,描述的是 Java 方法執(zhí)行的內(nèi)存模型,每次方法調(diào)用的數(shù)據(jù)都是通過棧傳遞的。

Java 虛擬機(jī)棧會出現(xiàn)兩種錯誤:

  • StackOverFlowError:當(dāng)線程請求棧的深度超過 Java 虛擬機(jī)棧的最大深度的時候拋出。
  • OutOfMemoryError:如果 Java 虛擬機(jī)棧允許動態(tài)擴(kuò)容,當(dāng)棧擴(kuò)容時無法申請到足夠的內(nèi)存時拋出。

最有名的 HotSpot 虛擬機(jī)的棧容量是不允許動態(tài)擴(kuò)容的,所以在 HotSpot 虛擬機(jī)上是不會出現(xiàn) OutOfMemoryError 的。

03、本地方法棧

本地方法棧與 Java 虛擬機(jī)棧類似,區(qū)別是本地方法棧執(zhí)行的是本地方法,也就是帶有 native 關(guān)鍵字修飾的方法。

在 HotSpot 虛擬機(jī)中,本地方法棧和 Java 虛擬機(jī)棧不做區(qū)分。

04、堆

堆是所有線程共享的一塊內(nèi)存區(qū)域,在 Java 虛擬機(jī)啟動的時候創(chuàng)建,用來存儲對象(數(shù)組也是一種對象)。

以前,Java 中“幾乎”所有的對象都會在堆中分配,但隨著 JIT(Just-In-Time)編譯器的發(fā)展和逃逸技術(shù)的逐漸成熟,所有的對象都分配到堆上漸漸變得不那么“絕對”了。從 JDK 7 開始,Java 虛擬機(jī)已經(jīng)默認(rèn)開啟逃逸分析了,意味著如果某些方法中的對象引用沒有被返回或者未被外面使用(也就是未逃逸出去),那么對象可以直接在棧上分配內(nèi)存。

簡單解釋一下 JIT 和逃逸分析。

常見的編譯型語言如 C++,通常會把代碼直接編譯成 CPU 所能理解的機(jī)器碼來運行。而 Java 為了實現(xiàn)“一次編譯,處處運行”的特性,把編譯的過程分成兩部分,首先它會先由 javac 編譯成通用的中間形式——字節(jié)碼,然后再由解釋器逐條將字節(jié)碼解釋為機(jī)器碼來執(zhí)行。所以在性能上,Java 可能會干不過 C++ 這類編譯型語言。

為了優(yōu)化 Java 的性能 ,JVM 在解釋器之外引入了 JIT 編譯器:當(dāng)程序運行時,解釋器首先發(fā)揮作用,代碼可以直接執(zhí)行。隨著時間推移,即時編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯優(yōu)化成本地代碼,來獲取更高的執(zhí)行效率。解釋器這時可以作為編譯運行的降級手段,在一些不可靠的編譯優(yōu)化出現(xiàn)問題時,再切換回解釋執(zhí)行,保證程序可以正常運行。

逃逸分析(Escape Analysis),簡單來講就是,Hotspot 虛擬機(jī)可以分析新創(chuàng)建對象的使用范圍,并決定是否在 Java 堆上分配內(nèi)存的一項技術(shù)。

堆是 Java 垃圾收集器管理的主要區(qū)域,因此也被稱作 GC 堆(Garbage Collected Heap)。從垃圾回收的角度來看,由于垃圾收集器基本都采用了分代垃圾收集的算法,所以堆還可以細(xì)分為:新生代和老年代。新生代還可以細(xì)分為:Eden 空間、From Survivor、To Survivor 空間等。進(jìn)一步劃分的目的是更好地回收內(nèi)存,或者更快地分配內(nèi)存。

堆這最容易出現(xiàn)的就是 OutOfMemoryError 錯誤,分為以下幾種表現(xiàn)形式:

  • OutOfMemoryError: GC Overhead Limit Exceeded:當(dāng) JVM 花太多時間執(zhí)行垃圾回收并且只能回收很少的堆空間時,就會發(fā)生該錯誤。
  • java.lang.OutOfMemoryError: Java heap space:假如在創(chuàng)建新的對象時, 堆內(nèi)存中的空間不足以存放新創(chuàng)建的對象, 就會引發(fā)該錯誤。和本機(jī)的物理內(nèi)存無關(guān),和我們配置的虛擬機(jī)內(nèi)存大小有關(guān)!

05、元空間

JDK 8 的時候,原有的方法區(qū)(更準(zhǔn)確的說應(yīng)該是永久代)被徹底移除,取而代之的是元空間。

我們來說說方法區(qū)吧。方法區(qū)和堆一樣,是線程共享的區(qū)域,它用來存儲已經(jīng)被 Java 虛擬機(jī)加載的類信息、常量、靜態(tài)變量,以及便器編譯后的代碼等。

在有些地方,方法區(qū)也被稱為永久代。但其實不能這么理解。

《Java 虛擬機(jī)規(guī)范》中只規(guī)定了有方法區(qū)這么一個概念和它的作用,并沒有規(guī)定如何去實現(xiàn)它。那么不同的 Java 虛擬機(jī)可能就會有不同的實現(xiàn)。永久代是 HotSpot 對方法區(qū)的一種實現(xiàn)形式。也就是說,永久代只是 HotSpot 中的一個概念,而方法區(qū)則是 Java 虛擬機(jī)規(guī)范中的一個定義,一種規(guī)范。

換句話說,方法區(qū)和永久代的關(guān)系就像是 Java 中接口和類的關(guān)系,類實現(xiàn)了接口。

在方法區(qū)中,還有一塊非常重要的部分,也就是運行時常量池。在講 class 文件的時候,提到了每個 class 文件都會有個常量池,用來存放字符串常量、類和接口的名字、字段名、常量等等。運行時常量池和 class 文件的常量池是一一對應(yīng)的,它就是通過 class 文件中的常量池來構(gòu)建的。

JDK 7 之前,運行時常量池中包含著字符串常量池,都在方法區(qū)。

JDK 7 的時候,字符串常量池從方法區(qū)中拿出來放到了堆中,運行時常量池中的其他東西還在方法區(qū)中。

JDK 8 的時候,HotSpot 移除了永久代,也就是說方法區(qū)不存在了,取而代之的是元空間。也就意味著字符串常量池在堆中,運行時常量池跑到了元空間。

再來說說為什么要將永久代 (PermGen) 或者說方法區(qū)替換為元空間 (MetaSpace) 。

  1. 永久代放在 Java 虛擬機(jī)中,就會受到 Java 虛擬機(jī)內(nèi)存大小的限制,而元空間使用的是本地內(nèi)存,也就脫離了 Java 虛擬機(jī)內(nèi)存的限制。
  2. JDK 8 的時候,在 HotSpot 中融合了 JRockit 虛擬機(jī),而 JRockit 中并沒有永久代的概念,因此新的 HotSpot 就沒有必要再開辟一塊空間來作為永久代了。

對于我們 Java 程序員來說,不需要像 C/C++ 程序員那樣時時刻刻關(guān)心著內(nèi)存泄露和內(nèi)存溢出的問題,但實際的工作中,這兩個問題出現(xiàn)的頻率還是蠻高的,尤其是在多線程并發(fā)的情況下。如果不了解 Java 虛擬機(jī)是如何管理內(nèi)存的,那么一旦遇到問題可能就會束手無策。

了解 Java 虛擬機(jī)的內(nèi)存區(qū)域劃分有助于我們更好的去理解 Java 虛擬機(jī),從而掌握內(nèi)存問題排查的主動權(quán)。

到此本篇關(guān)于 Java 虛擬機(jī)內(nèi)存區(qū)域的劃分的詳細(xì)內(nèi)容就介紹結(jié)束了,想要了解更多相關(guān) Java虛擬機(jī)的其他內(nèi)容請搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,也希望大家以后多多支持我們!

0 人點贊