天天看點

JVM中運作時資料區域(記憶體結構)

Java虛拟機所管理的記憶體通常包括以下幾個運作時資料區域,如圖所示:

JVM中運作時資料區域(記憶體結構)

1. 程式計數器

程式計數器是一塊較小的記憶體空間,可看做是目前線程所執行的位元組碼的行号訓示器。在虛拟機的概念模型裡,位元組碼解釋器工作時就是通過改變這個計數器的值來選擇下一條需要執行的位元組碼指令。分支、循環、跳轉、異常處理、線程恢複等基礎功能都需要依賴這個計數器來完成。

因為Java裡面是支援多線程的,是以要求每一個線程都有一個自己的程式計數器,以便線程切換後能恢複到正确的執行位置。各線程之間互不影響,獨立存儲,我們稱這類記憶體區域為“線程私有”的記憶體。

此記憶體區域沒有規定任何OutOfMemoryError情況的區域。

2. 虛拟機棧

虛拟機棧也是線程私有的,它的生命周期與線程相同。虛拟機棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時會建立一個棧幀用于存儲局部變量表、操作數棧、動态連結、方法出口等資訊。我們隻需要知道每個線程都會有一個虛拟機棧,然後線程裡的多個方法就對應棧裡面的棧幀(棧存儲元素的基本機關),每個方法從調用到執行完成對應一個棧幀在虛拟機棧中入棧到出棧的過程。

通常有人把Java記憶體分為堆記憶體和棧記憶體,其實這兩塊記憶體隻是與一些對象的記憶體配置設定有關,比較粗糙。比如通常Java程式中的所有對象均是在堆中配置設定的,它是一個線程共享的區域,如上圖所示;而棧則是我們所說的虛拟機棧,通常用來供方法内的局部變量進行配置設定,也就是上面提到的棧幀中的局部變量表。

局部變量表存放了編譯器可知的各種基本資料類型(boolean、byte、char、short、int、float、long、double)、對象引用(reference類型,它不等同于對象本身,可能是一個指向對象起始位址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象相關的位置)和returnAddress類型(指向了一條位元組碼指令的位址)。

Slot(局部變量空間)是局部變量表的基本機關(通常為32位),64位的long和double類型資料會占用2個局部變量空間,其餘的資料類型隻占用1個。局部變量表所需的記憶體空間在編譯期間完成配置設定,當進入一個方法時,這個方法需要在棧幀中配置設定多大的局部變量空間是完全确定的,在方法運作期間不會改變局部變量表的大小。

此記憶體區域規定了兩種異常狀況:若線程請求的棧深度大于虛拟機所允許的深度,将抛出StackOverflowError異常(一個方法可能遞歸請求了大量的方法且還未傳回,使得請求的方法個數大于棧的深度,導緻棧内放不了那麼多的棧幀);如虛拟機可以動态擴充,且擴充時無法申請到足夠記憶體,則會抛出OutOfMemoryError異常。

3. 本地方法棧

與虛拟機棧其實是類似的,隻是虛拟機棧為虛拟機執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛拟機使用到的Native方法服務。與虛拟機棧一樣,本地方法棧區域也會抛出StackOverflowError和OutOfMemoryError異常。

4. Java堆

Java 堆算是Java虛拟機所管理的記憶體中最大的一塊。Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立,此記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在這裡配置設定記憶體。Java虛拟機規範中描述的是:所有的對象執行個體以及數組都要在堆上配置設定,當然随着技術發展,這個也就不是那麼絕對了。

Java堆是垃圾收集器管理的主要區域,是以很多時候也被稱為“GC堆”。因為很多對象都是在這裡存儲的,也就需要在這裡進行回收。從記憶體回收的角度看,由于現在收集器基本都采用分代收集算法,是以Java堆中還可以細分為:新生代和老年代,這個在後面文章中會講到,再詳細一些還有Eden空間、From Survivor空間、To Survivor空間等。從記憶體配置設定的角度看,線程共享的Java堆中可能劃分出多個線程私有的配置設定緩沖區(TLAB)。不過無論如何劃分,都與存放内容無關,無論哪個區域,存儲的都仍然是對象執行個體,進一步劃分的目的是為了更好地回收記憶體,或者更快地配置設定記憶體。

Java虛拟機規範規定,Java堆可以處于實體上不連續的記憶體空間中,隻要邏輯上是連續的即可。

5. 方法區

方法區也是線程共享的,它用于存儲已被虛拟機加載的類資訊、常量、靜态變量、即時編譯器編譯後的代碼等資料。要注意“方法區”和“永久代”并不是一個東西,僅僅是因為HotSpot虛拟機的設計團隊選擇把GC分代收集擴充至方法區,或者說使用永久代來實作方法區而已,這樣HotSpot的垃圾收集器就可以像管理Java堆一樣管理這部分記憶體。

其實垃圾收集行為在這個區域是比較少出現的,這區域的記憶體回收目标主要是針對常量池的回收和對類型的解除安裝。而且回收成績難以讓人滿意,尤其是類型的解除安裝條件相當苛刻,但這部分區域的回收确實有必要。此區域有可能會抛出OutOfMemoryError異常。java對象的“死活”中描述了一些關于方法區回收的細節。

6. 運作時常量池

運作時常量池是方法區的一部分。Class檔案中會有一項資訊是常量池,用于存放編譯期生成的各種字面量和符号引用,這部分内容将在類加載後進入方法區的運作時常量池中存放。

Java虛拟機規範沒有對運作時常量池做任何細節的要求,但一般來說,除了儲存Class檔案中描述的符号引用外,還會把翻譯出來的直接引用也存儲在運作時常量池中。

運作時常量池相對于Class檔案常量池的另外一個重要特征就是具備動态性,Java語言并不要求常量一定隻有編譯器才能産生,也就是并非預置入Class檔案中常量池的内容才能進入方法區運作時常量池,運作期間也可将新的常量池放入池中,這種特性被開發人員利用的比較多的是String類的intern()方法。

7. 直接記憶體

直接記憶體并不是虛拟機運作時資料區的一部分,也不是Java虛拟機規範中定義的記憶體區域。但是這部分記憶體也會被頻繁的使用。在JDK1.4中新加入了NIO(New Input/Output)類,引入了一種基于通道與緩沖區的I/O方式,它可以使用Native函數庫直接配置設定堆外記憶體,然後通過一個存儲在Java堆中的DirectByteBuffer對象作為這塊記憶體的引用進行操作,這樣避免了在Java堆和Native堆中來回複制資料。