JVM是每一個Java開發天天都會接觸到的東西, 其相關知識也應該是每一個人都要深刻了解的. 但接觸了不少人發現: 或瞭解片面或知識體系陳舊. 所以最近抽時間研讀了幾本評價較高的JVM入門書籍, 算是總結於此. 本系列博客的主體來自 深刻理解Java虛擬機(第二版) 和 實戰Java虛擬機 兩部書, 部份內容參考 HotSpot實戰 和 深刻理解計算機系統 以及網上大量的文章. 若文內有引文未註明出處的, 還請聯繫做者修改.html
JVM 虛擬機架構(圖片來源: 淺析Java虛擬機結構與機制)java
JVM會將Java進程所管理的內存劃分爲若干不一樣的數據區域. 這些區域有各自的用途、建立/銷燬時間:程序員
(圖片來源: JAVA的內存模型及結構)算法
線程私有數據區域生命週期與線程相同, 依賴用戶線程的啓動/結束而建立/銷燬(在Hotspot VM內, 每一個線程都與操做系統的本地線程直接映射, 所以這部份內存區域的存/否跟隨本地線程的生/死).數組
一塊較小的內存空間, 做用是當前線程所執行字節碼的行號指示器(相似於傳統CPU模型中的PC), PC在每次指令執行後自增, 維護下一個將要執行指令的地址. 在JVM模型中, 字節碼解釋器就是經過改變PC值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴PC完成(僅限於Java方法, Native方法該計數器值爲undefined
).
不一樣於OS以進程爲單位調度, JVM中的併發是經過線程切換並分配時間片執行來實現的. 在任何一個時刻, 一個處理器內核只會執行一條線程中的指令. 所以, 爲了線程切換後能恢復到正確的執行位置, 每條線程都須要有一個獨立的程序計數器, 這類內存被稱爲「線程私有」內存.架構
虛擬機棧描述的是Java方法執行的內存模型: 每一個方法被執行時會建立一個棧幀(Stack Frame)用於存儲局部變量表、操做數棧、動態連接、方法出口等信息. 每一個方法被調用至返回的過程, 就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程(VM提供了-Xss
來指定線程的最大棧空間, 該參數也直接決定了函數調用的最大深度).併發
long
和double
佔用2個局部變量空間(Slot), 其他只佔用1個. 以下Java方法代碼可使用javap命令或javassist等字節碼工具讀到:public String test(int a, long b, float c, double d, Date date, List<String> list) { StringBuilder sb = new StringBuilder().append(a).append(b).append(c).append(d).append(date); for (String str : list) { sb.append(str); } return sb.toString(); }
注: javap/javassist讀到的實際上是靜態數據, 而局部變量表內存儲的倒是運行時動態加載的動態數據, 但由於局部變量表所需的內存空間在編譯期間便可完成分配, 當進入一個方法時, 這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間大小不會改變, 所以能夠在概念上認定這兩部份內容存儲的數據格式相同.app
與Java Stack做用相似, 區別是Java Stack爲執行Java方法服務, 而本地方法棧則爲Native方法服務, 若是一個VM實現使用C-linkage模型來支持Native調用, 那麼該棧將會是一個C棧(詳見: JVM學習筆記-本地方法棧(Native Method Stacks)), 但HotSpot VM直接就把本地方法棧和虛擬機棧合二爲一.函數
隨虛擬機的啓動/關閉而建立/銷燬.工具
幾乎全部對象實例和數組都要在堆上分配(棧上分配、標量替換除外), 所以是VM管理的最大一塊內存, 也是垃圾收集器的主要活動區域. 因爲現代VM採用分代收集算法, 所以Java堆從GC的角度還能夠細分爲: 新生代(Eden區、From Survivor區和To Survivor區)和老年代; 而從內存分配的角度來看, 線程共享的Java堆還還能夠劃分出多個線程私有的分配緩衝區(TLAB). 而進一步劃分的目的是爲了更好地回收內存和更快地分配內存.
即咱們常說的永久代(Permanent Generation), 用於存儲被JVM加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據. HotSpot VM把GC分代收集擴展至方法區, 即使用Java堆的永久代來實現方法區, 這樣HotSpot的垃圾收集器就能夠像管理Java堆同樣管理這部份內存, 而沒必要爲方法區開發專門的內存管理器(永久帶的內存回收的主要目標是針對常量池的回收和類型的卸載, 所以收益通常很小)
不過在1.7的HotSpot已經將本來放在永久代的字符串常量池移出:
而在1.8中, 永久區已經被完全移除, 取而代之的是元數據區Metaspace(這一點在查看GC日誌和使用jstat -gcutil查看GC狀況時能夠觀察到),與永久代不一樣, 若是不指定Metaspace大小, 若是方法區持續增加, VM會默認耗盡全部系統內存.
test
方法中讀到的signature
信息). 但Java語言並不要求常量必定只能在編譯期產生, 即並不是預置入Class文件中常量池的內容才能進入方法區運行時常量池, 運行期間也可能將新的常量放入池中, 如String
的intern()
方法.直接內存並非JVM運行時數據區的一部分, 但也會被頻繁的使用: 在JDK 1.4引入的NIO提供了基於Channel與Buffer的IO方式, 它可使用Native函數庫直接分配堆外內存, 而後使用DirectByteBuffer
對象做爲這塊內存的引用進行操做(詳見: Java I/O 擴展), 這樣就避免了在Java堆和Native堆中來回複製數據, 所以在一些場景中能夠顯著提升性能.
顯然, 本機直接內存的分配不會受到Java堆大小的限制(即不會遵照-Xms、-Xmx等設置), 但既然是內存, 則確定仍是會受到本機總內存大小及處理器尋址空間的限制, 所以動態擴展時也會出現OutOfMemoryError
異常.
new
一個Java Object(包括數組和Class對象), 在JVM會發生以下步驟:
new
指令: 首先去檢查該指令的參數是否能在常量池中定位到一個類的符號引用, 並檢查這個符號引用表明的類是否已被加載、解析和初始化過. 若是沒有, 必須先執行相應的類加載過程.-XX:+/-UseTLAB
參數設定).<init>
方法還沒有執行, 全部字段還都爲零). 因此new
指令以後通常會(由字節碼中是否跟隨有invokespecial
指令所決定-Interface通常不會有, 而Class通常會有)接着執行<init>
方法, 把對象按照程序員的意願進行初始化, 這樣一個真正可用的對象纔算徹底產生出來.HotSpot VM內, 對象在內存中的存儲佈局能夠分爲三塊區域:對象頭、實例數據和對齊填充:
注意: 並不是全部VM實現都必須在對象數據上保留類型指針, 也就是說查找對象的元數據並不是必定要通過對象自己(詳見下面句柄定位對象方式).
狀態 | 標誌位 | 存儲內容 |
---|---|---|
未鎖定 | 01 | 對象哈希碼、對象分代年齡 |
輕量級鎖定 | 00 | 指向鎖記錄的指針 |
膨脹(重量級鎖定) | 10 | 執行重量級鎖定的指針 |
GC標記 | 11 | 空(不須要記錄信息) |
可偏向 | 01 | 偏向線程ID、偏向時間戳、對象分代年齡 |
longs
/doubles
、ints
、shorts
/chars
、bytes
/booleans
、oops
(Ordinary Object Pointers), 相同寬度的字段老是被分配到一塊兒, 在知足這個前提條件下, 在父類中定義的變量會出如今子類以前. 若是CompactFields
參數值爲true
(默認), 那子類中較窄的變量也可能會插入到父類變量的空隙中.創建對象是爲了使用對象, Java程序須要經過棧上的reference來操做堆上的具體對象. 主流的有句柄和直接指針兩種方式去定位和訪問堆上的對象:
句柄: Java堆中將會劃分出一塊內存來做爲句柄池, reference中存儲對象的句柄地址, 而句柄中包含了對象實例數據與類型數據的具體各自的地址信息:
直接指針(HotSpot使用): 該方式Java堆對象的佈局中就必須考慮如何放置訪問類型數據的相關信息, reference中存儲的直接就是對象地址:
這兩種對象訪問方式各有優點: 使用句柄來訪問的最大好處是reference中存儲的是穩定句柄地址, 在對象被移動(垃圾收集時移動對象是很是廣泛的行爲)時只會改變句柄中的實例數據指針,而reference自己不變. 而使用直接指針最大的好處就是速度更快, 它節省了一次指針定位的時間開銷,因爲對象訪問很是頻繁, 所以這類開銷積小成多也是一項很是可觀的執行成本.