java程序運行時jvm內存分配

概述

衆所周知,在內存管理方面,對於從事C,C++的開發人員來說,他們是內存管理方面的“上帝”,負責着每一個對象生命開始到結束,這樣一定程度上給程序員增加了很多麻煩(每個new操作都要寫相應的delete/free代碼),而對於java程序員來說,虛擬機提供內存管理機制,不容易出現內存泄漏和內存溢出問題(但是帶了的缺點就是一旦出現問題,如果不瞭解虛擬機內存分配將很難定位錯誤)。接下來說說程序運行時jvm內存的各個區域劃分以及這些區域的作用、服務對象和可能產生的問題。

運行時數據區域

jvm在執行程序時會把它所管理的內存劃分爲若干不同數據區域。這些區域有各自的用途、創建時間和銷燬時間。這幾個區域分別爲:程序計數器、虛擬機棧、本地方法棧、方法區和堆。
如圖:
jvm內存分配
其中淡黃色區域爲線程共享的,橘黃色區域爲線程私有的。
那麼這些區域的作用是什麼呢?

1. 程序計數器

程序計數器(Program Counter Register)是一塊很小的內存空間,它是當前線程所執行的字節碼的行號指示器“指哪打哪”)。
jvm的多線程是通過線程輪流切換並分配處理器執行時間(時間片)的方式來實現的,在任何一個時刻,一個處理器(對於多核處理器來說是一個內核)都會執行一條線程中的指令,爲了確保線程切換後能恢復到正確的執行位置,每個線程都有一個獨立的程序計數器,各個線程的程序計數器獨立存儲,它們是線程私有的內存區域
如果線程正在執行一個java方法,這個計數器記錄的就是正在執行的虛擬機字節碼指令的地址;如果執行的是Native方法,這個計數器的值爲Undefined。此內存區域是唯一一個在java虛擬機規範中沒有規定任何OutOfMemroryError情況的區域。

2. 虛擬機棧

虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,他的生命週期於線程相同。虛擬機棧描述的是java方法執行的內存模型:每個方法在執行的同時會在虛擬機棧中創建一個棧幀(Stack Frame),棧幀用於存儲局部變量、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的操作就對應一個棧幀在虛擬機中入棧到出棧的過程。
說到棧幀,有人會疑惑,棧幀和虛擬機棧是什麼關係呢?

每個線程對應有一個虛擬機棧,在這個線程中每個函數(方法)被調用時分別從這個棧佔用一段區域,稱爲幀。簡而言之,虛擬機棧是相對於某個線程而言的,棧幀則是相對於某個函數(方法)而言的。虛擬機棧中包含棧幀,每個棧幀對應於一個未完成運行的函數。

局部變量表存放了編譯期可知的各種基本數據類型(boolean,byte,char,short,int,float,long,double)、對象引用(reference)和returnAddress類型。其中64位長度的long和double類型數據會佔用兩個局部變量空間(Slot),其餘數據類型佔用一個。局部變量表所需要的內存空間在編譯期完成分配,方法運行時不會改變局部變量表的大小。關於棧幀結構的詳細說明大家可以參考http://www.360doc.com/content/14/0925/13/1073512_412236522.shtml
如果線程請求的棧深度大於虛擬機所允許的深度,則會拋出StackOverflowError異常;如果虛擬機棧可以動態擴展,在擴展時無法申請到足夠的內存時,就會拋出OutOfMemoryError異常。

3. 本地方法棧

本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用類似,區別就是虛擬機棧爲虛擬機執行java方法(即字節碼)服務,而本地方法棧則爲虛擬機使到的native方法服務。有趣的是,我們常用的HotSpot虛擬機直接把本地方法棧和虛擬機棧合二爲一。與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError。

4. 堆

java堆(Java Heap)是jvm管理的內存中最大的一塊。java堆是所有線程共享的一塊內存區域,在虛擬機啓動時創建,用於存放對象實例,所有的對象實例和數組都要在堆上分配。(但是隨着JIT編譯器的發展與逃逸分枝技術的成熟,棧上分配、標量替換優化技術使對象分配不是那麼“絕對”了)。
java堆是GC管理的主要區域,因此很多時候我們也叫其爲GC堆(Garbage Collected Heap)。從內存回收角度來看,現在收集器基本都採用分代收集算法,因此java堆中還可細分爲:新生代和老年代;從內存分配角度來看,線程共享的java堆中可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer)。但是,無論什麼情況,存儲的都是對象實例。進一步劃分只是爲了更好的回收內存。
java堆可以處於物理上不連續的內存空間中,邏輯上連續即可(使用邏輯地址很容易實現)。如果堆中沒有內存完成實例分配,而且堆也無法拓展時(主流虛擬機都是按照可拓展來實現),將會拋出OutOfMemoryError異常。

5. 方法區

方法區(Method Area)與java堆一樣,是線程共享的內存區域,方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。相對於堆來說,垃圾收集行爲在這個區域比較少,這個區域的內存回收目標主要是針對常量池的回收和類型的卸載(卸載條件非常苛刻)。那麼常量池是什麼呢?

Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項就是常量池,常量池這個概念是針對Java class文件而言的。當java代碼被編譯成字節碼(class)文件時,會將字面量(文本字符串,聲明爲final的常量值)、符號引用存放在class文件的常量池中。這部分內容將在類加載後進入方法區的運行時常量池中存放。
運行時常量池(Runtime Constant Pool)是方法區的一部分。運行時常量池就是class文件被加載(load)到JVM之後,常量池存放的內存區,該內存區屬於Heap區。
運行時常量池相對於class文件常量池的另外一個特徵是具備動態性,java語言並不要求常量一定只有在編譯期才能產生,也就是說並非預置入calss文件常量池的內容才能進入方法區運行時常量池,運行期間也可以將新的常量放入池中。如String的intern()方法。

如下面的代碼,當string對象s調用intern()時,如果運行時常量池中有s的值,則直接返回該字符串在運行時常量池的地址;如果不存在s的值,則將s的地址(引用)存放在運行時常量池,並返回s的地址(引用)。

public class Test(){
    public static void main(args[]){
    String s = new String("123");
    s.intern();
    }
}

當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

補充:直接內存

直接內存(Direct Memory)並不是虛擬機運行時數據區的一部分,但這部分內存也被頻繁使用,也可能會導致OutOfMemoryError異常。
在JDK1.4中加入了NIO類,引入了基於通道(Channal)與緩衝區(Buffer)的I/O方式,它可以使用Native函數庫直接分配堆外內存,通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,這樣避免了在java堆和Native堆中來回複製數據,提高了性能。
直接內存的分配不會受到java堆大小的限制(受本機總內存制約)。在配置虛擬機參數時,應考慮到直接內存的使用,防止內存區域總和大於物理內存限制,從而防止OutOfMemoryError異常。

本文介紹了java程序運行時jvm內存分配的情況,參考《深入理解java虛擬機》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章