簡書 占小狼
轉(zhuǎn)載請注明原創(chuàng)出處,謝謝!
前言
對于C語言開發(fā)的程序員來說,在內(nèi)存管理方面,必須負責每一個對象的生命周期,從有到無。
對于Java程序員你來說,在虛擬機內(nèi)存管理的幫助下,不需要為每個new對象都匹配free操作,內(nèi)存泄露和內(nèi)存溢出等問題也不太容易出現(xiàn),不過也正是因為把內(nèi)存管理交給了虛擬機,一旦運行中的程序出現(xiàn)了內(nèi)存泄露問題,給排查過程造成很大困難。所以只有理解了Java虛擬機的運行機制,才能夠運籌帷幄于各種代碼。本文以HotSpot為例說說虛擬機的那些事。
JAVA虛擬機把管理的內(nèi)存劃分為幾個不同的數(shù)據(jù)區(qū)。

Java堆
Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,主要用于存放對象實例,Java虛擬機規(guī)范中有這樣一段描述:所有的對象實例和數(shù)據(jù)都要在堆上進行分配。為對象分配內(nèi)存就是把一塊大小確定的內(nèi)存從堆內(nèi)存中劃分出來,通常有兩種方法實現(xiàn):
1 、指針碰撞法
假設(shè)Java堆中內(nèi)存時完整的,已分配的內(nèi)存和空閑內(nèi)存分別在不同的一側(cè),通過一個指針作為分界點,需要分配內(nèi)存時,僅僅需要把指針往空閑的一端移動與對象大小相等的距離。
2、空閑列表法
事實上,Java堆的內(nèi)存并不是完整的,已分配的內(nèi)存和空閑內(nèi)存相互交錯,JVM通過維護一個列表,記錄可用的內(nèi)存塊信息,當分配操作發(fā)生時,從列表中找到一個足夠大的內(nèi)存塊分配給對象實例,并更新列表上的記錄。
對象創(chuàng)建是一個非常頻繁的行為,進行堆內(nèi)存分配時還需要考慮多線程并發(fā)問題,可能出現(xiàn)正在給對象A分配內(nèi)存,指針或記錄還未更新,對象B又同時分配到原來的內(nèi)存,解決這個問題有兩種方案:
1、采用CAS保證數(shù)據(jù)更新操作的原子性;
2、把內(nèi)存分配的行為按照線程進行劃分,在不同的空間中進行,每個線程在Java堆中預(yù)先分配一個內(nèi)存塊,稱為本地線程分配緩沖(Thread Local Allocation Buffer, TLAB);
Java棧
Java棧是線程私有的,每個線程對應(yīng)一個Java棧,每個線程在執(zhí)行一個方法時會創(chuàng)建一個對應(yīng)的棧幀(Stack Frame),棧幀負責存儲局部變量變量表、操作數(shù)棧、動態(tài)鏈接和方法返回地址等信息。每個方法的調(diào)用過程,相當于棧幀在Java棧的入棧和出棧過程。

局部變量表 用于存放方法參數(shù)和方法內(nèi)部定義的局部變量,其大小在代碼編譯期間已經(jīng)確定,在方法運行期間不會改變。局部變量表以變量槽(Slot)為最小存儲單位,每個Slot能夠存放一個boolean、byte、char、shot、int、float、reference和returnAddress類型的32位數(shù)據(jù),對于64位的數(shù)據(jù)類型long和double,虛擬機會以高位對齊的方式為其分配兩個連續(xù)的Slot空間。
在方法執(zhí)行時,如果是實例方法,即非static方法,局部變量表中第0位Slot默認存放對象實例的引用,在方法中可以通過關(guān)鍵字 this 進行訪問,方法參數(shù)按照參數(shù)列表順序,從第1位Slot開始分配,方法內(nèi)部變量則按照定義順序進行分配其余的Slot。
class test {
public int calc(int a, int b, String operation) {
operation = "+";
return a + b;
}
public void main(String args[]) {
calc(100, 200, "+");
}
}
對應(yīng)的局部變量表如下:

使用 javap -c 命令查看方法calc的字節(jié)碼

其中iload_1和iload_2分別從局部變量表中的第1位和第2位中加載數(shù)據(jù)。
方法區(qū)
方法區(qū)和Java堆一樣,是所有線程共享的內(nèi)存區(qū)域,用于存放已被虛擬機加載的類信息、常量、靜態(tài)變量和即時編譯器編譯后的代碼等數(shù)據(jù)。
運行時常量池是方法區(qū)的一部分,用于存放編譯期間生成的各種字面常量和符號引用。
指令計數(shù)器
指令計數(shù)器是線程私有的,每個線程都有獨立的指令計數(shù)器,計數(shù)器記錄著虛擬機正在執(zhí)行的字節(jié)碼指令的地址,分支、循環(huán)、跳轉(zhuǎn)、異常處理和線程恢復(fù)等操作都依賴這個計數(shù)器完成。如果線程執(zhí)行的是native方法,這個計數(shù)器則為空。
對象的內(nèi)存布局
對象在內(nèi)存中布局可以分成三塊區(qū)域:對象頭、實例數(shù)據(jù)和對齊填充。
1、對象頭
對象頭包括兩部分信息:運行時數(shù)據(jù)和類型指針,如果對象是一個數(shù)組,還需要一塊用于記錄數(shù)組長度的數(shù)據(jù)。
1.1、運行時數(shù)據(jù)包括哈希碼(HashCode)、GC分代年齡、鎖狀態(tài)標志、線程持有的鎖、偏向鎖ID和偏向時間戳等,這部分數(shù)據(jù)在32位和64位虛擬機中的長度分別為32bit和64bit,官方稱為"Mark Word"。Mark Word被設(shè)計成非固定的數(shù)據(jù)結(jié)構(gòu),以實現(xiàn)在有限空間內(nèi)保存盡可能多的數(shù)據(jù)。
32位的虛擬機中,對象未被鎖定的狀態(tài)下,Mark Word的32bit中25bit存儲對象的HashCode、4bit存儲對象分代年齡、2bit存儲鎖標志位、1bit固定為0,具體如下:

其它狀態(tài)(輕量級鎖定、重量級鎖定、GC鎖定、可偏向鎖)下Mark Word的存儲內(nèi)容如下:

1.2、對象頭的類型指針指向該對象的類元數(shù)據(jù),虛擬機通過這個指針可以確定該對象是哪個類的實例。
2、實例數(shù)據(jù)
實例數(shù)據(jù)就是在程序代碼中所定義的各種類型的字段,包括從父類繼承的,這部分的存儲順序會受到虛擬機分配策略和字段在源碼中定義順序的影響。
3、對齊填充
由于HotSpot的自動內(nèi)存管理要求對象的起始地址必須是8字節(jié)的整數(shù)倍,即對象的大小必須是8字節(jié)的整數(shù)倍,對象頭的數(shù)據(jù)正好是8的整數(shù)倍,所以當實例數(shù)據(jù)不夠8字節(jié)整數(shù)倍時,需要通過對齊填充進行補全。
END。
我是占小狼。
在魔都艱苦奮斗,白天是上班族,晚上是知識服務(wù)工作者。
如果讀完覺得有收獲的話,記得關(guān)注和點贊哦。
非要打賞的話,我也是不會拒絕的。