本系列專題的第二個板塊「理解JVM」是對周志明老師的《深刻理解Java虛擬機》著做的學習和擴展,也是在春招過程當中發現本身Java基礎的不足,特地精選了幾個重要知識點進行總結。如今先從很是重要的內存管理開始吧~java
本篇將瞭解JVM內存是如何劃分的,以及每一個區域的具體內容。
- 概述
- JVM內存區域劃分
- 操做系統內存與JVM內存
- HotSpot虛擬機內存對象探祕
1.概述算法
Java與C++之間有一堵由內存動態分配和垃圾回收機制所圍成的高牆,牆外面的人想進去,牆裏面的人出不來。數組
必要性:雖然JVM有自動內存管理機制,不須要人爲地給每個new操做寫配對的delete/free代碼,不容易出現內存泄漏和內存溢出問題。然而一旦出現內存泄漏和溢出方面的問題,若是不清楚JVM內存的內存管理機制,那麼將很難定位與解決問題。安全
2.JVM內存區域劃分佈局
JVM執行Java程序的過程:Java源代碼文件(.java)會被Java編譯器編譯爲字節碼文件(.class),而後由JVM中的類加載器加載各個類的字節碼文件,加載完畢以後,交由JVM執行引擎執行。學習
在上述過程當中,JVM會用一段空間來存儲執行程序期間須要用到的數據和相關信息,這段空間就是運行時數據區(Runtime Data Area),也就是常說的JVM內存。JVM會將它所管理的內存劃分爲若干個不一樣的數據區域,劃分結果如圖:操作系統
可見,運行時數據區被分爲線程私有數據區和線程共享數據區兩大類:線程
- 線程私有數據區包含:程序計數器、虛擬機棧、本地方法棧
- 線程共享數據區包含:Java堆、方法區(內部包含常量池)
接下來分別介紹:指針
a.程序計數器(Program Counter Register)code
- 是當前線程所執行的字節碼的行號指示器。
- 若是線程正在執行的是一個Java方法,那麼計數器記錄的是正在執行的虛擬機字節碼指令的地址;
- 若是線程正在執行的是一個Native方法,那麼計數器的值則爲空。
字節碼解釋器工做時,就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
- 爲了線程切換後能恢復到正確的執行位置,每條線程都須要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,所以它是線程私有的內存。
- 在Java虛擬機規範中,是惟一一個沒有規定任何OutOfMemoryError狀況的區域。
b.Java虛擬機棧(Java Virtual Machine Stacks)
- 是Java方法執行的內存模型。
- 每一個方法在執行的同時都會建立一個棧幀,用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。
- 每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。
局部變量表存放了編譯期可知的各類基本數據類型、對象引用類型和returnAddress類型,它所需的內存空間在編譯期間完成分配。
- 是線程私有的內存,與線程生命週期相同。
- 通常把Java內存區分爲堆內存(Heap)和棧內存(Stack),其中『棧』指的是虛擬機棧,『堆』指的是Java堆。
- 在Java虛擬機規範中,對這個區域規定了兩種異常情況:
- 若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;
- 若是虛擬機棧可動態擴展且擴展時沒法申請到足夠的內存,將拋出OutOfMemoryError異常。
c.本地方法棧(Native Method Stack)
- 是虛擬機使用到的Native方法服務。
- 在虛擬機規範中,對這個區域無強制規定,由具體的虛擬機自由實現。與虛擬機棧同樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
d.Java堆(Java Heap)
- 用於存放幾乎全部的對象實例和數組。
- 被全部線程共享的一塊內存區域,在虛擬機啓動時建立。
在Java堆中,可能劃分出多個線程私有的分配緩衝區(Thread Local Allocation Buffer,TLAB),但不管哪一個區域,存儲的都仍然是對象實例,進一步劃分的目的是爲了更好地回收內存,或者更快地分配內存。
- 是垃圾收集器管理的主要區域,也被稱作「GC堆」。
- 是Java虛擬機所管理的內存中最大的一塊。
- 在Java虛擬機規範中,若是在堆中沒有內存完成實例分配,且堆也沒法再擴展時,將會拋出OutOfMemoryError異常。
e.方法區(Method Area)
- 用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
- 與Java堆同樣,是各個線程共享的內存區域。
- 人們更願意把這個區域稱爲**「永久代」(Permanent Generation),在發佈的JDK1.7的HotSpot中,已經把本來放在永久代的字符串常量池移出。它還有個別名叫作Non-Heap(非堆)**。
- 在Java虛擬機規範中,當方法區沒法知足內存分配需求時,將拋出OutOfMemoryError異常。
f.運行時常量池(Runtime Constant Pool)
Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池(Constant Pool Table),用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後進入方法區的運行時常量池中存放。
- 相對於Class文件常量池的重要特徵是具有動態性,體如今並不是只有預置入Class文件中常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中。
- 是方法區的一部分,會受到方法區內存的限制。
- 在Java虛擬機規範中,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。
推薦閱讀:JVM內存溢出詳解(棧溢出,堆溢出,持久代溢出、沒法建立本地線程)
3.操做系統內存與JVM內存
從上圖可見操做系統內存和JVM內存的聯繫:
操做系統分爲棧和堆:
- 棧由操做系統管理,並由操做系統自動回收。
- 堆由用戶分配使用。
- 除JVM本地方法棧之外的JVM內存使用的操做系統的堆,以防JVM分配的內存被操做系統回收。
圖片來源:JVM內存管理—運行時內存區域
4.HotSpot虛擬機內存對象探祕
在熟悉虛擬機內存劃分及其具體內容以後,爲詳細瞭解虛擬機內存中數據的其餘細節,以經常使用的虛擬機HotSpot和經常使用的內存區域Java堆爲例,探討HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。
a.對象的建立:遇到一個new指令後建立過程分三步
- 類加載檢查:檢查new指令的參數是否能在常量池中定位到一個類的符號引用且該符號引用表明的類是否已被加載、解析和初始化,若沒有則需先執行相應的類加載,反之下一步。
- 分配內存:由Java堆中的內存是否規整決定如何給新生對象分配可用空間。
- 若規整,採用「指針碰撞」分配方式:
- 過程:將用過和空閒的內存放在兩邊,中間以一個指針做爲分界指示器。當分配內存時,就把指針向空閒一邊挪動與對象大小相等的距離便可。
- 應用:Serial、ParNew等帶Compact過程的收集器。
- 若非規整,採用「空閒列表」分配方式:
- 過程:維護一個記錄可用內存塊的列表。當分配內存時,就從列表中找到一塊足夠大的空間劃分給對象實例並更新記錄。
- 應用:基於Mark-Sweep算法的CMS收集器。
保證內存分配是線程安全的解決方案:
- 對內存分配的動做進行同步處理;
- 每一個線程在Java堆中預先分配一塊內存(本地線程分配緩衝TLAB),在本線程的TLAB上進行分配,當TLAB用完須要分配新的TLAB時再同步鎖定。
- 設置對象頭:將對象的所屬類、找到類的元數據信息的方式、對象的哈希碼、對象的GC分代年齡等信息存放在對象的對象頭中。
通過上述步驟,一個對象就產生了,但此時全部的字段都還爲零,還須要執行<init>
方法進行初始化,才能成爲真正可用的對象。
b.對象的內存佈局:分爲三塊區域
- 對象頭(Header):包括兩部分信息
- Mark Word:用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。
- 類型指針:用於肯定這個對象的所屬類。
- 實例數據(Instance Data):存儲真正的有效信息,是程序代碼中定義的各類類型的字段內容。存儲順序會受虛擬機分配策略參數和字段在Java源碼中定義順序這兩個因素影響。
- 對齊填充(Padding):佔位符,幫助補全未對齊的對象實例數據部分(保證是8字節的倍數),非必需。
c.對象的訪問定位:主流的兩種訪問方式
- 經過句柄訪問對象:在Java堆中劃分出一塊內存來做爲句柄池,reference存儲的是對象的句柄地址,在句柄中包含了對象實例數據與類型數據各自的具體地址信息。好處:reference中存儲的是穩定的句柄地址,在對象被移動時只會改變句柄中的實例數據指針,而reference自己不須要修改。
- 經過直接指針訪問對象:在Java堆對象的佈局中考慮如何放置訪問類型數據的相關信息,reference存儲的直接就是對象地址。好處:速度更快,節省了一次指針定位的時間開銷。
下篇將介紹和內存管理緊密相關的垃圾回收機制。