深刻理解Java虛擬機:第2章 Java內存區域與內存溢出異常

 

2.2 運行時數據區域

Java堆

線程共享。程序員

GC堆,存放對象實例和數組(對象數組、基本類型數組)。算法

若是在堆中沒有內存完成實例分配,而且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。數組

現代的垃圾收集器基本都是採用分代收集算法,其主要的思想是針對不一樣類型的對象採起不一樣的垃圾回收算法,能夠將堆分紅兩塊:oop

  1. 新生代(Young Generation)主要是用來存放新生的對象。
  2. 老年代(Old Generation)主要存放應用程序中生命週期長的內存對象。

 

方法區

線程共享。佈局

存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼。線程

JDK 1.8 以前,HotSpot 虛擬機把它當成永久代來進行垃圾回收。可是很難肯定永久代的大小,由於它受到不少因素影響,而且每次 Full GC 以後永久代的大小都會改變,因此常常會拋出 OutOfMemoryError 異常。設計

爲了更容易管理方法區,從 JDK 1.8 開始,移除永久代,並把方法區移至元空間,它位於本地內存中,而不是虛擬機內存中。指針

注:HotSpot虛擬機的設計團隊選擇把GC分代收集擴展至方法區,或者說使用永久代來實現方法區(Non-Heap)。對象

 

運行時常量池(Runtime Constant Pool

Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量(Literal,如文本字符串、 聲明爲final的常量值等)和符號引用(Symbolic References,類和接口的全限定名(Fully Qualified Name)、字段的名稱和描述符(Descriptor)、方法的名稱和描述符)[2],這部份內容將在類加載後進入方法區的運行時常量池中存放。

  • 常量池與運行時常量池的區別?
  1. Java虛擬機對Class文件每一部分(天然也包括常量池)的格式都有嚴格規定,每個字節用於存儲哪一種數據都必須符合規範上的要求才會被虛擬機承認、裝載和執行,但對於運行時常量池,Java虛擬機規範沒有作任何細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的須要來實現這個內存區域。
  2. 運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,Java語言並不要求常量必定只有編譯期才能產生,也就是並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的即是String類的intern()方法。

 

虛擬機棧

線程私有。

Java方法執行的內存模型:每一個 Java 方法在執行的同時會建立一個棧幀用於存儲局部變量表、操做數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。局部變量表存放了編譯期可知的基本數據類型、對象引用(reference類型)、returnAddress類型。

StackOverflowError異常(若是線程請求的棧深度大於虛擬機所容許的深度),OutOfMemoryError異常(若是虛擬機棧能夠動態擴展, 擴展時沒法申請到足夠的內存)。

 

本地方法棧

線程私有。

與虛擬機棧做用相似,爲虛擬機使用到的native方法服務。

StackOverflowErrorOutOfMemoryError異常。

 

程序計數器

線程私有。

當前線程所執行的字節碼的行號指示器。若是線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;若是正在執行的是Native方法,這個計數器值則爲空(Undefined)。

 

2.3 HotSpot虛擬機對象探祕

對象的建立

  1. 虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,而且檢查這個符號引用表明的類是否已被加載、 解析和初始化過。 若是沒有,那必須先執行相應的類加載過程。
  2. 在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。 對象所需內存的大小在類加載完成後即可徹底肯定。分配方式有「指針碰撞」(Serial、 ParNew)和「空閒列表」(CMS)。
  3. 內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值(不包括對象頭),若是使用TLAB,這一工做過程也能夠提早至TLAB分配時進行。 這一步操做保證了對象的實例字段在Java代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。
  4. 接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪一個類的實例、 如何才能找到類的元數據信息、 對象的哈希碼、 對象的GC分代年齡等信息。 這些信息存放在對象的對象頭(Object Header)之中。根據虛擬機當前的運行狀態的不一樣,如是否啓用偏向鎖等,對象頭會有不一樣的設置方式。
  5. 從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象建立纔剛剛開始,執行new指令以後會接着執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

 

對象的內存佈局

1) 對象頭(Header)
Mark Word:用於存儲對象自身的運行時數據,如哈希碼(HashCode)、 GC分代年齡、 鎖狀態標誌、 線程持有的鎖、 偏向線程ID、 偏向時間戳等.
類型指針:即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例。若是對象是一個Java數組,那在對象頭中還必須有一塊用於記錄數組長度的數據。
2) 實例數據(Instance Data)
對象真正存儲的有效信息,也是在程序代碼中所定義的各類類型的字段內容。存儲順序會受到虛擬機分配策略(longs/doubles、 ints、 shorts/chars、bytes/booleans、 oops)參數和字段在Java源碼中定義順序的影響。

  1. 對齊填充(Padding)
    僅僅起着佔位符的做用。HotSpot VM的自動內存管理系統要求對象起始地址必須是8字節的整數倍,而對象頭部分正好是8字節的倍數(1倍或者2倍)。

 

對象的訪問定位

Java程序須要經過棧上的reference數據來操做堆上的具體對象。對象訪問方式有「使用句柄」和「直接指針」。
1) 使用句柄

  • Java堆中將會劃分出一塊內存來做爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。
  • 優勢:對象被移動時(垃圾收集時很廣泛)只會改變句柄中的實例數據指針,而reference自己不須要修改。
    2) 直接指針(Sun HotSpot默認)
  • Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息,而reference中存儲的直接就是對象地址。
  • 優勢:速度快,節省了一次指針定位的時間開銷。
相關文章
相關標籤/搜索