JVM內存模型解析

摘要:

Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域,這些數據區域都有各自的用途,以及創建和銷燬的時間,並且它們可以分爲兩種類型:線程共享的方法區和堆,線程私有的虛擬機棧、本地方法棧和程序計數器。

 

一、java內存模型

Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域,這些數據區域可以分爲兩個部分:一部分是線程共享的,一部分則是線程私有的。其中,線程共享的數據區包括方法區和堆,線程私有的數據區包括虛擬機棧、本地方法棧和程序計數器。如下圖所示:

 

(一)線程私有的區域:程序計數器、虛擬機棧和本地方法棧三個區域,內涵分別爲:

 

(1)程序計數器保證線程正常切換):

線程是CPU調度的基本單位。在多線程情況下,當線程數超過CPU數量或CPU內核數量時,線程之間就要根據時間片輪詢搶奪CPU時間資源。也就是說,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能夠恢復到正確的執行位置,每條線程都需要一個獨立的程序計數器去記錄其正在執行的字節碼指令地址。

因此,程序計數器是線程私有的一塊較小的內存空間,其可以看做是當前線程所執行的字節碼的行號指示器。如果線程正在執行的是一個 Java 方法,計數器記錄的是正在執行的字節碼指令的地址;如果正在執行的是 Native 方法,則計數器的值爲空。

 程序計數器是唯一一個沒有規定任何 OutOfMemoryError 的區域。

 

(2)虛擬機棧JAVA執行的內存模型)

虛擬機棧描述的是Java方法執行的內存模型,是線程私有的。每個方法在執行的時候都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息,而且 每個方法從調用直至完成的過程,對應一個棧幀在虛擬機棧中入棧到出棧的過程。其中,局部變量表主要存放一些基本類型的變量(int, short, long, byte, float, double, boolean, char)和 對象句柄,它們可以是方法參數,也可以是方法的局部變量。

 

虛擬機棧有兩種異常情況:StackOverflowError 和 OutOfMemoryError。我們知道,一個線程擁有一個自己的棧,這個棧的大小決定了方法調用的可達深度(遞歸多少層次,或嵌套調用多少層其他方法,-Xss 參數可以設置虛擬機棧大小),若線程請求的棧深度大於虛擬機允許的深度,則拋出 StackOverFlowError 異常。此外,棧的大小可以是固定的,也可以是動態擴展的,若虛擬機可以動態擴展(大多數虛擬機都可以),但擴展時無法申請到足夠的內存(比如沒有足夠的內存爲一個新創建的線程分配棧空間時),則拋出 OutofMemoryError 異常。下圖爲棧幀結構圖:

 

(3)本地方法棧(JVM棧非常相似,JVM棧執行java方法,本地執行native 方法)

本地方法棧與Java虛擬機棧非常相似,也是線程私有的,區別是虛擬機棧爲虛擬機執行 Java 方法服務,而本地方法棧爲虛擬機執行 Native 方法服務。與虛擬機棧一樣,本地方法棧區域也會拋出 StackOverflowError 和 OutOfMemoryError 異常

(二)線程共享區

線程共享區分爲JAVA堆和方法區兩個區域,內涵分別爲:

(1) java堆(存放對象實例)

Java 堆的唯一目的就是存放對象實例,幾乎所有的對象實例(和數組)都在這裏分配內存。Java堆是線程共享的,類的對象從中分配空間,這些對象通過new、newarray、 anewarray 和 multianewarray 等指令建立,它們不需要程序代碼來顯式的釋放。

由於Java堆唯一目的就是用來存放對象實例,因此其也是垃圾收集器管理的主要區域,故也稱爲稱爲 GC堆從內存回收的角度看,由於現在的垃圾收集器基本都採用分代收集算法所以爲了方便垃圾回收Java堆還可以分爲 新生代  老年代 新生代用於存放剛創建的對象以及年輕的對象,如果對象一直沒有被回收,生存得足夠長,對象就會被移入老年代。新生代又可進一步細分爲 eden、survivorSpace0 和 survivorSpace1。剛創建的對象都放入 eden,s0 和 s1 都至少經過一次GC並倖存。如果倖存對象經過一定時間仍存在,則進入老年代。

 

