1. 概述
根據(jù)Java虛擬機(jī)規(guī)范,Java程序在運(yùn)行時,在內(nèi)存中定義了若干個區(qū)域。這些區(qū)域的用途,生命周期各不相同。本文將盡量簡要地介紹這些數(shù)據(jù)區(qū),避免過多細(xì)節(jié)堆砌,具體細(xì)節(jié)以后再給出。數(shù)據(jù)區(qū)域可以由下圖表示

2. 運(yùn)行時數(shù)據(jù)區(qū)
如上圖所示,大體上我們可以把Java內(nèi)存區(qū)域劃分為方法區(qū),堆區(qū),Java虛擬機(jī)棧,本地方法棧,程序計數(shù)器。還有一個直接內(nèi)存(DirectMemory, 也稱堆外內(nèi)存,它不屬于運(yùn)行時數(shù)據(jù)區(qū)的內(nèi)容,使用過NIO的同學(xué)可能了解過)下面簡要說明各個數(shù)據(jù)區(qū)域。
2.2 堆區(qū)
堆區(qū)(Java Heap)最大的作用就是存放對象實(shí)例,但是并不是所有對象一定要存放到堆上,隨著JIT技術(shù)和逃逸分析技術(shù)的發(fā)展,對象可以存放到棧上。
堆區(qū)被所有線程共享。
堆區(qū)不一定要分配在物理連續(xù)的內(nèi)存中,只要其邏輯連續(xù)即可。
當(dāng)堆區(qū)沒有足夠內(nèi)存完成實(shí)例分配時,并且堆無法擴(kuò)展時,將會拋出OutOfMemoryError異常。
談到堆區(qū)時,就不得不提到垃圾回收技術(shù),堆區(qū)是垃圾回收器管理的主要區(qū)域,因此也被稱為GC堆。不同的垃圾回收算法會把堆區(qū)劃分為各個不同的區(qū)域,比如新生代和老年代,具體細(xì)節(jié)將在垃圾回收算法部分詳解。
2.3 方法區(qū)(靜態(tài)區(qū))
方法區(qū)(Method Area)用于存儲已被JVM加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。當(dāng)我們創(chuàng)建對象實(shí)例后,對象的類型信息存儲在方法堆之中,實(shí)例數(shù)據(jù)存放在堆中;實(shí)例數(shù)據(jù)指的是在 Java 中創(chuàng)建的各種實(shí)例對象以及它們的值,類型信息指的是定義在 Java 代碼中的常量、靜態(tài)變量、以及在類中聲明的各種方法、方法字段等等;同時可能包括即時編譯器編譯后產(chǎn)生的代碼數(shù)據(jù)。
方法區(qū)被所有線程共享。
注意HotSpot虛擬機(jī)在實(shí)現(xiàn)時,將方法區(qū)的實(shí)現(xiàn)在堆區(qū)的永久代實(shí)現(xiàn),因此有時候?qū)⒎椒▍^(qū)看作永久代的一部分。
2.3.1 運(yùn)行時常量池
用于存放編譯期生成的各種字面量和符號引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時常量池中存放。
2.4 Java虛擬機(jī)棧
Java虛擬機(jī)棧(Java Virtual Machine Stack)由若干個Java虛擬機(jī)棧幀(Stack Frame)組成,而棧幀是JVM方法調(diào)用和方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu),每一個方法的調(diào)用和返回都對應(yīng)著棧幀入棧出棧的過程。棧頂?shù)臈Q為當(dāng)前棧幀(Current Stack Frame),對應(yīng)當(dāng)前方法。
每一個棧幀主要由局部變量表,操作數(shù)棧,動態(tài)鏈接和返回地址和棧幀信息構(gòu)成。
2.4.1 局部變量表
局部變量表用于存放方法參數(shù)和局部變量,虛擬機(jī)通過索引定位的方式使用局部變量表。
變量槽(Variable Slot)是局部變量表的最小單位,沒有強(qiáng)制規(guī)定大小為 32 位,雖然32位足夠存放大部分類型的數(shù)據(jù)。一個 Slot 可以存放 boolean、byte、char、short、int、float、reference 和 returnAddress 8種類型。其中 reference 表示對一個對象實(shí)例的引用,通過它可以得到對象在Java 堆中存放的起始地址的索引和該數(shù)據(jù)所屬數(shù)據(jù)類型在方法區(qū)的類型信息。returnAddress 則指向了一條字節(jié)碼指令的地址。 對于64位的 long 和 double 變量而言,虛擬機(jī)會為其分配兩個連續(xù)的 Slot 空間。
2.4.2 操作數(shù)棧
方法執(zhí)行過程中,進(jìn)行算術(shù)運(yùn)算或者是調(diào)用其他的方法進(jìn)行參數(shù)傳遞的時候是通過操作數(shù)棧進(jìn)行的。
如果線程請求的棧深度大于虛擬機(jī)規(guī)定的最大深度,將會拋出StackOverflowError異常;如果虛擬機(jī)??梢詣討B(tài)擴(kuò)展,并且擴(kuò)展時無法申請到足夠的內(nèi)存,就會跑出OutOfMemoryError異常
2.4.3 動態(tài)鏈接
每個棧幀都包含一個執(zhí)行運(yùn)行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支持方法調(diào)用過程中的動態(tài)連接(Dynamic Linking)。
Class 文件中存放了大量的符號引用,字節(jié)碼中的方法調(diào)用指令就是以常量池中指向方法的符號引用作為參數(shù)。這些符號引用一部分會在類加載階段或第一次使用時轉(zhuǎn)化為直接引用,這種轉(zhuǎn)化稱為靜態(tài)解析。另一部分將在每一次運(yùn)行期間轉(zhuǎn)化為直接引用,這部分稱為動態(tài)連接。
2.4.4 方法返回地址
當(dāng)一個方法開始執(zhí)行以后,只有兩種方法可以退出當(dāng)前方法:
當(dāng)執(zhí)行遇到返回指令,會將返回值傳遞給上層的方法調(diào)用者,這種退出的方式稱為正常完成出口(Normal Method Invocation Completion),一般來說,調(diào)用者的PC計數(shù)器可以作為返回地址。
當(dāng)執(zhí)行遇到異常,并且當(dāng)前方法體內(nèi)沒有得到處理,就會導(dǎo)致方法退出,此時是沒有返回值的,稱為異常完成出口(Abrupt Method Invocation Completion),返回地址要通過異常處理器表來確定。
2.4.5 棧幀信息
虛擬機(jī)規(guī)范并沒有規(guī)定具體虛擬機(jī)實(shí)現(xiàn)包含什么附加信息,這部分的內(nèi)容完全取決于具體實(shí)現(xiàn)。在實(shí)際開發(fā)中,一般會把動態(tài)連接,方法返回地址和附加信息全部歸為一類,稱為棧幀信息。
2.5 程序計數(shù)器
每一個Java線程都擁有自己的程序計數(shù)器,他可以看作多當(dāng)前線程所執(zhí)行的字節(jié)碼行號指示器。字節(jié)碼解釋器通過改變程序計數(shù)器的值來選取下一條需要執(zhí)行的指令。分支、循環(huán)、跳轉(zhuǎn)、異常處理、線程恢復(fù)等基礎(chǔ)功能都需要依賴這個計數(shù)器完成。
2.6 直接內(nèi)存
直接內(nèi)存(Direct Memory)并不是虛擬機(jī)運(yùn)行時數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域,但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError 異常出現(xiàn),所以我們放到這里一起講解。
在JDK 1.4 中新加入了NIO(New Input/Output)類,引入了一種基于通道(Channel)與緩沖區(qū)(Buffer)的I/O 方式,它可以使用Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java 堆里面的DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景中顯著提高性能,因為避免了在Java 堆和Native 堆中來回復(fù)制數(shù)據(jù)。顯然,本機(jī)直接內(nèi)存的分配不會受到Java 堆大小的限制,但是,既然是內(nèi)存,則肯定還是會受到本機(jī)總內(nèi)存(包括RAM 及SWAP 區(qū)或者分頁文件)的大小及處理器尋址空間的限制。服務(wù)器管理員配置虛擬機(jī)參數(shù)時,一般會根據(jù)實(shí)際內(nèi)存設(shè)置-Xmx等參數(shù)信息,但經(jīng)常會忽略掉直接內(nèi)存,使得各個內(nèi)存區(qū)域的總和大于物理內(nèi)存限制(包括物理上的和操作系統(tǒng)級的限制),從而導(dǎo)致動態(tài)擴(kuò)展時出現(xiàn)OutOfMemoryError異常。
2.7 本地方法棧
本地方法棧與JVM棧發(fā)揮的作用是非常相似的,他們的區(qū)別是JVM棧為執(zhí)行Java方法服務(wù),本地方法棧為執(zhí)行Native方法服務(wù)。