Java虛擬機內存區域詳解

更多文章 訪問我的博客:http://www.caoyong.xin:8080/blogger


Java虛擬機內存區域詳解

半年前買了一本深入理解Java虛擬機,買了就放在那裏去了,期間拿出來想研究一下,還沒有看一會,哇 !腦袋疼。也就又放回原處,這段時間事情不多,自己也靜下心來,看看這本被譽爲佳作的書。


目錄結構

    1:Java虛擬機介紹

    2:內存區域介紹

        2.1:程序計數器

        2.2:Java虛擬機棧

        2.3:Java堆

        2.4:方法區

        2.5:本地方法棧

    3:對象的創建(轉載)


1:Java虛擬機介紹

    學習Java的人,都聽說過Java虛擬機,也叫JVM,估計也就停留在這裏的,(我也差不多,剛開始)。

    Java語言的誕生在1995年(我出生)Java發佈了第一個版本Java1.0,這個時候Java喊出了一句口號 Write Once,Run AnyWhere 一次編寫,隨處運行 而那個時候的Java虛擬機是 Sun Classic VM 。到2000年的時候Java1.3,就把HotSpot作爲了一直沿用至今的Java虛擬機。所以我們現在用大部分Java虛擬機都是HotSpot


2:內存區域介紹

        Java虛擬機在執行Java程序的時候會把內存分爲幾個數據區域,看下面的圖,介紹了Java虛擬機的運行時數據區的劃分

    QQ截圖20180412135323.png

    下面我們就來一一介紹這些數據區域

    2.1:程序計數器

    程序計數器是一塊較小的內存區域,在java的字節碼解析器當中,需要辨別當前的字節碼解析到了哪個地方,同時需要來控制程序的流程,如果在程序當中沒有一個東西來記錄當前程序執行到哪個,同時下一步應該執行哪一步操作例如:分支、循環、跳轉、異常處理等操作都不是按照原本程序書寫的順序來執行的,所以爲了能夠引導程序的運行,就需要引進一個用來引導字節碼解析順序的東西,就叫做程序計數器。

        Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的。如果一個線程運行一半,就被掛起,等待另一個線程執行完畢後在接着執行。爲了線程切換後可以正確的恢復到原來執行的位置,所以每個線程都應該有一個獨立的程序計數器,也就是說程序計數器這一塊內存區域是私有的。也就叫"線程私有"。

        還有一點,如果線程正在執行的是一個java方法,那麼計數器記錄的是正在執行的虛擬機字節碼指令地址。如果執行的native方法,計數器當中的內容應當是空。 還有此內存區域在java的虛擬機規範當中是唯一一個沒有規定OutOfMemoryError(內存溢出錯誤)的區域。

     2.2:Java虛擬機棧

    也叫Java棧,他也是線程私有的,Java虛擬機棧描述的是Java方法執行的內存模型。每個方法在執行的同時都會在Java虛擬機棧中創建一個棧幀,用於存儲局部變量表,操作數棧,動態鏈接,方法出口等信息。每一個方發從調入到執行完畢,也就對應這棧幀進棧到出棧的過程。

      局部變量表存放着編譯期可知的各種基本數據類型和對象的引用,我們通常說的Java棧存放對象引用,Java堆存放對象實例,現在應該明白了具體存放哪裏了。

QQ截圖20180412145340.png

Java虛擬機棧有兩種異常狀況

    第一種 線程請求的棧深度大於最大可用深度,則拋出stackOverflowError;

    第二種棧是可動態擴展的,但沒有內存空間支持擴展,則拋出OutofMemoryError。

   2.3:Java堆

    Java堆是Java虛擬機所管理的最大的一塊內存區域,Java堆是線程共享的一塊區域,當然Java堆也是垃圾收集器管理的主要區域,可以分爲新生代和老年代(tenured)。新生代用於存放剛創建的對象以及年輕的對象,如果對象一直沒有被回收,生存得足夠長,老年對象就會被移入老年代。新生代又可進一步細分爲eden、survivorSpace0(s0,from space)、survivorSpace1(s1,to space)。剛創建的對象都放入eden,s0和s1都至少經過一次GC並倖存。如果倖存對象經過一定時間仍存在,則進入老年代(tenured)。

QQ截圖20180412150950.png


2.4:方法區

        方法區也是各個線程共享的內存區域,它 用於存儲已被虛擬機加載的類信息,常量,靜態變量,及時編譯器編譯後的代碼數據。

       方法區中有三個池(jdk1.6之前),

        QQ截圖20180412152907.png

  • 常量池(Constant Pool):常量池數據編譯期被確定,是Class文件中的一部分。存儲了類、方法、接口等中的常量,當然也包括字符串常量。

  • 字符串池/字符串常量池(String Pool/String Constant Pool):是常量池中的一部分,存儲編譯期類中產生的字符串類型數據。

  • 運行時常量池(Runtime Constant Pool):方法區的一部分,所有線程共享。虛擬機加載Class後把常量池中的數據放入到運行時常量池。


  1. 常量池:可以理解爲Class文件之中的資源倉庫,它是Class文件結構中與其他項目資源關聯最多的數據類型。

  2. 常量池中主要存放兩大類常量:字面量(Literal)和符號引用(Symbolic Reference)。

  3. 字面量:文本字符串、聲明爲final的常量值等;

  4. 符號引用:類和接口的完全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符

      JDK1.6之前字符串常量池位於方法區之中。 
      JDK1.7字符串常量池已經被挪到堆之中。