注意,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。而且,Java堆在實現時,既可以是固定大小的,也可以是可拓展的,並且主流虛擬機都是按可擴展來實現的(通過-Xmx(最大堆容量) 和 -Xms(最小堆容量)控制)。如果在堆中沒有內存完成實例分配,並且堆也無法再拓展時,將會拋出 OutOfMemoryError 異常。

 

TLAB (Thread Local Allocation Buffer,線程私有分配緩衝區)

Sun Hotspot JVM 爲了提升對象內存分配的效率,對於所創建的線程都會分配一塊獨立的空間 TLAB,其大小由JVM根據運行的情況計算而得。在TLAB上分配對象時不需要加鎖(相對於CAS配上失敗重試方式 ),因此JVM在給線程的對象分配內存時會盡量的在TLAB上分配,在這種情況下JVM中分配對象內存的性能和C基本是一樣高效的,但如果對象過大的話則仍然是直接使用堆空間分配。

(2)方法區

方法區與Java堆一樣,也是線程共享的並且不需要連續的內存,其用於存儲已被虛擬機加載的 類信息常量靜態變量即時編譯器編譯後的代碼等數據方法區通常和永久區(Perm)關聯在一起,但永久代與方法區不是一個概念,只是有的虛擬機用永久代來實現方法區,這樣就可以用永久代GC來管理方法區,省去專門內存管理的工作。根據Java虛擬機規範的規定,當方法區無法滿足內存分配的需求時,將拋出 OutOfMemoryError 異常。

運行時常量池(Runtime Constant Pool)是方法區的一部分,用於存放編譯期生成的各種 字面量  符號引用。其中,字面量比較接近Java語言層次的常量概念,如文本字符串、被聲明爲final的常量值等;而符號引用則屬於編譯原理方面的概念,包括以下三類常量:類和接口的全限定名字段的名稱和描述符  方法的名稱和描述符。因爲運行時常量池(Runtime Constant Pool)是方法區的一部分,那麼當常量池無法再申請到內存時也會拋出 OutOfMemoryError 異常。

運行時常量池相對於Class文件常量池的一個重要特徵是具備動態性。Java語言並不要求常量一定只有編譯期才能產生,運行期間也可能將新的常量放入池中,比如字符串的手動入池方法intern()。

 

3)Java堆 與 方法區的區別

Java堆是 Java代碼可及的內存,是留給開發人員使用的;而非堆(Non-Heap)是JVM留給自己用的,所以方法區、JVM內部處理或優化所需的內存 (JIT編譯後的代碼緩存)、每個類結構 (如運行時常量池、字段和方法數據)以及方法和構造方法的代碼都在非堆內存中。

4)、方法區的回收

方法區的內存回收目標主要是針對 常量池的回收  對類型的卸載。回收廢棄常量與回收Java堆中的對象非常類似。以常量池中字面量的回收爲例,假如一個字符串“abc”已經進入了常量池中,但是當前系統沒有任何一個String對象是叫做“abc”的,換句話說是沒有任何String對象引用常量池中的“abc”常量,也沒有其他地方引用了這個字面量,如果在這時候發生內存回收,而且必要的話,這個“abc”常量就會被系統“請”出常量池。常量池中的其他類(接口)、方法、字段的符號引用也與此類似。

判定一個常量是否是廢棄常量比較簡單,而要判定一個類是否是無用的類的條件則相對苛刻許多。類需要同時滿足下面3個條件才能算是無用的類

1該類所有的實例都已經被回收,也就是Java堆中不存在該類的任何實例

2加載該類的ClassLoader已經被回收;

3該類對應的 java.lang.Class 對象沒有在任何地方被引用,無法在任何地方通過反射訪問該類的方法。

      參考博客:           http://blog.csdn.net/justloveyou_/article/details/71189093

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