根據《Java虛擬機規(guī)范》的規(guī)定,運行時數(shù)據區(qū)通常包括這幾個部分:程序計數(shù)器(Program Counter Register)、虛擬機棧(VM Stack)、本地方法棧(Native Method Stack)、方法區(qū)(Method Area)、堆(Heap)。其中,程序計數(shù)器、虛擬機棧和本地方法棧是線程私有的,方法區(qū)和堆是線程共有的。如下圖所示:

一、程序計數(shù)器(Program Counter Register)
程序計數(shù)器可以看作是 當前線程所執(zhí)行的字節(jié)碼的行號指示器,字節(jié)碼解釋器工作時就是通過改變這個計數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼。
多線程是通過線程輪流切換來獲得CPU執(zhí)行時間的,因此,在任一具體時刻,一個CPU的內核只會執(zhí)行一條線程中的指令。因此,為了能夠使得每個線程都在線程切換后能夠恢復在切換之前的程序執(zhí)行位置,每個線程都需要有自己獨立的程序計數(shù)器,互不影響,為 線程私有 的。
如果線程執(zhí)行的是Java方法,則程序計數(shù)器中保存的是當前需要執(zhí)行的虛擬機字節(jié)碼指令的地址;如果線程執(zhí)行的是native方法,則程序計數(shù)器中的值是undefined。
二、Java虛擬機棧(VM Stack)
Java虛擬機棧也是 線程私有 的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內存模型:每個方法 在執(zhí)行的同時都會創(chuàng)建一個 棧幀(Stack Frame),用于存儲 局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。每個方法從調用直至執(zhí)行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
程序員主要關注的 棧內存,就是虛擬機棧中 局部變量表 部分。局部變量表存放了編譯器可知的各種 基本數(shù)據類型(boolean、byte、char、short、int、float、long、double),對象引用(reference)。局部變量表所需的內存空間在 編譯期間 完成分配,當進入一個方法時,這個方法需要在棧中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變局部變量表的大小。

