以下內容都是我自己對本書讀后的一個理解加叁,感覺總結一下理解的更深刻哥力,如果能幫助到別人就更好了蔗怠,可能會有些內容說的不正確,如果發(fā)現(xiàn)了吩跋,希望幫忙留言指正寞射,謝謝。
背景
計算機的種類很多锌钮,硬件也都不太一樣桥温。比如Windows,Android梁丘,iOS等侵浸,而具體到某一個平臺,也分32位氛谜,64位等掏觉。想要編寫一個適用于眾多平臺的軟件比較困難,要考慮到各個平臺的兼容性值漫。Java虛擬機就提供了這么一個平臺澳腹,讓開發(fā)者只關心程序的功能開發(fā),兼容的工作就交給虛擬機即可。Java虛擬機做到了一次編譯酱塔,到處運行沥邻。
在JVM上跑一個程序的大概過程
基于JVM的語言除了Java還有Groovy,JRuby羊娃,Scala唐全,F(xiàn)antom,Jython等蕊玷。JVM支持的只有一種文件格式:.class文件邮利。基于JVM的語言集畅,不管語法是什么樣的近弟,脫去漂亮的衣服,最終都要編譯成.class文件格式挺智,才能在JVM虛擬機上運行祷愉。
.class文件結構
.class文件結構是固定這樣的,ux就代表x個字節(jié)的內容赦颇,.class文件結構中只有兩種數(shù)據(jù)內容:無符號數(shù)和表二鳄,表可以認為是一個數(shù)據(jù)結構,表中還是無符號數(shù)和表媒怯。
- magic订讼,代表該文件是個class文件
- mijor_version,major_version扇苞,版本號
- constant_pool_count欺殿,常量池中常量個數(shù)
-
cp_info,常量池鳖敷,常量池中的常量個數(shù)是constant_pool_count-1個脖苏,0號常量被空了出來。
常量池中類型:字面量和符號引用定踱。字面量是基礎類型棍潘,字符串,整型崖媚,浮點型這些亦歉。符號引用就是各字段(類的全局變量),方法的名稱及描述符畅哑,類和接口的全限定名肴楷,是以引用其他常量的方式存在,最終都會引用到字面量常量上去荠呐。
常量池項目類型:
常量池常量項的結構列表:
舉個例子:
//TestClass.java
package com.ejushang.TestClass;
public class TestClass implements Super{
private static final int staticVar = 0;
private int instanceVar=0;
public int instanceMethod(int param){
return param+1;
}
}
interface Super{ }
該Java文件編譯為class文件后:
我在"'深入理解Java虛擬機'2018-04-16"這個筆記中阶祭,手動將每一項內容都進行了分析绷杜,這里就直接用Javap -verbose TestClass輸出常量表
ConstantPool就是常量池,索引對應的是常量池的第幾個常量濒募。
-
access_flags,訪問標志圾结,表示是類還是接口瑰剃,public,abstract等等
- this_class筝野,super_class晌姚,interfaces_count,interfaces歇竟,這幾項確定了類的繼承關系
-
fields_count挥唠,fields,字段表焕议,包含屬性表
-
methods_count宝磨,methods,方法表盅安,包含有屬性表
-
attributes_count唤锉,attributes,屬性表
Code屬性:
Java程序方法體中的代碼經(jīng)過編譯别瞭,轉換成了字節(jié)碼指令存儲在Code屬性中窿祥。Code屬性中max_stack代表操作數(shù)棧深度,max_locals代表方法中局部變量所占Slot之和蝙寨,Slot是虛擬機為局部變量分配內存使用的最小單位晒衩。
code存儲的就是code_length個字節(jié)碼指令,字節(jié)碼指令表示你使用代碼要做的操作墙歪,比如new听系,+等等,字節(jié)碼指令可以跟隨參數(shù)箱亿,如果需要參數(shù)跛锌,字節(jié)碼后面跟隨的應該就是它的參數(shù)。
類加載
類從被加載到虛擬機內存中開始届惋,到卸載出內存為止髓帽,生命周期包括:加載,驗證脑豹,準備郑藏,解析,初始化瘩欺,使用和卸載必盖。
什么情況下類被加載拌牲?由虛擬機來自由把握。
- 加載歌粥,通過類的全限定名獲取到對應的clas文件塌忽,然后將類信息加載到JVM方法區(qū),再堆中實例化一個Class對象失驶,作為方法區(qū)中該類的入口
- 驗證土居,確認類型符合Java語言的語義,并且不會危及JVM的完整性
- 準備嬉探,為類變量(Static變量)分配內存擦耀,設置初值(通過內存清零實現(xiàn),此階段不執(zhí)行Java代碼)涩堤,final修飾的常量初始值就是Java代碼中的初始值
- 解析眷蜓,在類的常量池中尋找類,接口胎围,方法和字段的符號引用吁系,將符號引用替換為直接引用
- 初始化,為類變量賦予Java代碼中的初始值的過程痊远。初始化階段是執(zhí)行類構造器<clinit>()方法的過程垮抗,該方法是由編輯器自動收集類中的所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的。收集順序由源文件中出現(xiàn)順序決定碧聪。在子類的<clinit>()方法執(zhí)行前父類的<clinit>()方法已經(jīng)執(zhí)行完畢冒版。因此虛擬機中第一個被執(zhí)行<clinit>()方法的類肯定是java.lang.Object。
有且只有五種情況必須立即對類初始化
- 遇到new逞姿,getstatic辞嗡,putstatic,invokestatic這4條字節(jié)碼指令時
- 對一個類反射調用時
- 初始化一個類滞造,如果父類還沒初始化续室,則先初始化父類
- 用戶指定一個要執(zhí)行的主類(包含main()方法的那個類)
- 使用JDK1.7的動態(tài)語言支持時,如果java.lang.MethodHandler實例最后的解析結果REF_getStatic谒养,REF_putStatic挺狰,REF_invokeStatic的方法句柄,并且這個方法句柄所對應的類沒有進行過初始化买窟,需要先初始化
虛擬機字節(jié)碼執(zhí)行引擎
運行時棧幀結構丰泊,先了解下棧幀結構,為以后的運行代碼過程做鋪墊
虛擬機棧描述的是Java方法執(zhí)行的內存模型始绍。每個方法在執(zhí)行的同時都會創(chuàng)建一個棧幀瞳购,用于存儲局部變量表,操作數(shù)棧亏推,動態(tài)鏈接学赛,方法出口等信息年堆。
- 局部變量表,用于存儲方法參數(shù)和方法內部的局部變量
- 操作數(shù)棧盏浇,方法執(zhí)行過程中变丧,會有各種字節(jié)碼指令往操作數(shù)棧中寫入和提取內容,也就是出/入棧操作绢掰。我的理解锄贷,就是在內存中順序將指令寫在內存空間上,這個被寫指令的內存空間被叫做操作數(shù)棧曼月,這條指令寫完了,還有用就留著柔昼,繼續(xù)在下一個地址寫下一條指令哑芹,寫完如果不用了就清空,直到寫完最后一條指令捕透。不知道怎么畫個易懂的圖出來聪姿,如果有會畫出入棧這種圖的朋友,不吝賜教啊乙嘀。
- 動態(tài)鏈接末购,每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,應該就是方法區(qū)中類信息方法的地址吧虎谢。
- 方法返回地址盟榴,遇到方法返回的字節(jié)碼指令時,退出當前方法婴噩,把當前棧幀出棧擎场。棧幀中保存有PC計數(shù)器值,根據(jù)該值几莽,恢復上層方法迅办,把返回值(如果有)壓入調用者棧幀的操作數(shù)棧中。調整PC計數(shù)器指向方法調用的后一條指令章蚣。
對象的創(chuàng)建站欺,內存布局和訪問
- 對象的創(chuàng)建
當虛擬機遇到一個new指令時,首先去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的引用符號纤垂,并且檢查這個引用符號代表的類是否已經(jīng)被加載矾策,解析,初始化洒忧。如果沒有蝴韭,就先加載該類。接下來為新生對象分配內存熙侍。 - 對象的內存布局
對象在內存中的布局分為三塊區(qū)域榄鉴,對象頭履磨,實例數(shù)據(jù),對齊補充庆尘。
對象頭剃诅,分為兩部分,第一部分用來存儲自身的運行時數(shù)據(jù)驶忌,如哈希碼矛辕,GC分代年齡,鎖狀態(tài)標志付魔,線程持有的鎖聊品,偏向線程ID,偏向時間戳等几苍。第二部分是類型指針翻屈,指向他的類元數(shù)據(jù)的指針。通過這個指針確定對象是哪個類的實例妻坝。
實例數(shù)據(jù)伸眶,存儲的是字段內容,包括從父類繼承的和自己的刽宪。
對齊填充厘贼,對象大小必須是8的整數(shù)倍,沒有對齊的圣拄,通過對齊填充補全嘴秸。
目前問題
- 垃圾回收機制
判斷對象是否已死
引用計數(shù)法,對象中添加一個引用計數(shù)售担,每當有一個地方引用它趋惨,引用計數(shù)就加1仅胞。當引用失效時短条,就減1恭朗。計數(shù)器為0的對象就是不可能再被使用的。但是主流Java虛擬機沒有選用該方法來管理內存的哥攘。循環(huán)引用一個對象的話剖煌,該對象不會被回收。
可達性分析算法逝淹,當一個引用和GC Roots沒有引用鏈相連耕姊,則證明此對象可回收。GC Roots包括栅葡,虛擬機棧中引用茉兰,方法區(qū)靜態(tài)屬性引用,方法區(qū)常量引用欣簇,本地方法棧引用规脸。
引用
存儲著另一塊內存的起始地址坯约。分為強引用,軟引用莫鸭,弱引用闹丐,虛引用。
兩次標記
對象在可達性分析后被因,發(fā)現(xiàn)沒有與GC Roots相連卿拴,它將會被第一次標記并進行篩選,當對象沒有覆蓋finalize()方法梨与,或者finalize已經(jīng)被虛擬機調用過堕花,則會被回收。如果finalize中將引用與GC Roots關聯(lián)了粥鞋,則不會被回收航徙。
垃圾收集算法
標記-清除算法,最基礎的算法陷虎,效率不高,容易產(chǎn)生大量不連續(xù)的內存碎片
復制算法杠袱,將可用內存容量分為大小相等的兩塊尚猿,每次只用一塊,當一塊用完了楣富,就將還存活著的對象復制到另一塊上凿掂,然后將已使用的一次清理掉。現(xiàn)在的商業(yè)虛擬機都采用這種算法纹蝴,但并未按照1:1劃分內存空間庄萎,而是劃分為一塊較大的Eden和兩塊較小的Survivor,每次使用一塊Eden和其中一塊Survivor塘安。當回收時糠涛,將Eden和Survivor中還存活著的對象一次性復制到另一塊Survivor上,最后清理掉Eden和剛才用過的Survivor兼犯。Eden和Survivor的比例是8:1忍捡。
標記整理算法,分代收集算法 -
類加載雙親委派模型
- 內存溢出
在Java虛擬機規(guī)范中切黔,虛擬機棧有兩種異常情況:如果線程請求的棧深度大于虛擬機所允許的棧深度砸脊,將拋出StackOverflowError異常;如果虛擬機可以動態(tài)擴展纬霞,如果擴展到無法申請到足夠的內存凌埂,就會拋出OutOfMemoryError異常。
如果堆中沒有內存完成實例分配诗芜,并且堆也無法再擴展時瞳抓,將會拋出OutOfMemoryError異常埃疫。
根據(jù)虛擬機規(guī)范的規(guī)定,當方法區(qū)無法滿足內存分配需求時挨下,將拋出OutOfMemoryError異常熔恢。
Java程序從編譯到運行完成的過程
public class A{
int value;
public String getValue(){
return value;
}
}
public class Example{
public static void main(String args[]){
A a = new A();
a.getValue();
}
}
1.Java代碼編譯為class文件,class中包含常量池臭笆,字段表叙淌,方法表,屬性表等愁铺;
2.加載(虛擬機自己定義加載時機)鹰霍,將class文件加載到方法區(qū),在堆中實例化一個Class對象茵乱,做為方法區(qū)中該類的入口茂洒;
驗證;準備瓶竭,為類變量分配內存督勺,設置初值0;
解析斤贰,將常量池中符號引用替換為直接引用智哀;
初始化,給類變量賦真正的初始值荧恍;
3.開始執(zhí)行main方法瓷叫,根據(jù)Example的全限定名在方法區(qū)中找到Example,找到name是main的方法送巡,創(chuàng)建main方法棧幀摹菠,將參數(shù)args,對象引用a存儲到main棧幀的局部變量表中骗爆,然后將new指令入棧(操作數(shù)棧)次氨,將參數(shù)A入棧,在堆中保存a對象實例摘投。接下來方法調用指令入棧糟需,參數(shù)入棧,參數(shù)中有a和getValue谷朝,可以根據(jù)這些內容找到方法區(qū)中A類getValue方法的地址洲押,然后運行該方法,創(chuàng)建getValue方法棧幀圆凰,將return指令入棧(getValue的操作數(shù)棧)杈帐,參數(shù)value入棧,該方法執(zhí)行結束,value出棧挑童,return指令出棧累铅。根據(jù)程序計數(shù)器,跳回剛才的main棧幀的操作數(shù)棧中的位置站叼。退出指令入棧娃兽,退出指令出棧,其他內容逐一出棧尽楔。OK投储,程序結束。