眾所周知,Java是具有跨平臺性,也就是放在任何一個操作平臺上運行的。這是因為Java自身有一個虛擬機,編寫代碼事先都會在Java虛擬機中進行編譯操作。只要系統(tǒng)中安裝了Java虛擬機,都可以運行Java程序。
在談 JVM 內存區(qū)域劃分之前,我們先來看一下 Java 程序的具體執(zhí)行過程,我畫了一幅圖。
Java 源代碼文件經過編譯器編譯后生成字節(jié)碼文件,然后交給 JVM 的類加載器,加載完畢后,交給執(zhí)行引擎執(zhí)行。在整個執(zhí)行的過程中,JVM 會用一塊空間來存儲程序執(zhí)行期間需要用到的數據,這塊空間一般被稱為運行時數據區(qū),也就是常說的 JVM 內存。
所以,當我們在談 JVM 內存區(qū)域劃分的時候,其實談的就是這塊空間——運行時數據區(qū)。
大家應該對官方出品的《Java 虛擬機規(guī)范》有所了解吧?了解這個規(guī)范可以讓我們更深入地理解 JVM。該規(guī)范主要包含 6 個部分,分別是:
- 第一章:引言
- 第二章:Java 虛擬機結構
- 第三章:Java 虛擬機編譯
- 第四章:Class 文件
- 第五章:加載、鏈接和初始化
- 第六章:Java 虛擬機指令集
- 第七章:操作碼
根據第二章 Java 虛擬機結構中的規(guī)定,運行時數據區(qū)可以分為以下幾個部分,見下圖。
01、程序計數器
程序計數器(Program Counter Register)所占的內存空間不大,很小一塊,可以看作是當前線程所執(zhí)行的字節(jié)碼指令的行號指示器。字節(jié)碼解釋器會在工作的時候改變這個計數器的值來選取下一條需要執(zhí)行的字節(jié)碼指令,像分支、循環(huán)、跳轉、異常處理、線程恢復等功能都需要依賴這個計數器來完成。
在 JVM 中,多線程是通過線程輪流切換來獲得 CPU 執(zhí)行時間的,因此,在任一具體時刻,一個 CPU 的內核只會執(zhí)行一條線程中的指令,因此,為了線程切換后能恢復到正確的執(zhí)行位置,每個線程都需要有一個獨立的程序計數器,并且不能互相干擾,否則就會影響到程序的正常執(zhí)行次序。
也就是說,我們要求程序計數器是線程私有的。
《Java 虛擬機規(guī)范》中規(guī)定,如果線程執(zhí)行的是非本地(native)方法,則程序計數器中保存的是當前需要執(zhí)行的指令地址;如果線程執(zhí)行的是本地方法,則程序計數器中的值是 undefined。
為什么本地方法在程序計數器中的值是 undefined 的?因為本地方法大多是通過 C/C++ 實現(xiàn)的,并未編譯成需要執(zhí)行的字節(jié)碼指令。
由于程序計數器中存儲的數據所占的空間不會隨程序的執(zhí)行而發(fā)生大小上的改變,因此,程序計數器是不會發(fā)生內存溢出現(xiàn)象(OutOfMemory)的。
02、Java 虛擬機棧
Java 虛擬機棧中是一個個棧幀,每個棧幀對應一個被調用的方法。當線程執(zhí)行一個方法時,會創(chuàng)建一個對應的棧幀,并將棧幀壓入棧中。當方法執(zhí)行完畢后,將棧幀從棧中移除。棧遵循的是后進先出的原則,所以線程當前執(zhí)行的方法對應的棧幀必定在 Java 虛擬機棧的頂部。
棧幀包含以下 5 個部分,見下圖。
1.局部變量表
顧名思義,就是用來存儲方法中的局部變量的,包括方法的參數。對于基本數據類型的變量,直接存儲變量的值;對于引用類型的變量,存儲的是對象的引用。局部變量表的大小在編譯期間就確定了,程序執(zhí)行期間,它的大小是不會改變的。
2.操作數棧
表達式的計算是在操作數棧中完成的。當一個方法剛開始執(zhí)行的時候,這個方法的操作數棧是空的,在方法的執(zhí)行過程中,會有各種字節(jié)碼指令往操作數棧中寫入和提取內容,也就是入棧/出棧操作。例如,在做算術運算的時候是通過操作數棧來進行的,又或者在調用其他方法的時候是通過操作數棧來進行參數傳遞的。
3.指向運行時常量池的引用
當前方法所屬的類的運行時常量池的引用,引用其他的常量類或者使用字符串常量池中的字符串。
4.方法返回地址
方法執(zhí)行完(不論是正常執(zhí)行還是發(fā)生了異常)后需要返回到方法被調用的位置,程序才能繼續(xù)執(zhí)行,方法返回地址保存一些用來幫助恢復上層方法的執(zhí)行狀態(tài)的信息。
5.動態(tài)鏈接
每個棧幀都包含了一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調用過程中的動態(tài)鏈接。
與程序計數器一樣,Java 虛擬機棧也是線程私有的,它的生命周期和線程相同,描述的是 Java 方法執(zhí)行的內存模型,每次方法調用的數據都是通過棧傳遞的。
Java 虛擬機棧會出現(xiàn)兩種錯誤:
- StackOverFlowError:當線程請求棧的深度超過 Java 虛擬機棧的最大深度的時候拋出。
- OutOfMemoryError:如果 Java 虛擬機棧允許動態(tài)擴容,當棧擴容時無法申請到足夠的內存時拋出。
最有名的 HotSpot 虛擬機的棧容量是不允許動態(tài)擴容的,所以在 HotSpot 虛擬機上是不會出現(xiàn) OutOfMemoryError 的。
03、本地方法棧
本地方法棧與 Java 虛擬機棧類似,區(qū)別是本地方法棧執(zhí)行的是本地方法,也就是帶有 native 關鍵字修飾的方法。
在 HotSpot 虛擬機中,本地方法棧和 Java 虛擬機棧不做區(qū)分。
04、堆
堆是所有線程共享的一塊內存區(qū)域,在 Java 虛擬機啟動的時候創(chuàng)建,用來存儲對象(數組也是一種對象)。
以前,Java 中“幾乎”所有的對象都會在堆中分配,但隨著 JIT(Just-In-Time)編譯器的發(fā)展和逃逸技術的逐漸成熟,所有的對象都分配到堆上漸漸變得不那么“絕對”了。從 JDK 7 開始,Java 虛擬機已經默認開啟逃逸分析了,意味著如果某些方法中的對象引用沒有被返回或者未被外面使用(也就是未逃逸出去),那么對象可以直接在棧上分配內存。
簡單解釋一下 JIT 和逃逸分析。
常見的編譯型語言如 C++,通常會把代碼直接編譯成 CPU 所能理解的機器碼來運行。而 Java 為了實現(xiàn)“一次編譯,處處運行”的特性,把編譯的過程分成兩部分,首先它會先由 javac 編譯成通用的中間形式——字節(jié)碼,然后再由解釋器逐條將字節(jié)碼解釋為機器碼來執(zhí)行。所以在性能上,Java 可能會干不過 C++ 這類編譯型語言。
為了優(yōu)化 Java 的性能 ,JVM 在解釋器之外引入了 JIT 編譯器:當程序運行時,解釋器首先發(fā)揮作用,代碼可以直接執(zhí)行。隨著時間推移,即時編譯器逐漸發(fā)揮作用,把越來越多的代碼編譯優(yōu)化成本地代碼,來獲取更高的執(zhí)行效率。解釋器這時可以作為編譯運行的降級手段,在一些不可靠的編譯優(yōu)化出現(xiàn)問題時,再切換回解釋執(zhí)行,保證程序可以正常運行。
逃逸分析(Escape Analysis),簡單來講就是,Hotspot 虛擬機可以分析新創(chuàng)建對象的使用范圍,并決定是否在 Java 堆上分配內存的一項技術。
堆是 Java 垃圾收集器管理的主要區(qū)域,因此也被稱作 GC 堆(Garbage Collected Heap)。從垃圾回收的角度來看,由于垃圾收集器基本都采用了分代垃圾收集的算法,所以堆還可以細分為:新生代和老年代。新生代還可以細分為:Eden 空間、From Survivor、To Survivor 空間等。進一步劃分的目的是更好地回收內存,或者更快地分配內存。
堆這最容易出現(xiàn)的就是 OutOfMemoryError 錯誤,分為以下幾種表現(xiàn)形式:
- OutOfMemoryError: GC Overhead Limit Exceeded:當 JVM 花太多時間執(zhí)行垃圾回收并且只能回收很少的堆空間時,就會發(fā)生該錯誤。
- java.lang.OutOfMemoryError: Java heap space:假如在創(chuàng)建新的對象時, 堆內存中的空間不足以存放新創(chuàng)建的對象, 就會引發(fā)該錯誤。和本機的物理內存無關,和我們配置的虛擬機內存大小有關!
05、元空間
JDK 8 的時候,原有的方法區(qū)(更準確的說應該是永久代)被徹底移除,取而代之的是元空間。
我們來說說方法區(qū)吧。方法區(qū)和堆一樣,是線程共享的區(qū)域,它用來存儲已經被 Java 虛擬機加載的類信息、常量、靜態(tài)變量,以及便器編譯后的代碼等。
在有些地方,方法區(qū)也被稱為永久代。但其實不能這么理解。
《Java 虛擬機規(guī)范》中只規(guī)定了有方法區(qū)這么一個概念和它的作用,并沒有規(guī)定如何去實現(xiàn)它。那么不同的 Java 虛擬機可能就會有不同的實現(xiàn)。永久代是 HotSpot 對方法區(qū)的一種實現(xiàn)形式。也就是說,永久代只是 HotSpot 中的一個概念,而方法區(qū)則是 Java 虛擬機規(guī)范中的一個定義,一種規(guī)范。
換句話說,方法區(qū)和永久代的關系就像是 Java 中接口和類的關系,類實現(xiàn)了接口。
在方法區(qū)中,還有一塊非常重要的部分,也就是運行時常量池。在講 class 文件的時候,提到了每個 class 文件都會有個常量池,用來存放字符串常量、類和接口的名字、字段名、常量等等。運行時常量池和 class 文件的常量池是一一對應的,它就是通過 class 文件中的常量池來構建的。
JDK 7 之前,運行時常量池中包含著字符串常量池,都在方法區(qū)。
JDK 7 的時候,字符串常量池從方法區(qū)中拿出來放到了堆中,運行時常量池中的其他東西還在方法區(qū)中。
JDK 8 的時候,HotSpot 移除了永久代,也就是說方法區(qū)不存在了,取而代之的是元空間。也就意味著字符串常量池在堆中,運行時常量池跑到了元空間。
再來說說為什么要將永久代 (PermGen) 或者說方法區(qū)替換為元空間 (MetaSpace) 。
- 永久代放在 Java 虛擬機中,就會受到 Java 虛擬機內存大小的限制,而元空間使用的是本地內存,也就脫離了 Java 虛擬機內存的限制。
- JDK 8 的時候,在 HotSpot 中融合了 JRockit 虛擬機,而 JRockit 中并沒有永久代的概念,因此新的 HotSpot 就沒有必要再開辟一塊空間來作為永久代了。
對于我們 Java 程序員來說,不需要像 C/C++ 程序員那樣時時刻刻關心著內存泄露和內存溢出的問題,但實際的工作中,這兩個問題出現(xiàn)的頻率還是蠻高的,尤其是在多線程并發(fā)的情況下。如果不了解 Java 虛擬機是如何管理內存的,那么一旦遇到問題可能就會束手無策。
了解 Java 虛擬機的內存區(qū)域劃分有助于我們更好的去理解 Java 虛擬機,從而掌握內存問題排查的主動權。
到此本篇關于 Java 虛擬機內存區(qū)域的劃分的詳細內容就介紹結束了,想要了解更多相關 Java虛擬機的其他內容請搜索W3Cschool以前的文章或繼續(xù)瀏覽下面的相關文章,也希望大家以后多多支持我們!