三、本地方法棧(Native Method Stack)
本地方法棧和虛擬機棧是類似的,同樣是 線程私有,作用也是類似的。不同的地方在于,虛擬機棧為虛擬機執(zhí)行Java方法服務,而本地方法棧則為虛擬機使用到的 Native方法 服務。甚至有的虛擬機(如HotSpot)直接把本地方法棧和虛擬機棧合二為一了。
四、Java堆(Heap)
Java堆是被所有線程 共享 的一塊內存區(qū)域,在虛擬機啟動時創(chuàng)建。此內存區(qū)域的唯一目的就是存放對象實例。JVM規(guī)范中描述:所有的對象實例以及數(shù)組都要在堆上分配。
Java堆是垃圾收集器管理的主要區(qū)域,因為也被稱為 “GC堆”。從內存回收的角度來看,由于現(xiàn)代收集器基本都采用分代收集算法,所以Java堆還可以細分為:新生代 和 老年代。
從內存分配的角度來看,線程共享的Java堆可能劃分出 多個線程私有 的分配緩沖區(qū)(Thread Local Allocation Buffer, TLAB)。
五、方法區(qū)(Method Area)
方法區(qū)與Java堆一樣,是各個 線程共享 的內存區(qū)域,它用于存儲已被虛擬機加載的類信息(Class)、常量(final)、靜態(tài)變量(static)、即時編譯器編譯后的代碼等數(shù)據。
在HotSpot中方法區(qū)常常被成為 “永久代”,垃圾收集行為在這個區(qū)域比較少出現(xiàn),這個區(qū)域的內存回收目標主要是針對 常量池 的回收和對 類型 的卸載。
六、運行時常量池(Runtime Constant Pool) --> 方法區(qū)的一部分
運行時常量池是方法區(qū)的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用于存放編譯期生成的各種 字面量 和 符號引用 ,這部分內容將在類加載后進入方法區(qū)的運行時常量池中存放。
運行時常量池相對于CLass文件常量池的另外一個重要特征是具備 動態(tài)性,Java語言并不要求常量一定只有編譯期才能產生,也就是并非預置入CLass文件中常量池的內容才能進入方法區(qū)運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發(fā)人員利用比較多的就是 String類的intern()方法。
七、直接內存(Direct Memory)
直接內存并不是虛擬機運行時數(shù)據區(qū)的一部分,在JDK1.4中加入了NIO,引入基于通道(channel)與緩沖區(qū)(Buffer) I/O方式,它可以使用Native函數(shù)庫直接分配堆外內存。也就是說通過這種方式,不會在運行時數(shù)據區(qū)域分配內存,這樣就避免了在運行時數(shù)據區(qū)域來回復制數(shù)據,直接調用外部內存,在一些場景中顯著提高性能。==>了解一下Netty
HotSpot對象探秘
一、對象的創(chuàng)建
1.當虛擬機遇到一條new指令的時候,首先將去檢查這個指定的參數(shù)是否能在 常量池 中定位到這個類的符號引用,并且檢查這個符號引用代表的類是否已被 加載、解析和初始化 過。
2.如果類加載檢查通過后,虛擬機將為新生對象分配內存。對象所需內存的大小在類加載完成后便可以完全確定。==> Q:如何確定
兩種分配方式:
- 指針碰撞(Bump the Pointer): 內存是規(guī)整的,空閑內存在一端,不空閑的內存在一端,中間有一個指針作為分界點的指示器。分配內存就是簡單地往空閑空間挪動一段與對象大小相等的距離。
- 空閑列表(Free List): 內存是不規(guī)整的,虛擬機需要維護一個列表,記錄哪些內存塊是可用的,在分配的時候在列表中找到一塊足夠大的空間劃分給對象實例,并更新列表上的記錄。
內存分配的線程安全性:
- 同步分配內存: 對分配內存空間的動作進行同步處理————實際上虛擬機采用CAS配上失敗重試的方式保證更新操作的原子性。
- TLAB: 把內存分配動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱為 本地線程分配緩沖(Thread Local Allocation Buffer,TLAB)。
3.內存分配完成后,虛擬機需要將分配到的內存空間都初始化為 零值(不包括對象頭),這一步操作保證了對象的實例字段在Java代碼中可以不賦初始值就直接使用。
4.虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數(shù)據信息(Class)、對象的哈希碼、對象的GC分代年齡(新生代Eden,Survisor,老年代)等信息。這些信息存放在對象的 對象頭 (Object Header)中。
5.執(zhí)行new指令之后會接著執(zhí)行 <init>方法(構造方法),把對象按照程序員的意愿進行初始化。
二、對象的內存布局
對象在內存中存儲的布局可以分為三塊區(qū)域:對象頭(Header),實例數(shù)據(Instance Data) 和 對齊填充(Padding)。
對象頭
包括兩個部分,第一部分用于存儲自身運行時的數(shù)據例如GC標志位,MonirGC次數(shù),哈希碼,鎖狀態(tài),哪個線程可以擁有等被稱為MarkWord(標記字)。第二部分是類型指針,即對象指向它的類元數(shù)據(位于方法區(qū))的指針。
實例數(shù)據
是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。無論是從父類繼承下來的,還是在子類中定義的( 是否會導致數(shù)據冗余)
對齊填充
并不是必要的,也沒有特別含義,僅僅起著占位符的作用。
三:對象的訪問定位
建立對象是為了使用對象,我們的Java程序需要通過棧上(虛擬機棧)的reference數(shù)據來操作堆上的具體對象。目前reference主流的實現(xiàn)有 句柄 和 直接指針 兩種,其中HotSpot使用直接指針。
句柄
如果使用句柄,那么java堆中將會劃分出一部分內存作為 句柄池,reference中存儲的是對象的句柄地址,而句柄包含對象實例數(shù)據(堆)與類型數(shù)據(方法區(qū))各自的具體地址信息。

直接指針
reference引用直接指向堆中的對象實例,對象實例的對象頭存放對象類型指針。

區(qū)別
使用句柄訪問的最大好處就是reference中存儲的是穩(wěn)定的句柄地址,在對象被移動時(GC中很頻繁)只會改變句柄中的實例數(shù)據地址,而reference本身不需要修改。使用直接指針訪問最大的好處就是 速度更快 ,它節(jié)省了一次指針定位的時間開銷。