聲明:本文集中的文章都是看了《深入理解Java
虛擬機(jī)》所做的筆記,很多內(nèi)容是從書中摘抄,特此聲明。這里主要學(xué)習(xí)一些基本的概念,真正掌握可能還需要多加實(shí)踐。
一、運(yùn)行時(shí)數(shù)據(jù)區(qū)域
注:圖片來自http://www.lxweimin.com/p/6173a467165e
Java
虛擬機(jī)在執(zhí)行Java
程序的過程中會(huì)把它管理的內(nèi)存劃分為若干個(gè)不同的數(shù)據(jù)區(qū)域,根據(jù)《Java
虛擬機(jī)規(guī)范(Java SE 7
版)》的規(guī)定,Java
虛擬機(jī)所管理的內(nèi)存將會(huì)包含如圖所示的幾個(gè)運(yùn)行時(shí)區(qū)域。
1.1 程序計(jì)數(shù)器
程序計(jì)數(shù)器是一塊較小的內(nèi)存空間,它可以看作是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器。如果線程正在執(zhí)行的是一個(gè)Java
方法,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令的地址;如果正在執(zhí)行的是Native
方法,這個(gè)計(jì)數(shù)器值則為空(Undefined
)。此內(nèi)存區(qū)域是惟一一個(gè)在Java
虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError
情況的區(qū)域。
1.2 Java虛擬機(jī)棧
- 與程序計(jì)數(shù)器一樣,
Java
虛擬機(jī)棧(Java Virtual Machine Stacks
)也是線程私有的,它的生命周期與線程相同。虛擬機(jī)棧描述的是Java
方法執(zhí)行的內(nèi)存模型:每個(gè)方法在執(zhí)行的同時(shí)都會(huì)創(chuàng)建一個(gè)棧幀(Stack Frame
)用于存儲(chǔ)局部變量表、操作數(shù)棧、動(dòng)態(tài)鏈接、方法出口等信息。 -
局部變量表存放了編譯期可知的各種基本類型(
boolean、byte、char、short、int、float、long、double
)、對(duì)象引用(reference
類型,它不同于對(duì)象本身,可能是一個(gè)指向?qū)ο笃鹗嫉刂返囊弥羔槪部赡苁侵赶蛞粋€(gè)代表對(duì)象的句柄或其他與此對(duì)象相關(guān)的位置)和returnAddress
類型(指向了一條字節(jié)碼指令的地址) - 其中
64
位長度的long
和double
類型的數(shù)據(jù)會(huì)占用兩個(gè)局部變量空間(Slot
),其余的類型只占一個(gè)。局部變量表所需的內(nèi)存空間在編譯期間完成分配,當(dāng)進(jìn)入一個(gè)方法時(shí),這個(gè)方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運(yùn)行期間不會(huì)改變局部變量表的大小。 - 在
Java
虛擬機(jī)規(guī)范中,對(duì)這個(gè)區(qū)域規(guī)定了兩種異常狀況:如果線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度,將拋出StackOverflowEorror
異常;如果虛擬機(jī)可以動(dòng)態(tài)擴(kuò)展(虛擬機(jī)棧大小也可以是固定的),如果擴(kuò)展時(shí)無法申請(qǐng)到足夠的內(nèi)存,將拋出OutOfMemoryError
異常。
1.3 本地方法棧
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用是非常相似的,它們之間的區(qū)別不過是虛擬機(jī)棧為虛擬機(jī)執(zhí)行Java
方法(也就是字節(jié)碼)服務(wù),而本地方法棧則為虛擬機(jī)使用到的Native
方法服務(wù)。虛擬機(jī)規(guī)范中對(duì)本地方法中方法使用的語言、使用方式與數(shù)據(jù)結(jié)構(gòu)并沒有強(qiáng)制規(guī)定,因此具體的虛擬機(jī)可以自由實(shí)現(xiàn)它。甚至有的虛擬機(jī)(如Sun HotSpot
)直接就把本地方法棧和虛擬機(jī)棧合二為一。
1.4 Java堆
對(duì)于大多數(shù)應(yīng)用來說,
Java
堆(Java Heap
)是Java
虛擬機(jī)所管理的內(nèi)存中最大的一塊。Java
堆是被所有線程共享的一塊內(nèi)存區(qū)域。在虛擬機(jī)啟動(dòng)時(shí)創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對(duì)象實(shí)例,幾乎所有的對(duì)象實(shí)例都在這里分配內(nèi)存。在Java
規(guī)范中的描述是:所有對(duì)象實(shí)例以及數(shù)組都要在堆上分配,但是隨著技術(shù)的發(fā)展,也不是那么絕對(duì)了。Java
堆是垃圾收集器的主要區(qū)域,因此很多時(shí)候也被稱為“GC
堆”。從內(nèi)存回收的角度來看,由于現(xiàn)在收集器基本都采用分代收集算法,所以Java
堆中還可以細(xì)分為:新生代和老年代;再細(xì)致一點(diǎn)有Eden
空間、From Survivor
空間、To Survivor
空間等。從內(nèi)存分配角度看,線程共享的
Java
堆中可能劃分出多個(gè)線程私有的分配緩沖區(qū)(Thread Local Allocation Buffer,TLAB
)。根據(jù)Java
虛擬機(jī)規(guī)范的規(guī)定,Java
堆可以處于物理上不連續(xù)的內(nèi)存空間,只要邏輯上是連續(xù)的即可。可以通過-Xmx
指定可用最小內(nèi)存,通過-Xms
指定可用最大內(nèi)存。
1.5 方法區(qū)
方法區(qū)與
Java
堆一樣,是各個(gè)線程共享的內(nèi)存區(qū)域,它用于存儲(chǔ)已被虛擬機(jī)加載的類信息、常量、靜態(tài)變量、即時(shí)編譯器編譯后的代碼等數(shù)據(jù)。雖然Java
虛擬機(jī)規(guī)范把方法區(qū)描述為堆的一個(gè)邏輯部分,但是它卻有一個(gè)別名叫做Non-Heap
(非堆),目的是和堆區(qū)分。Java
虛擬機(jī)規(guī)范對(duì)方法區(qū)的限制非常寬松,除了和堆一樣不需要連續(xù)的內(nèi)存和可以選擇固定大小或者可擴(kuò)展外,還可以選擇不是先垃圾收集。垃圾收集在這個(gè)區(qū)域比較少見,但是并非進(jìn)入此區(qū)域的數(shù)據(jù)就永久存在了,這個(gè)區(qū)域的內(nèi)存回收目標(biāo)主要是針對(duì)常量池的回收和對(duì)類型的卸載,但是回收效果不是太好。
1.6 運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池(Runtime Constant Pool
)是方法區(qū)的一部分。Class
文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項(xiàng)信息是常量池(Constant poo Table
),用于存放編譯器期生成的各種字面量和符號(hào)引用,這部分內(nèi)容將在類加載后進(jìn)入方法區(qū)的運(yùn)行時(shí)常量池中存放。
1.7 直接內(nèi)存
直接內(nèi)存(
Direct Memory
)并不是虛擬機(jī)運(yùn)行數(shù)據(jù)區(qū)的一部分,也不是Java
虛擬機(jī)規(guī)范中定義的內(nèi)存區(qū)域。但是這部分內(nèi)存也被頻繁地使用,而且也可能導(dǎo)致OutOfMemoryError
異常出現(xiàn)。在
JDK1.4
中新加入了NIO
類,引入了一種基于通道與緩沖區(qū)的I/O
方式,它可以使用Native
函數(shù)庫直接分配堆外內(nèi)存,然后通過一個(gè)存儲(chǔ)在Java
堆中的DirectByteBuffer
對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場景顯著提高性能,因?yàn)楸苊饬嗽?code>Java堆和Native
堆中來回復(fù)制數(shù)據(jù)。
二、HotSpot虛擬機(jī)對(duì)象探秘
2.1 對(duì)象的創(chuàng)建
在語言層面上,創(chuàng)建對(duì)象通常僅僅是一個(gè)new
關(guān)鍵字而已,現(xiàn)在看在虛擬機(jī)中是怎樣的過程。
虛擬機(jī)遇到一條
new
指令時(shí),首先將去檢查這個(gè)指令的參數(shù)是否能在常量池中定位到一個(gè)類的符號(hào)引用,并且檢查這個(gè)符號(hào)引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執(zhí)行相應(yīng)的類加載過程,這在后面詳細(xì)討論。-
在類加載檢查通過后,接下來虛擬機(jī)將為新生對(duì)象分配內(nèi)存。對(duì)象所需內(nèi)存的大小在類加載完成后便可完全去確定。為對(duì)象分配空間的任務(wù)等同于把一塊確定大小的內(nèi)存從
Java
堆中劃分出來,一般有兩種分配方式:-
指針碰撞(
Bump the Point
):如果堆是絕對(duì)規(guī)整的,用過的內(nèi)存和沒用過的內(nèi)存分開放置,中間使用一個(gè)指針作為分界點(diǎn)的指示器,那分配的時(shí)候只需要將指針移動(dòng)所需大小的內(nèi)存即可。 -
空閑列表(
Free List
):如果堆內(nèi)存不是規(guī)整的,則需要維護(hù)一個(gè)列表來記錄哪些內(nèi)存是可用的,哪些是不可用的,在分配的時(shí)候需要根據(jù)這個(gè)表來進(jìn)行分配。
選擇哪種分配方式有Java
堆是否規(guī)整決定,而Java
堆是否規(guī)整又由所采用的垃圾收集器是否帶有壓縮整理功能決定。因此,使用Serial、ParNew
等帶Compact
過程的收集器時(shí),采用前一種;而使用CMS
這種基于Mark-Sweep
算法的收集器時(shí),采用后一種。
-
指針碰撞(
-
在分配內(nèi)存還需要考慮一個(gè)問題,就是創(chuàng)建對(duì)象在虛擬機(jī)中是很頻繁的,即使是僅僅修改一個(gè)指針?biāo)赶虻奈恢茫诓l(fā)情況下也并不是線程安全的,可能同一塊內(nèi)存會(huì)同時(shí)分配給多個(gè)線程。解決這個(gè)問題有兩種方案:
- 一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用
CAS
配上失敗重試的方式保證更新操作的原子性。 - 另一種是把內(nèi)存分配的動(dòng)作按照線程劃分在不同的空間中進(jìn)行,即每個(gè)線程在
Java
堆中預(yù)先分配一小塊內(nèi)存,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB
)。哪個(gè)線程要分配內(nèi)存就在哪個(gè)線程的TLAB
上分配,只有TLAB
用完并分配新的TLAB
時(shí),才需要同步鎖定。虛擬機(jī)是否使用TLAB
,可以通過-XX:+/-UserTLAB
參數(shù)來設(shè)定。
- 一種是對(duì)分配內(nèi)存空間的動(dòng)作進(jìn)行同步處理——實(shí)際上虛擬機(jī)采用
內(nèi)存分配完成后,虛擬機(jī)需要將分配到的內(nèi)存空間都初始化為零(不包括對(duì)象頭),如果使用
TLAB
,這一工作過程也可以提前至TLAB
分配時(shí)進(jìn)行,這一步操作保證了對(duì)象的實(shí)例字段在Java
代碼中可以不賦初值就可以直接使用。之后,虛擬機(jī)要對(duì)對(duì)象進(jìn)行必要的設(shè)置,例如這個(gè)對(duì)象是哪個(gè)類的實(shí)例、如何才能找到類的元數(shù)據(jù)信息、對(duì)象的哈希碼、對(duì)象的
GC
分代年齡等信息,這些信息都存放在對(duì)象頭(Object Header
)中。在上面工作完成后,從虛擬機(jī)角度看,一個(gè)新對(duì)象已經(jīng)完成,但從
Java
程序的角度看,對(duì)象創(chuàng)建才剛剛開始——<init>
方法還沒執(zhí)行,所有字段都還為零。一般來說,執(zhí)行new
指令后會(huì)接著執(zhí)行<init>
方法,把對(duì)象按照程序員的意思進(jìn)行初始化,這樣一個(gè)對(duì)象才算真正產(chǎn)生出來。
2.2 對(duì)象的內(nèi)存布局
在HotSpot
虛擬機(jī)中,對(duì)象在內(nèi)存中存儲(chǔ)的布局可以分為三塊區(qū)域:對(duì)象頭(Header
)、實(shí)例數(shù)據(jù)(Instance Data
)和對(duì)齊填充(Padding
)。
對(duì)象頭包括兩部分信息:
- 第一部分用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù),如哈希碼、
GC
分代年齡、鎖狀態(tài)標(biāo)志、線程持有的鎖、偏向線程ID
、偏向時(shí)間戳等,這部分?jǐn)?shù)據(jù)的長度在32
位和64
位的虛擬機(jī)(未開啟壓縮指針)中分別為32bit
和64bit
,官方稱為"Mark Word"
。其實(shí)對(duì)象要存儲(chǔ)的運(yùn)行時(shí)數(shù)據(jù)很多,其實(shí)已經(jīng)超出32
位、64
位Bitmap
能記錄的限度,但是Mark Word
被設(shè)計(jì)成一個(gè)非固定的數(shù)據(jù)結(jié)構(gòu)以便在極小的空間內(nèi)存儲(chǔ)盡量多的信息,它會(huì)根據(jù)對(duì)象的裝填復(fù)用存儲(chǔ)空間。如32
位HotSpot
虛擬機(jī)中,如果對(duì)象處于未被鎖定的狀態(tài)下,那么25bit
存儲(chǔ)對(duì)象哈希碼,4bit
存儲(chǔ)對(duì)象分代年齡,2bit
存儲(chǔ)鎖標(biāo)志位,1bit
固定為0
,而在其他狀態(tài)如下對(duì)象的存儲(chǔ)內(nèi)容如下:
存儲(chǔ)內(nèi)容 | 標(biāo)志位 | 狀態(tài) |
---|---|---|
對(duì)象哈希碼、對(duì)象的分代年齡 | 01 |
未鎖定 |
指向鎖記錄的指針 | 00 |
輕量級(jí)鎖定 |
指向重量級(jí)鎖的指針 | 10 |
膨脹(重量級(jí)鎖定) |
空,不需要記錄信息 | 11 |
GC 標(biāo)記 |
偏向線程ID 、偏向時(shí)間戳、對(duì)象分代年齡 |
01 |
可偏向 |
對(duì)象頭的另外一部分是類型指針,即對(duì)象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例。并不是所有的虛擬機(jī)實(shí)現(xiàn)都必須在對(duì)象數(shù)據(jù)上保留類型指針,也就是說查找對(duì)象的元數(shù)據(jù)信息并不一定要經(jīng)過對(duì)象本身,這在后面討論。如果對(duì)象是一個(gè)
Java
數(shù)組,那么對(duì)象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù),因?yàn)樘摂M機(jī)可以通過普通Java
對(duì)象的元數(shù)據(jù)確定Java
對(duì)象的大小,但是從數(shù)組的元數(shù)據(jù)中卻不能確定數(shù)組的大小。對(duì)象頭之后便是實(shí)例數(shù)據(jù)部分。即對(duì)象真正存儲(chǔ)的有效信息,也是在程序代碼中定義的各種類型的字段內(nèi)容(父類繼承的、子類中定義的都要記錄)。存儲(chǔ)的策略受到虛擬機(jī)分配策略參數(shù)(
FieldsAllocationStyle
)和字段在Java
源碼中定義順序的影響。默認(rèn)分配策略為long/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers)
,相同寬度的字段總是被分配在一起。在滿足這個(gè)條件下,在父類中定義的變量會(huì)在子類之前。如果CompactFields
參數(shù)值為true
(默認(rèn)),那么子類中較窄的變量可能會(huì)插入到父類變量的空隙中。第三部分對(duì)齊填充不是必然存在的,也沒有特別含義,僅僅起著占位符的作用。由于虛擬機(jī)的自動(dòng)內(nèi)存管理系統(tǒng)要求對(duì)象起始地址必須是把八字節(jié)的整數(shù)倍,即對(duì)象的大小必須是八字節(jié)的整數(shù)倍,如果對(duì)象不夠八字節(jié)的整數(shù)倍則需要填充對(duì)齊。
2.3 對(duì)象的訪問定位
建立對(duì)象是為了使用對(duì)象,我們的Java程序需要通過棧上的reference
數(shù)據(jù)來操作堆上的具體對(duì)象。由于reference
類型在Java
虛擬機(jī)規(guī)范中只規(guī)定了一個(gè)指向?qū)ο蟮囊茫]有定義這個(gè)引用應(yīng)該通過何種方式去定位、訪問堆中的對(duì)象的具體位置,所以對(duì)象訪問方式也取決于虛擬機(jī)實(shí)現(xiàn)而定的。目前主流的訪問方式有使用句柄和直接指針兩種。
-
如果使用句柄訪問的話,那么
Java
堆中將會(huì)劃分出一塊內(nèi)存來作為句柄池,reference
中存儲(chǔ)的就是對(duì)象的句柄地址,而句柄中包含了對(duì)象實(shí)例類型與類型數(shù)據(jù)各自的具體地址信息。如圖所示。
2 -
如果使用直接指針訪問,那么
Java
堆對(duì)象的布局中就必須考慮如何放置訪問類型數(shù)據(jù)的相關(guān)信息,而reference
中存儲(chǔ)的直接就是對(duì)象地址。如圖所示。
3
這兩種對(duì)象訪問方式各有優(yōu)勢,使用句柄來訪問最大的好處就是reference
中存儲(chǔ)的是穩(wěn)定的句柄地址,在對(duì)象被移動(dòng)(垃圾收集時(shí)移動(dòng)對(duì)象是非常普遍的行為)時(shí)只會(huì)改變句柄中的實(shí)例數(shù)據(jù)指針,而reference
本身不需要修改。使用直接指針訪問方式最大的好處就是速度更快。對(duì)于Sun HotSpot
而言,使用的是第二種方式進(jìn)行對(duì)象訪問的。