2.5:本地方法棧

和虛擬機棧功能相似,但管理的不是JAVA方法,是本地方法,本地方法是用C實現的。Java底層會調用C編寫的的類庫中的方法,在Java中調用本地方法使用native關鍵詞。而本地方法棧就是管理這些本地方法的。


下面這一部分是對象在Java虛擬機創建的一系列過程,看到有位博主寫了篇關於這部分的內容,所以就轉載一下。寫的很具體

轉載:https://blog.csdn.net/sc313121000/article/details/50819741

一、對象的創建

new Animal();

1.類加載檢查:

檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類的加載過程。

2.爲對象分配內存

對象所需內存的大小在類加載完成後便完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。

2.1根據Java堆中是否規整有兩種內存的分配方式:

(Java堆是否規整由所採用的垃圾收集器是否帶有壓縮整理功能決定)

指針碰撞(Bump the pointer):

Java堆中的內存是規整的,所有用過的內存都放在一邊,空閒的內存放在另一邊,中間放着一個指針作爲分界點的指示器,分配內存也就是把指針向空閒空間那邊移動一段與內存大小相等的距離。例如:Serial、ParNew等收集器。

空閒列表(Free List):

Java堆中的內存不是規整的,已使用的內存和空閒的內存相互交錯,就沒有辦法簡單的進行指針碰撞了。虛擬機必須維護一張列表,記錄哪些內存塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給對象實例,並更新列表上的記錄。例如:CMS這種基於Mark-Sweep算法的收集器。

2.2分配內存時解決併發問題的兩種方案:

對象創建在虛擬機中時非常頻繁的行爲,即使是僅僅修改一個指針指向的位置,在併發情況下也並不是線程安全的,可能出現正在給對象A分配內存,指針還沒來得及修改,對象B又同時使用了原來的指針來分配內存的情況。

對分配內存空間的動作進行同步處理—實際上虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性; 
把內存分配的動作按照線程劃分爲在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB)。哪個線程要分配內存,就在哪個線程的TLAB上分配。只有TLAB用完並分配新的TLAB時,才需要同步鎖定。

3.內存空間初始化

虛擬機將分配到的內存空間都初始化爲零值(不包括對象頭),如果使用了TLAB,這一工作過程也可以提前至TLAB分配時進行。 
內存空間初始化保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

4.對象設置

虛擬機對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息存放在對象的對象頭之中。

5.init

在上面的工作都完成之後,從虛擬機的角度看,一個新的對象已經產生了。 
但是從Java程序的角度看,對象的創建纔剛剛開始方法還沒有執行,所有的字段都還是零。 
所以,一般來說(由字節碼中是否跟隨invokespecial指令所決定),執行new指令之後會接着執行init方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算產生出來。

二、對象的內存佈局

在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

1.對象頭:

HotSpot虛擬機的對象頭包括兩部分信息。

1.1 第一部分用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。

HotSpot虛擬機對象頭Mark Word

1.2 另外一個部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

如果對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java對象的元數據信息確定Java對象的大小,但是從數組的元數據中無法確定數組的大小。 
(並不是所有的虛擬機實現都必須在對象數據上保留類型指針,換句話說,查找對象的元數據並不一定要經過對象本身,可參考 三對象的訪問定位) 
QQ截圖20180412155353.png

2.實例數據:

實例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類 
中繼承下來的,還是在子類中定義的,都需要記錄下來。 
HotSpot虛擬機默認的分配策略爲longs/doubles、ints、shorts/chars、bytes/booleans、oop,從分配策略中可以看出,相同寬度的字段總是分配到一起。

3.對齊填充:

對齊填充並不是必然存在的,也沒有特定的含義,僅僅起着佔位符的作用。 
由於HotSpot虛擬機的自動內存管理系統要求對象的起始地址必須是8字節的整數倍,也就是對象的大小必須是8字節的整數倍。而對象頭部分正好是8字節的倍數(1倍或者2倍),因此,當對象實例數據部分沒有對齊的時候,就需要通過對齊填充來補全。

三、對象的訪問定位

建立對象是爲了使用對象,我們的Java程序需要通過棧上的引用數據來操作堆上的具體對象。 
對象的訪問方式取決於虛擬機實現,目前主流的訪問方式有使用句柄和直接指針兩種。

使用句柄: 
如果使用句柄的話,那麼Java堆中將會劃分出一塊內存來作爲句柄池,引用中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

QQ截圖20180412155403.png


通過句柄訪問對象

優勢:引用中存儲的是穩定的句柄地址,在對象被移動(垃圾收集時移動對象是非常普遍的行爲)時只會改變句柄中的實例數據指針,而引用本身不需要修改。

直接指針: 
如果使用直接指針訪問,那麼Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而引用中存儲的直接就是對象地址。 

QQ截圖20180412155411.png

通過直接指針訪問對象

優勢:速度更快,節省了一次指針定位的時間開銷。由於對象的訪問在Java中非常頻繁,因此這類開銷積少成多後也是非常可觀的執行成本。(例如HotSpot)

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