對(duì)應(yīng)視頻地址:
(上集):https://www.bilibili.com/video/BV1BT4y1G73q
(下集):https://www.bilibili.com/video/BV13Z4y147mt
筆記MarkDown版下載地址:https://wwa.lanzous.com/i7TxGj2mhrc
本文章原創(chuàng)博客地址:https://blog.mcplugin.cn/p/699
1. 操作數(shù)棧(OS)和本地變量表(LVA)
本地變量表 (LVA)
本地變量表是一個(gè)以0為起始下標(biāo)的字?jǐn)?shù)組拜秧,它包含了所有參數(shù)和局部變量,每一個(gè)slot都是4個(gè)字節(jié)(Byte)大小
int乃秀、float和reference類型的值在數(shù)組中占據(jù)1個(gè)slot,即4個(gè)字節(jié)峻呛。
double和long的值在數(shù)組中占據(jù)2個(gè)連續(xù)的slot迫肖,即總共8個(gè)字節(jié)景醇。
Byte欲侮、short和char的值在存儲(chǔ)前會(huì)被轉(zhuǎn)換為int類型崭闲,占據(jù)1個(gè)slot,即4個(gè)字節(jié)威蕉。
但是不同的 JVM 對(duì) Boolean 的存儲(chǔ)方式是不同的刁俭。但大多數(shù)JVM在局部變量數(shù)組中給Boolean提供了1個(gè)slot。
參數(shù)會(huì)按照聲明的順序放入局部變量數(shù)組中忘伞。
操作數(shù)棧 (OS)
JVM使用操作數(shù)棧作為工作空間薄翅,是用來(lái)存儲(chǔ)中間計(jì)算的結(jié)果的沙兰。
操作數(shù)棧的組織形式是像本地變量表 (LVA) 一樣的字?jǐn)?shù)組氓奈,每一個(gè)slot占用4個(gè)字節(jié)(Byte)。但它不像本地變量數(shù)組那樣使用索引來(lái)訪問(wèn)鼎天,而是通過(guò)一些指令來(lái)訪問(wèn) (OS是一個(gè)Stack模型舀奶,可以進(jìn)行push或者pop) 這些指令可以將值推送到操作數(shù)棧(如:push),一些指令(如:pop)可以從操作數(shù)棧中彈出值 還有一些指令可以執(zhí)行必要的操作斋射。例:50 + 20 = ?JVM內(nèi)運(yùn)行環(huán)境的OS和LVA育勺,以及其對(duì)應(yīng)的字節(jié)碼的操作
iload_0 把一個(gè)int值從本地變量表中index為0的地方壓入操作數(shù)棧
iload_1 把一個(gè)int值從本地變量表中index為1的地方壓入操作數(shù)棧
將棧頂兩int型數(shù)值相減并將結(jié)果壓入棧頂
istore_2 講一個(gè)int值從操作數(shù)棧中彈出,并存儲(chǔ)到本地變量表index為2的地方
(資料來(lái)源:Java Virtual Machine (JVM) Stack Area - GeeksforGeeks)
2. this指針是何時(shí)賦值的
JVM字節(jié)碼:
對(duì)應(yīng)的Java代碼
這段代碼的0~7是對(duì)一個(gè)Test4對(duì)象進(jìn)行new
在執(zhí)行invokespecial指令的時(shí)候進(jìn)行的賦值罗岖。執(zhí)行完畢invokespecial之后涧至,才回去執(zhí)行<com/qimingnan/jvm/Test4.<init>>調(diào)用Test4的構(gòu)造器
invokespecial:調(diào)用超類構(gòu)建方法, 實(shí)例初始化方法, 私有方法。
那么為什么要在invokespecial的時(shí)候進(jìn)行賦值呢桑包?
因?yàn)槲覀冊(cè)跇?gòu)造方法中南蓬,需要使用this指針。所以我們要在調(diào)用Test4的構(gòu)造器之前進(jìn)行this指針的賦值
所有的非靜態(tài)方法中的局部變量表中index0的位置,存放的都是this指針
2.1 new一個(gè)對(duì)象是否是線程安全的赘方?
new是非線程安全的**烧颖,因?yàn)橐痪?/p>
Java
Test4demo=newTest4
就有4句JVM字節(jié)碼來(lái)對(duì)應(yīng)實(shí)現(xiàn)。
0 new#2<com/qimingnan/jvm/Test4>
3 dup
4 invokespecial#3<com/qimingnan/jvm/Test4.<init>>
7 astore_1
這里7為什么是astore_1窄陡,而不是astore_0炕淮?
因?yàn)閍store_0存儲(chǔ)的是this指針,所以demo對(duì)象只能放在index1的位置
在main()方法中跳夭,index0存放的是args參數(shù)
3. 棧和棧幀的區(qū)別:
棧:每一個(gè)線程涂圆,都會(huì)有一個(gè)獨(dú)特自己的虛擬機(jī)棧(Stack),所以在棧里的數(shù)據(jù)是不會(huì)進(jìn)行線程互斥优妙,線程安全問(wèn)題的乘综。
棧幀:每調(diào)用一個(gè)方法,就會(huì)產(chǎn)生一個(gè)棧幀套硼。
4. return的時(shí)候都做了些什么卡辰?
如:ireturn
ireturn:從當(dāng)前方法返回int
不僅僅是這樣,當(dāng)JVM執(zhí)行ireturn的時(shí)候邪意,做了四件事:
1. 將局部變量表指針恢復(fù)成上一幀的指針
2. 將操作數(shù)棧的指針恢復(fù)成上一幀的指針
3. 將運(yùn)行時(shí)數(shù)據(jù)區(qū)內(nèi)的程序計(jì)數(shù)器改成調(diào)用invokevirtual <com/qimingnan/jvm/Test4.add>的下一個(gè)字節(jié)碼指令序號(hào) (如圖所示九妈,即:改成15)
4. 將add()方法占用的棧幀內(nèi)存全部回收
4.1額外思考:
如圖所示,
為什么add方法中的賦值操作雾鬼,需要先將10push到操作數(shù)棧(OS)萌朱,在istore到本地變量表(LVA)中呢?
而不是直接對(duì)本地變量表進(jìn)行操作呢策菜?
由于視頻中老師沒(méi)有進(jìn)行具體的解答晶疼,所以我在SOF進(jìn)行了提問(wèn),發(fā)現(xiàn)有人提出過(guò)類似的問(wèn)題
我發(fā)的帖子下面有人進(jìn)行的回復(fù)截圖:
我發(fā)現(xiàn)別人提問(wèn)的帖子地址:What is the role of operand stack in JVM? (stackoverflow.com)
有興趣的同學(xué)可以自行閱覽又憨。
5. 運(yùn)行時(shí)數(shù)據(jù)區(qū)內(nèi)幾個(gè)區(qū)域之間的關(guān)系
5.1 虛擬機(jī)棧 與 堆
如在一個(gè)方法中翠霍,有這么一句代碼:
Java
publicvoidtest{
Testdemo=newTest();
}
則,在test()方法的本地變量表中蠢莺,index1的位置寒匙,會(huì)有一個(gè)名為demo的對(duì)象,其引用內(nèi)容為堆中的對(duì)象內(nèi)容
6. 堆(Heap)
6.1 堆的默認(rèn)大絮锝:
最谐酢:1/64的物理內(nèi)存
最大:1/4的物理內(nèi)存
6.2 堆為什么要分新生代和老年代:
6.2.1 老年代存儲(chǔ)的內(nèi)容
大對(duì)象。什么是大對(duì)象祸憋?超過(guò)eden區(qū)会宪,就是大對(duì)象
GC年齡超過(guò)15。經(jīng)歷過(guò)15次GC
空間分配擔(dān)保一句話解釋JVM中空間分配擔(dān)保的問(wèn)題先解釋YGC(輕GC):當(dāng)對(duì)象生成在EDEN區(qū)失敗時(shí)蚯窥,出發(fā)一次YGC掸鹅,先掃描EDEN區(qū)中的存活對(duì)象喜命,進(jìn)入S0區(qū),S0放不下的進(jìn)入OLD區(qū)河劝,再掃描S1區(qū)壁榕,若存活次數(shù)超過(guò)閥值則進(jìn)入OLD區(qū),其它進(jìn)入S0區(qū)赎瞎,然后S0和S1交換一次牌里。那么當(dāng)發(fā)生YGC時(shí),JVM會(huì)首先檢查老年代最大的可用連續(xù)空間是否大于新生代所有對(duì)象的總和务甥,如果大于牡辽,那么這次YGC是安全的,如果不大于的話敞临,JVM就需要判斷HandlePromotionFailure是否允許空間分配擔(dān)保态辛。允許分配擔(dān)保:JVM繼續(xù)檢查老年代最大的可用連續(xù)空間是否大于歷次晉升到老年代的對(duì)象的平均大小,如果大于挺尿,則正常進(jìn)行一次YGC奏黑,盡管有風(fēng)險(xiǎn)(因?yàn)榕袛嗟氖瞧骄笮。锌赡苓@次的晉升對(duì)象比平均值大很多)编矾;如果小于熟史,或者HandlePromotionFailure設(shè)置不允許空間分配擔(dān)保,這時(shí)要進(jìn)行一次FGC窄俏。新生代采用的是復(fù)制收集算法蹂匹,S0和S1始終只是用其中一塊內(nèi)存區(qū),當(dāng)出現(xiàn)YGC后大部分對(duì)象仍然存活的話凹蜈,就需要老年代進(jìn)行分配擔(dān)保限寞,把survior區(qū)無(wú)法容納的對(duì)象直接晉升到老年代。那么這種空間分配擔(dān)保的前提是老年代還有容納的空間仰坦,一共有多少對(duì)象會(huì)活下來(lái)履植,在實(shí)際完成內(nèi)存回收之前是無(wú)法明確知道的,所以只好取之前每次回收晉升到老年代對(duì)象容量的平均值大小作為經(jīng)驗(yàn)值缎岗,與老年代的剩余空間比較静尼,決定是否進(jìn)行FGC(重GC)來(lái)讓老年代騰出更多空間白粉。
6.3 新生代中各大分區(qū)占比比例的由來(lái)
對(duì)大量程序經(jīng)過(guò)統(tǒng)計(jì)之后传泊,發(fā)現(xiàn)Eden區(qū)的對(duì)象只有5%~10%的對(duì)象會(huì)存活,所以在新生代中鸭巴,各大區(qū)域分布比例為8:1:1
7. 對(duì)象大小的計(jì)算方式
7.1 Class文件
Class文件開(kāi)始的兩個(gè)字(Word=2Byte)的大小的數(shù)據(jù)為cafe babe(咖啡Baby)
7.2 對(duì)象(Object)結(jié)構(gòu)
Mrk Word:用于存儲(chǔ)對(duì)象自身的運(yùn)行時(shí)數(shù)據(jù)眷细,如哈希碼(HashCode)、GC分代年齡鹃祖、鎖狀態(tài)標(biāo)志溪椎、線程持有的鎖、偏向線程ID、偏向時(shí)間戳等等校读。Mark Word在32位JVM中的長(zhǎng)度是32bit沼侣,在64位JVM中長(zhǎng)度是64bit。32位MarkWord數(shù)據(jù)結(jié)構(gòu)【共4B】:
類型指針:指向Class對(duì)象(即這個(gè)對(duì)象(Object)的模板)
數(shù)組長(zhǎng)度:若這個(gè)對(duì)象是數(shù)組歉秫,則此處存放數(shù)組長(zhǎng)度=====以上為頭部(Head)區(qū)域=====
實(shí)例數(shù)據(jù):類中定義的一些屬性等
對(duì)齊填充:在JDK里蛾洛,所有數(shù)據(jù)都是8Byte對(duì)齊。若一個(gè)對(duì)象數(shù)據(jù)為12Byte雁芙,則其后面必須填充4Byte轧膘,即12+4=16Byte 為8Byte的整數(shù)倍則只需要設(shè)置指針每次移動(dòng)9Byte,就可正確讀取內(nèi)存中的數(shù)據(jù)同理兔甘,Windows內(nèi)核中也是以4K對(duì)齊谎碍,稱之為頁(yè)(Page),以4M為大頁(yè)(Huge Page)為什么JVM要對(duì)齊填充洞焙?為了后面的指針壓縮
7.3 指針壓縮算法
開(kāi)啟指針壓縮的JVM參數(shù):-XX:+UseCompressedOops
64位地址分為堆的基地址+偏移量蟆淀,當(dāng)堆內(nèi)存<32GB時(shí)候,在壓縮過(guò)程中澡匪,把偏移量/8后保存到32位地址扳碍。在解壓再把32位地址放大8倍,所以啟用CompressedOops的條件是堆內(nèi)存要在4GB*8=32GB以內(nèi)仙蛉。
開(kāi)啟后笋敞,線性地址為4Byte,若不開(kāi)荠瘪,則為8Byte
指針壓縮在JDK6以后就是默認(rèn)開(kāi)啟的夯巷!若要關(guān)閉,則使用-XX:-UseCompressedOopsJVM參數(shù)
7.4 空對(duì)象大小
空對(duì)象是什么哀墓?空對(duì)象是沒(méi)有任何普通(靜態(tài))屬性的對(duì)象趁餐,而并非指指針為Null的對(duì)象。
class Test01{
inta=10;//int占4Byte篮绰,以及末尾的對(duì)齊后雷,所以并非空對(duì)象
}
public class Test{
public static void main(String[]args){
Test01 demo01 = new Test01();
Test01 demo02 = null;
? ? }
}
demo01和demo02并非空對(duì)象
public class Test02{
}
Test02為空對(duì)象
空對(duì)象占多少字節(jié)?
開(kāi)啟指針壓縮時(shí):16Byte16B = 8B【Mark Word】 + 4B【類型指針(Klass Pointer) 經(jīng)過(guò)指針壓縮優(yōu)化后變成4B】 + 0B【數(shù)組長(zhǎng)度】+ 0B【實(shí)際數(shù)據(jù)】 + 4B【對(duì)齊】調(diào)優(yōu)參數(shù)(開(kāi)啟指針壓縮參數(shù)):-XX:+UseCompressedOops
關(guān)閉指針壓縮時(shí):16Byte16B = 8B【Mark Word】 + 8B【類型指針(Klass Pointer)】 + 0B【數(shù)組長(zhǎng)度】+ 0B【實(shí)際數(shù)據(jù)】
7.5 普通對(duì)象大小
開(kāi)啟指針壓縮時(shí):32Byte32B= 8B【Mark Word】 + 4B【類型指針(Klass Pointer)】 + 0B【數(shù)組長(zhǎng)度】+ (4B【int大小】+4B【int大小】+8B【double大小】)【實(shí)際數(shù)據(jù)】 + 4B【對(duì)齊】
關(guān)閉指針壓縮時(shí):32Byte32B= 8B【Mark Word】 + 8B【類型指針(Klass Pointer)】 + 0B【數(shù)組長(zhǎng)度】+ (4B【int大小】+4B【int大小】+8B【double大小】)【實(shí)際數(shù)據(jù)】
7.6 數(shù)組對(duì)象大小
如關(guān)閉指針壓縮的圖所示吠各,在沒(méi)有開(kāi)啟指針壓縮的數(shù)組對(duì)象的對(duì)象頭中其實(shí)是存在著對(duì)齊填充的臀突。對(duì)齊大小仍為8B的整數(shù)倍。
8. 對(duì)象指針(OOP)
8.1 壓縮指針是怎么實(shí)現(xiàn)的
前提假設(shè):對(duì)象內(nèi)存從0x00000開(kāi)始贾漏,且連續(xù)候学,中間無(wú)其他數(shù)據(jù)
JVM的實(shí)現(xiàn)方式是 因?yàn)閷?duì)象大小都是8B的倍數(shù),所以存儲(chǔ)的內(nèi)存地址都是以000結(jié)尾的纵散,壓縮時(shí)梳码,可以將末尾的000去掉
如:0x10000 壓縮后——> 0x10
解壓的時(shí)候隐圾,直接在末尾添加三個(gè)0即可
8.2 如何擴(kuò)容OOP?
咋不關(guān)閉指針壓縮的情況下掰茶,如何擴(kuò)容OOP暇藏?
可以可以在地址末尾補(bǔ)0,則OOP的位從35位變成36位濒蒋,支持最大的類空間則變成2^36
為什么JVM不擴(kuò)容叨咖?因?yàn)樽詈笠晃谎a(bǔ)0會(huì)浪費(fèi)空間。
8.3 哪些信息會(huì)被壓縮啊胶?
1.對(duì)象的全局靜態(tài)變量(即類屬性) 2.對(duì)象頭信息:64位平臺(tái)下甸各,原生對(duì)象頭大小為16字節(jié),壓縮后為12字節(jié) 3.對(duì)象的引用類型:64位平臺(tái)下焰坪,引用類型本身大小為8字節(jié)趣倾,壓縮后為4字節(jié) 4.對(duì)象數(shù)組類型:64位平臺(tái)下,數(shù)組類型本身大小為24字節(jié)某饰,壓縮后16字節(jié)
8.4 哪些信息不會(huì)被壓縮儒恋?
1.指向非Heap的對(duì)象指針 2.局部變量、傳參黔漂、返回值诫尽、NULL指針
9. 虛擬機(jī)棧溢出
9.1 導(dǎo)致棧溢出的原因有哪些?
調(diào)用鏈過(guò)長(zhǎng)
死循環(huán)
無(wú)限遞歸
JDK默認(rèn)棧大小是多少炬守?1M
演示棧溢出代碼:(已手動(dòng)設(shè)置JVM棧大小為160K【因?yàn)镴VM最小就是160K牧嫉,不能少于160K】)
若將虛擬機(jī)大小設(shè)置為160k-Xss160k,經(jīng)過(guò)測(cè)試后减途,調(diào)用深度為772
則可計(jì)算出酣藻,棧幀大小為:160*1024 / 772 = 212
9.2 如何避免棧溢出?
在寫(xiě)遞歸/死循環(huán)的時(shí)候鳍置,一定要給遞歸/死循環(huán)一個(gè)出口辽剧。