您好,登錄后才能下訂單哦!
學(xué)習(xí)java虛擬機已經(jīng)很久了,最近有空,于是將我所知道的一些關(guān)于java虛擬機的知識寫出來。首先當(dāng)做是重新復(fù)習(xí)一下,其次是給想了解java虛擬機的朋友一些參考。筆記內(nèi)容大量參看《深入理解java虛擬機》這本書。
一、虛擬機內(nèi)存組成模塊
java虛擬機規(guī)范中規(guī)定了以下組成部分:程序計數(shù)器、虛擬機方法棧、本地方法棧(Hotspot中將虛擬機方法棧和本地方法棧合并成方法棧)、java堆、方法區(qū)(java8以后將方法區(qū)移到了虛擬機外)、運行常量池。
另外java虛擬機還可以額外分配直接內(nèi)存,不過這不屬于java虛擬機內(nèi)存組成。整體組成如下圖:
程序計數(shù)器
java虛擬機之所以被稱為虛擬機是因為它模仿物理機運行實現(xiàn)的,它的程序計數(shù)器也類似于操作系統(tǒng)中的程序計數(shù)器,是線程私有的,作用是存儲線程將要執(zhí)行的下一個操作指令(java模仿物理機,也自己實現(xiàn)了多種操作指令)。程序計數(shù)器只占用了一小塊內(nèi)存區(qū)域。
方法棧和本地方法棧
java中每一次方法調(diào)用都對應(yīng)了方法棧的進(jìn)棧和出站操作,方法棧中每一個棧幀都對應(yīng)著java代碼中相應(yīng)的方法調(diào)用,棧幀中局部變量表存儲了基礎(chǔ)數(shù)據(jù)類型(boolean、byte、char、int、long、float、double、)和reference(reference包括兩種:句柄和指針,各自有各自的好處,使用句柄則在改變對象位置時不改變局部變量表里的引用只用改變句柄本身的指針即可,指針的優(yōu)點則是查詢效率快)。
這里有個知識點,實際上Java中的數(shù)組是Java虛擬機動態(tài)生成的一個對象,不屬于基礎(chǔ)數(shù)據(jù)類型,我們常用的數(shù)組的length屬性其實就是它的對象的一個public屬性。
java堆
java堆是虛擬機中最大的內(nèi)存組成部分,用來存儲程序執(zhí)行中產(chǎn)生的對象(不包括常量、靜態(tài)常量引用的對象)。java堆會因為垃圾回收以及對應(yīng)的垃圾回收器的不同而采用不用的劃分方式,但整體還是劃分為新生代和老年代兩個部分。新生代又分為eden區(qū)(伊甸區(qū))和survivor區(qū)域(幸存區(qū)域)。java默認(rèn)Eden區(qū)域是survivor區(qū)域的8倍大?。ɡ厥諒?fù)制算法執(zhí)行過程統(tǒng)計出來的合適倍數(shù))。不過存在survivor區(qū)域又分為兩塊相同大小的survivor區(qū)域:from?survivor區(qū)域和to?survivor區(qū)域,作為輪轉(zhuǎn)備用。簡單的說,java程序運行中對象就是Eden區(qū)域survivor區(qū)域和老年代中創(chuàng)建、清理、復(fù)制、整理。
方法區(qū)
方法區(qū)用于存儲虛擬機加載的類信息、常量、靜態(tài)常量等,也被稱為永久代。Hotspot在java8之前用永久代來實現(xiàn)方法區(qū),java8后永久代被移出虛擬機內(nèi)存,使用native?memory存儲。
運行時常量池
運行時常量池屬于方法區(qū)的一部分,用來存儲常量的值,存儲內(nèi)容分為兩種:字面量和符號引用。這個的理解需要結(jié)合Class類的前端編譯來解讀。在虛擬機加載類的時候比如類的名字、字段的名字、常量等的值需要存儲下來,而且會頻繁使用。Java中的基本數(shù)據(jù)類型和String類型都可以在虛擬機加載類的時候理解為虛擬機可以描述的值,并不是程序員自己定義的對象。這些值是需要并可以存儲在虛擬機中并供后期使用的,這些值便是運行時常量池中的字面量。另外如string.intern()方法也可以在運行期間將一個String的值放到常量池中并返回常量池的引用,只不過這個不是在虛擬機加載類時候放入的。常量池中另一種數(shù)據(jù)類型是符號引用,這個跟class的結(jié)構(gòu)也是相關(guān)的,前端編譯階段,對class結(jié)構(gòu)的描述過程中,一個字面量是可以反復(fù)被使用的,于是便可以給字面量編一個索引,在符號引用中引用這個索引去得到值,當(dāng)然,符號引用本身也會被索引供其他符號引用使用。這個便是常量池中的內(nèi)容,需要結(jié)合對class結(jié)構(gòu)的了解才能更好的理解為什么會有常量池以及常量池中 存儲的內(nèi)容,不能錯誤的直接理解為我們在開發(fā)時在class中自己定義的 “常量”,它包含了我們通常理解的“常量”,但遠(yuǎn)不止如此。另外,對于我自己常說的自己定義的“常量”,只有static 和 final修飾的基礎(chǔ)類型和string類型才屬于constantvalue,對象不屬于constantvalue。對象的內(nèi)存是分配在Java堆中,常量是分配在方法區(qū)中的運行時常量池中的。舉一個常量的特殊性的例子:如在使用ClassA.CONSTANT_VALUEA時,這個時候虛擬機使用的是常量,假如這個時候ClassA還沒有被加載,使用這個ClassA.CONSTANT_VALUEA的值時是不會觸發(fā)ClassA的加載的。
直接內(nèi)存
java直接allocat出來的內(nèi)存。NIO使用的緩沖區(qū)就是直接內(nèi)存。
二、虛擬機的垃圾回收
虛擬機的垃圾回收基本可等同于對Java堆的垃圾回收。
虛擬機中判斷對象是否死亡的算法——可達(dá)性算法
可達(dá)性算法的描述非常簡單 :對象是否被GC Roots所直接引用,是則存活;是否被GC Roots直接引用的對象所直接或通過其他對象間接引用,是則存活;不滿足則被標(biāo)記為死亡。
以下是從網(wǎng)上找的可達(dá)性算法示意:
GC Roots包含:
1.虛擬機棧
2.方法區(qū)中的靜態(tài)屬性
3.方法區(qū)中的常量
4.本地方法棧
對象的finalize方法
finalize方法經(jīng)常會在面試中被問到,它提供了類似C/C++中析構(gòu)函數(shù)的功能,當(dāng)Java中的對象將要被回收時,如果對象有重寫finalize方法,那么finalize方法將會被調(diào)用一次,當(dāng)?shù)诙我换厥諘r則不會被觸發(fā)調(diào)用。我們可以嘗試在finalize方法中拯救對象本身不被虛擬機回收,例如將對象被GC Roots引用,那樣便可以使對象免于被回收。但是finalize方法并不能一定保證這種操作一定能成功,成功的關(guān)鍵在于finalize方法中的代碼執(zhí)行的要比虛擬機垃圾回收要快,因此finalize方法中拯救對象本身不具備確定性。finalize方法所Java早期為贏得使用者的產(chǎn)物,建議不使用,它完全能被finally和其他方式代替。
垃圾回收算法——標(biāo)記清除算法
下圖是從網(wǎng)上找的標(biāo)記清除算法的示意圖,其原理非常簡單:首先對對象的可達(dá)性進(jìn)行標(biāo)記,然后清除掉不可達(dá)的對象。
標(biāo)記清除的算法的問題是清除之后留下的可用的存儲空間非常零碎,當(dāng)我們需要一個比較大的存儲空間來存儲大對象時,這將是個災(zāi)難。虛擬機不直接使用標(biāo)記清除算法來回收垃圾,但是標(biāo)記清除算法是其他優(yōu)化過的算法的基礎(chǔ)。
垃圾回收算法——復(fù)制算法
復(fù)制算法的原理也很簡單:將內(nèi)存劃分為兩塊相同大小的區(qū)域,只使用其中一塊,當(dāng)進(jìn)行垃圾回收時,將還存活的對象移至另一塊內(nèi)存中,本身則全部清除掉,這樣就不會產(chǎn)生內(nèi)存碎片。
下圖是復(fù)制算法的示意圖,圖片來自網(wǎng)上:
我們可以看出,復(fù)制算法是基于標(biāo)記清除算法的思想進(jìn)行的,復(fù)制算法的缺陷是浪費了太多內(nèi)存,Java虛擬機使用復(fù)制算法時當(dāng)然不會直接這樣去做。實際上復(fù)制算法是java堆中新生代的基本算法思想(實際上并沒有這么直接使用)。
Java虛擬機根據(jù)對象的存活時間不同的特點將Java堆分成新生代和老年代。新生代的對象“朝生夕死”,存活時間短,內(nèi)存重新分配頻繁,適合使用復(fù)制算法進(jìn)行垃圾回收。Java虛擬機將新生代劃分為eden區(qū)和survivor區(qū)(survivor區(qū)域有兩塊,一塊from區(qū)域,一塊to區(qū)域,輪轉(zhuǎn)備用),對應(yīng)復(fù)制算法需要的兩塊內(nèi)存區(qū)域。因為經(jīng)過垃圾回收后剩下的對象其實是少數(shù),所以survivor區(qū)域并不需要和eden區(qū)域一樣大,那樣太浪費內(nèi)存空間,虛擬機默認(rèn)的大小是eden區(qū)域是survivor區(qū)域的8倍大小,虛擬機啟動時支持配置。
另外,復(fù)制算法只是基礎(chǔ),虛擬的不同回收器實際執(zhí)行時還進(jìn)行了優(yōu)化。
垃圾回收算法——標(biāo)記整理算法
標(biāo)記整理算法也是基于標(biāo)記清除算法實現(xiàn)的,不同點是在標(biāo)記之后不是將對象直接清除,而是將存活對象前移,清除存活對象內(nèi)存空間之外的內(nèi)存空間。
下圖也是從網(wǎng)上找的示意圖:
當(dāng)內(nèi)存大對象多,且對象頻繁產(chǎn)生死亡的時候,效率是非常低下的,因此不適合新生代的垃圾回收。但是老年代的對象存活率高,內(nèi)存相對較小,很適合標(biāo)記整理算法。
三種算法總結(jié):標(biāo)記清除算法是其他兩種算法以及其他優(yōu)化過的垃圾回收算法的基礎(chǔ),復(fù)制算法適用于新生代,標(biāo)記整理算法適用于老年代,實際上,Java虛擬機也確實是分代進(jìn)行垃圾回收的。
概念——STW
STW:stop the world。Java虛擬機進(jìn)行垃圾回收時是需要中斷工作線程的執(zhí)行的,期間Java程序出現(xiàn)了短暫的停頓。當(dāng)然,現(xiàn)在虛擬機對垃圾回收的不斷優(yōu)化,幾乎可以忽略STW時間了。
垃圾回收器——CMS(current mark sweep)回收器
從名字就可以看出CMS回收器是基于標(biāo)記清除算法的回收器,它的運作過程分為四步驟:
1.初始標(biāo)記
初始標(biāo)記的作用是標(biāo)記出那些被GC Roots直接引用的對象。這個期間或產(chǎn)生短暫的STW時間
2.并發(fā)標(biāo)記
并發(fā)標(biāo)記是同時和用戶線程執(zhí)行的,標(biāo)記出被所有被引用的對象。不會產(chǎn)生STW。
3.重新標(biāo)記
并發(fā)標(biāo)記的時間相對長一些,這個期間可能用于用戶線程的操作,并發(fā)標(biāo)記的結(jié)果可能已經(jīng)跟實際產(chǎn)生了偏差,重新標(biāo)記便是糾正這個偏差的。期間會停止用戶線程,產(chǎn)生STW。
4.并發(fā)清除
并發(fā)清除就很好理解了,就是垃圾的清除工作是和用戶線程一起進(jìn)行的,不會導(dǎo)致用戶線程的停頓。
在進(jìn)行以上四步后并不能保證所有的垃圾都被清除掉了,因為用戶線程是在并發(fā)進(jìn)行的。遺漏的垃圾對象需要依賴于下次垃圾回收進(jìn)行清除。
CMS回收器是多線程并發(fā)執(zhí)行的,因此是對CPU敏感的,比較占用CPU資源。
CMS回收器因為是基于標(biāo)記清除算法的,單純的進(jìn)行這種算法也會產(chǎn)生內(nèi)存碎片。當(dāng)無法分配大的內(nèi)存空間時,會導(dǎo)致Full GC來整理內(nèi)存空間。
垃圾回收器——G1回收器
G1回收器和CMS在過程上有很多類似之處,只是稍有不同,但是兩個回收器的目的和實現(xiàn)方式時完全不一樣的。
G1回收器更專注于對于CPU資源的使用,充分發(fā)揮現(xiàn)代多核超線程CPU的優(yōu)勢。G1收集器不能確切的劃分為標(biāo)記清除算法、復(fù)制算法或者標(biāo)記整理算法,它在原來新生代老年代的基礎(chǔ)上將內(nèi)存劃分為多個區(qū)域Region,新生代和老年代都是由多個Region組成的集合。同時,它會跟各個Region垃圾的多少對各個Region進(jìn)行優(yōu)先級劃分,這種將內(nèi)存化整為零的做法避免來對全部內(nèi)存的操作。
G1回收器的實現(xiàn)細(xì)節(jié)遠(yuǎn)比上面描述的要復(fù)雜,但是其過程也可以劃分為以下四步:
1.初始標(biāo)記
2.并發(fā)標(biāo)記
3.最終標(biāo)記
4.篩選回收
前面3個步驟都和CMS很類似,只不過是分Region進(jìn)行的,篩選回收則是對所有Region進(jìn)行篩選,只選擇對那些有必要的Region進(jìn)行垃圾回收。
Minor GC 和 Major GC / Full GC
這三種稱呼其實有點混亂,而且也只是對Java虛擬機垃圾回收的一種思考角度,不能代表虛擬機的垃圾回收算法的劃分。
Minor GC 和 Major GC、Full GC的分界還是很清晰的。Minor GC是指對年新生代的垃圾回收動作,它的執(zhí)行頻率非常頻繁,回收速度也比較快。
Major GC是指對老年代的劃分,一般會伴隨一次Minor GC,一般速度較慢。Full GC可以理解為對整個堆的垃圾回收,其實和Major GC語意有點重復(fù),它的另一個語意是產(chǎn)生了STW。
對象的一生
我們現(xiàn)在已經(jīng)知道,從整體來說,對象是被分配在新生代和老年代中,新生代又被分為eden區(qū)域和survivo區(qū)域。當(dāng)一個創(chuàng)建時,它優(yōu)先是被分配在新生代的eden區(qū)域的,但是大的對象(默認(rèn)3M,可以在JVM啟動時設(shè)置)直接會被分配到老年代。JVM會為每個對象的“年齡”計數(shù)。當(dāng)存在于eden區(qū)域的對象經(jīng)歷過一次垃圾回收后,它就被移到survivor區(qū)域,同時它的年齡就被+1,當(dāng)它的年齡達(dá)到15(虛擬機啟動時可通過參數(shù)配置)的時候就會被移到老年代。另外,虛擬機會survivor區(qū)域的大小是否充足,如果內(nèi)存不足,對象也將直接移至老年代。
三、類文件結(jié)構(gòu)
在分析類文件結(jié)構(gòu)前,我們先寫一個簡單的類:
package?me.wxh.clazzstd; public?class?TestClass?{ ????private?int?m; ????public?static?String?CLASS_VARIABLE?=?"我是類變量"; ????public?final?static?String?CONSTANT_VALUE?=?"我才是常量"; ????public?final?static?int?CONSTANT_INT?=?1; ????public?int?inc()?{ ????????return?m?+?1; ????} ????public?static?void?main(String[]?args)?throws?Exception{ ????????System.out.println(CLASS_VARIABLE); ????????System.out.println(CONSTANT_VALUE); ????????System.out.println(CONSTANT_INT); ????????catInt("wuxuehai"); ????} ????public?static?Integer?catInt(String?intValue)?throws?Exception{ ????????try?{ ????????????return?Integer.parseInt(intValue); ????????} ????????catch?(NumberFormatException?e)?{ ????????????return?0; ????????} ????????finally?{ ????????????System.out.println("finally?塊執(zhí)行"); ????????} ????} }
然后我們使用javap -verbose 命令查看它的class文件結(jié)構(gòu),如下:
/System/Library/Frameworks/JavaVM.framework/Versions/A/Commands/javap?-verbose?TestClass.class Classfile?/Users/wuxuehai/IdeaProjects/algorithm/target/classes/me/wxh/clazzstd/TestClass.class ??Last?modified?2019-4-24;?size?1459?bytes ??MD5?checksum?fdc48a22d072179c43e64b1a57226ef1 ??Compiled?from?"TestClass.java" public?class?me.wxh.clazzstd.TestClass ??minor?version:?0 ??major?version:?49 ??flags:?ACC_PUBLIC,?ACC_SUPER Constant?pool: ???#1?=?Methodref??????????#16.#48????????//?java/lang/Object."<init>":()V ???#2?=?Fieldref???????????#6.#49?????????//?me/wxh/clazzstd/TestClass.m:I ???#3?=?Fieldref???????????#50.#51????????//?java/lang/System.out:Ljava/io/PrintStream; ???#4?=?Fieldref???????????#6.#52?????????//?me/wxh/clazzstd/TestClass.CLASS_VARIABLE:Ljava/lang/String; ???#5?=?Methodref??????????#53.#54????????//?java/io/PrintStream.println:(Ljava/lang/String;)V ???#6?=?Class??????????????#55????????????//?me/wxh/clazzstd/TestClass ???#7?=?String?????????????#56????????????//?我才是常量 ???#8?=?Methodref??????????#53.#57????????//?java/io/PrintStream.println:(I)V ???#9?=?String?????????????#58????????????//?wuxuehai ??#10?=?Methodref??????????#6.#59?????????//?me/wxh/clazzstd/TestClass.catInt:(Ljava/lang/String;)Ljava/lang/Integer; ??#11?=?Methodref??????????#60.#61????????//?java/lang/Integer.parseInt:(Ljava/lang/String;)I ??#12?=?Methodref??????????#60.#62????????//?java/lang/Integer.valueOf:(I)Ljava/lang/Integer; ??#13?=?String?????????????#63????????????//?finally?塊執(zhí)行 ??#14?=?Class??????????????#64????????????//?java/lang/NumberFormatException ??#15?=?String?????????????#65????????????//?我是類變量 ??#16?=?Class??????????????#66????????????//?java/lang/Object ??#17?=?Utf8???????????????m ??#18?=?Utf8???????????????I ??#19?=?Utf8???????????????CLASS_VARIABLE ??#20?=?Utf8???????????????Ljava/lang/String; ??#21?=?Utf8???????????????CONSTANT_VALUE ??#22?=?Utf8???????????????ConstantValue ??#23?=?Utf8???????????????CONSTANT_INT ??#24?=?Integer????????????1 ??#25?=?Utf8???????????????<init> ??#26?=?Utf8???????????????()V ??#27?=?Utf8???????????????Code ??#28?=?Utf8???????????????LineNumberTable ??#29?=?Utf8???????????????LocalVariableTable ??#30?=?Utf8???????????????this ??#31?=?Utf8???????????????Lme/wxh/clazzstd/TestClass; ??#32?=?Utf8???????????????inc ??#33?=?Utf8???????????????()I ??#34?=?Utf8???????????????main ??#35?=?Utf8???????????????([Ljava/lang/String;)V ??#36?=?Utf8???????????????args ??#37?=?Utf8???????????????[Ljava/lang/String; ??#38?=?Utf8???????????????Exceptions ??#39?=?Class??????????????#67????????????//?java/lang/Exception ??#40?=?Utf8???????????????catInt ??#41?=?Utf8???????????????(Ljava/lang/String;)Ljava/lang/Integer; ??#42?=?Utf8???????????????e ??#43?=?Utf8???????????????Ljava/lang/NumberFormatException; ??#44?=?Utf8???????????????intValue ??#45?=?Utf8???????????????<clinit> ??#46?=?Utf8???????????????SourceFile ??#47?=?Utf8???????????????TestClass.java ??#48?=?NameAndType????????#25:#26????????//?"<init>":()V ??#49?=?NameAndType????????#17:#18????????//?m:I ??#50?=?Class??????????????#68????????????//?java/lang/System ??#51?=?NameAndType????????#69:#70????????//?out:Ljava/io/PrintStream; ??#52?=?NameAndType????????#19:#20????????//?CLASS_VARIABLE:Ljava/lang/String; ??#53?=?Class??????????????#71????????????//?java/io/PrintStream ??#54?=?NameAndType????????#72:#73????????//?println:(Ljava/lang/String;)V ??#55?=?Utf8???????????????me/wxh/clazzstd/TestClass ??#56?=?Utf8???????????????我才是常量 ??#57?=?NameAndType????????#72:#74????????//?println:(I)V ??#58?=?Utf8???????????????wuxuehai ??#59?=?NameAndType????????#40:#41????????//?catInt:(Ljava/lang/String;)Ljava/lang/Integer; ??#60?=?Class??????????????#75????????????//?java/lang/Integer ??#61?=?NameAndType????????#76:#77????????//?parseInt:(Ljava/lang/String;)I ??#62?=?NameAndType????????#78:#79????????//?valueOf:(I)Ljava/lang/Integer; ??#63?=?Utf8???????????????finally?塊執(zhí)行 ??#64?=?Utf8???????????????java/lang/NumberFormatException ??#65?=?Utf8???????????????我是類變量 ??#66?=?Utf8???????????????java/lang/Object ??#67?=?Utf8???????????????java/lang/Exception ??#68?=?Utf8???????????????java/lang/System ??#69?=?Utf8???????????????out ??#70?=?Utf8???????????????Ljava/io/PrintStream; ??#71?=?Utf8???????????????java/io/PrintStream ??#72?=?Utf8???????????????println ??#73?=?Utf8???????????????(Ljava/lang/String;)V ??#74?=?Utf8???????????????(I)V ??#75?=?Utf8???????????????java/lang/Integer ??#76?=?Utf8???????????????parseInt ??#77?=?Utf8???????????????(Ljava/lang/String;)I ??#78?=?Utf8???????????????valueOf ??#79?=?Utf8???????????????(I)Ljava/lang/Integer; { ??public?static?java.lang.String?CLASS_VARIABLE; ????descriptor:?Ljava/lang/String; ????flags:?ACC_PUBLIC,?ACC_STATIC ??public?static?final?java.lang.String?CONSTANT_VALUE; ????descriptor:?Ljava/lang/String; ????flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_FINAL ????ConstantValue:?String?我才是常量 ??public?static?final?int?CONSTANT_INT; ????descriptor:?I ????flags:?ACC_PUBLIC,?ACC_STATIC,?ACC_FINAL ????ConstantValue:?int?1 ??public?me.wxh.clazzstd.TestClass(); ????descriptor:?()V ????flags:?ACC_PUBLIC ????Code: ??????stack=1,?locals=1,?args_size=1 ?????????0:?aload_0 ?????????1:?invokespecial?#1??????????????????//?Method?java/lang/Object."<init>":()V ?????????4:?return ??????LineNumberTable: ????????line?3:?0 ??????LocalVariableTable: ????????Start??Length??Slot??Name???Signature ????????????0???????5?????0??this???Lme/wxh/clazzstd/TestClass; ??public?int?inc(); ????descriptor:?()I ????flags:?ACC_PUBLIC ????Code: ??????stack=2,?locals=1,?args_size=1 ?????????0:?aload_0 ?????????1:?getfield??????#2??????????????????//?Field?m:I ?????????4:?iconst_1 ?????????5:?iadd ?????????6:?ireturn ??????LineNumberTable: ????????line?14:?0 ??????LocalVariableTable: ????????Start??Length??Slot??Name???Signature ????????????0???????7?????0??this???Lme/wxh/clazzstd/TestClass; ??public?static?void?main(java.lang.String[])?throws?java.lang.Exception; ????descriptor:?([Ljava/lang/String;)V ????flags:?ACC_PUBLIC,?ACC_STATIC ????Code: ??????stack=2,?locals=1,?args_size=1 ?????????0:?getstatic?????#3??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ?????????3:?getstatic?????#4??????????????????//?Field?CLASS_VARIABLE:Ljava/lang/String; ?????????6:?invokevirtual?#5??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ?????????9:?getstatic?????#3??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ????????12:?ldc???????????#7??????????????????//?String?我才是常量 ????????14:?invokevirtual?#5??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ????????17:?getstatic?????#3??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ????????20:?iconst_1 ????????21:?invokevirtual?#8??????????????????//?Method?java/io/PrintStream.println:(I)V ????????24:?ldc???????????#9??????????????????//?String?wuxuehai ????????26:?invokestatic??#10?????????????????//?Method?catInt:(Ljava/lang/String;)Ljava/lang/Integer; ????????29:?pop ????????30:?return ??????LineNumberTable: ????????line?18:?0 ????????line?19:?9 ????????line?20:?17 ????????line?21:?24 ????????line?22:?30 ??????LocalVariableTable: ????????Start??Length??Slot??Name???Signature ????????????0??????31?????0??args???[Ljava/lang/String; ????Exceptions: ??????throws?java.lang.Exception ??public?static?java.lang.Integer?catInt(java.lang.String)?throws?java.lang.Exception; ????descriptor:?(Ljava/lang/String;)Ljava/lang/Integer; ????flags:?ACC_PUBLIC,?ACC_STATIC ????Code: ??????stack=2,?locals=4,?args_size=1 ?????????0:?aload_0 ?????????1:?invokestatic??#11?????????????????//?Method?java/lang/Integer.parseInt:(Ljava/lang/String;)I ?????????4:?invokestatic??#12?????????????????//?Method?java/lang/Integer.valueOf:(I)Ljava/lang/Integer; ?????????7:?astore_1 ?????????8:?getstatic?????#3??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ????????11:?ldc???????????#13?????????????????//?String?finally?塊執(zhí)行 ????????13:?invokevirtual?#5??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ????????16:?aload_1 ????????17:?areturn ????????18:?astore_1 ????????19:?iconst_0 ????????20:?invokestatic??#12?????????????????//?Method?java/lang/Integer.valueOf:(I)Ljava/lang/Integer; ????????23:?astore_2 ????????24:?getstatic?????#3??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ????????27:?ldc???????????#13?????????????????//?String?finally?塊執(zhí)行 ????????29:?invokevirtual?#5??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ????????32:?aload_2 ????????33:?areturn ????????34:?astore_3 ????????35:?getstatic?????#3??????????????????//?Field?java/lang/System.out:Ljava/io/PrintStream; ????????38:?ldc???????????#13?????????????????//?String?finally?塊執(zhí)行 ????????40:?invokevirtual?#5??????????????????//?Method?java/io/PrintStream.println:(Ljava/lang/String;)V ????????43:?aload_3 ????????44:?athrow ??????Exception?table: ?????????from????to??target?type ?????????????0?????8????18???Class?java/lang/NumberFormatException ?????????????0?????8????34???any ????????????18????24????34???any ??????LineNumberTable: ????????line?26:?0 ????????line?32:?8 ????????line?26:?16 ????????line?28:?18 ????????line?29:?19 ????????line?32:?24 ????????line?29:?32 ????????line?32:?34 ????????line?33:?43 ??????LocalVariableTable: ????????Start??Length??Slot??Name???Signature ???????????19??????15?????1?????e???Ljava/lang/NumberFormatException; ????????????0??????45?????0?intValue???Ljava/lang/String; ????Exceptions: ??????throws?java.lang.Exception ??static?{}; ????descriptor:?()V ????flags:?ACC_STATIC ????Code: ??????stack=1,?locals=0,?args_size=0 ?????????0:?ldc???????????#15?????????????????//?String?我是類變量 ?????????2:?putstatic?????#4??????????????????//?Field?CLASS_VARIABLE:Ljava/lang/String; ?????????5:?return ??????LineNumberTable: ????????line?7:?0 } SourceFile:?"TestClass.java"
接下開,我們利用這兩個文件來簡單解釋下類文件的結(jié)構(gòu),順便會涉及到部分class加載的過程解析和虛擬機內(nèi)存模型的知識。實際上,我們開發(fā)過程中并不太可能需要去閱讀class文件,但了解class文件的結(jié)構(gòu)有助于我們理解和驗證Java虛擬機的執(zhí)行的過程結(jié)構(gòu)。
常量池——constant pool
類的開始是一些基礎(chǔ)的描述,相信并不需要過多的解讀,真正需要理解的地方便是從constant pool這里開始,constant pool其實便是我經(jīng)常所說的常量池。
下面我們來解讀下常量池里內(nèi)容。
常量池中每一行便是一個常量,最左邊的#1、#2、#3……是常量的索引。常量分為字面量和符號引用兩種,符號引用會引用其他符號引用和字面量最終也可以解析成一個固定格式的值。
?“=” 號后的值如“Utf8”、“NameAndType”等都是常量的類型描述,常量的類型有很多種,需要了解更多的可結(jié)合資料和書籍去了解,我們只能注重于理解。在這里舉一個例子:索引#7的常量是一個String類型的常量,它的第三列是#56,這時我們看第二張圖,索引#56的常量是一個“Utf8”類型的常量,表示一個Uft8編碼的文本,也就是我們代碼里的“public final static String CONSTANT_VALUE = "我才是常量”;”這行中的值,這里編譯器是把“我才是常量”這個值生成了一個字面量,然后被#7引用,定義成了一個String類型的符號引用。
第三列是常量的值,如果是字面量,則會是“我才是常量”、1….這樣的值,如果是符號引用,則會是對其他常量的索引引用。
最后“//”后面的是對常量的注釋,如果是字面量,則沒有這一列,如果是符號引用則備注了符號引用的實際值。
常量池在虛擬機中非常重要,javac編譯(前端編譯)出的字節(jié)碼中代碼中大量引用到常量池中的值,如我們的代碼中:
這里的字節(jié)碼指令中#3、#4….等等都是對常量池中的常量的引用。而前端編譯是為了后面的類加載提供的基礎(chǔ)的。
字段表
字段表緊跟常量池之后,包含了我們在類中定義的類變量和實例變量,在我們的代碼中如下:
我們可以看到它描述了我們定義的CLASS_VARIABLE、CONSTANT_VALUE、CONSTANT_INT 這三個字段,表示出了它們的訪問權(quán)限、類型、返回值等等。同時在我們的常量池中,我們也可以看到它們的字段名被分別定義成#19、#21、#23這三個Utf8常量。
比較下CLASS_VARIABLE和CONSTANT_VALUE、CONSTANT_INT的區(qū)別,我們可以發(fā)現(xiàn)后面兩個變量多了一個ConstantValue屬性,這就是我們之前在介紹虛擬機內(nèi)存組成模塊時介紹常量池時所說的,只有同時被static和final修飾的才是常量,這一點很重要,常量的賦值是虛擬機自動執(zhí)行,而類變量的賦值是在<clinit>(類初始化)方法中執(zhí)行,實例變量是在<init>(對象初始化)方法中執(zhí)行,這就導(dǎo)致我們在其他類中使用常量時,不會觸發(fā)類的加載。
方法表
方法表存在于字段表之后,我們可以注意到,正如我們才學(xué)Java時所知道的一樣,當(dāng)我們沒有寫構(gòu)造方法時,編譯器會為我們默認(rèn)實現(xiàn)一個構(gòu)造方法(雖然當(dāng)時不知道原理,但確實是這樣的):
下面我們用我們代碼中的main函數(shù)來解析下方法表中方法的構(gòu)造。main函數(shù)的代碼如下:
它經(jīng)過前端編譯后的代碼如下:
我們來對應(yīng)著看,首先在字節(jié)碼的最上部描述出了方法的名稱、參數(shù)、返回信息、拋出的異常、訪問權(quán)限等等,這些都非常的直觀,我們不多做贅述。
Code屬性
Code屬性是方法表里核心信息,它將我們方法里的代碼描述成Java的字節(jié)碼指令,然后在虛擬機中執(zhí)行這些指令便是我們代碼的執(zhí)行(實際上還需要翻譯成匯編指令,翻譯行為又分為解釋執(zhí)行和編譯執(zhí)行兩種)。指令后不帶參數(shù)的都是對操作數(shù)棧(后面會解釋到)棧頂元素的操作,帶參數(shù)的指令需要結(jié)合常量池的索引翻譯成完整的指令。invoke*這樣的指令是對方法的調(diào)用,不過方法分很多種,invokevirtual指令是帶參數(shù)的,但也就是對棧頂對象實例方法的調(diào)用,調(diào)用棧頂實例的指定方法(我們這里是System.out的println方法,System.out就是我們的棧頂?shù)膶嵗?span >這里又個很重要的知識點,invokevirtual這樣的調(diào)用形式,雖然我們的指令形式都是一樣的,但是我們的棧頂對象是可變的,如果我們父類和子類都有同樣名字的方法,那么在棧頂?shù)膶ο笫歉割愡€是子類將決定我們調(diào)用的實際方法的不同,實際上,這便是Java中多態(tài)的實現(xiàn)基礎(chǔ)!
簡單介紹下這里的指令:
getstatic? ? 訪問類字段
invokevirtual?? ?? 調(diào)用虛方法,這只是方法調(diào)用的一種,后面我們會知道所有的方法調(diào)用指令。
invokestatic? ? 調(diào)用靜態(tài)方法
iconst_1? ? 將int類型的常量1加載到操作數(shù)棧
ldc? ? 將一個常量加載到操作數(shù)棧
return? ? 方法返回void
LineNumberTable
不知道大家有沒有想過這樣的問題:為什么我們在debug時候,開發(fā)工具能夠找到我們對應(yīng)的代碼的源碼呢?!有時候我們的class文件和我們的源碼版本不一樣,那debug時候就亂跑一通?!實際上就是這個LineNumberTable造成的,它的功能非常簡單,將方法中的指令的行號和源碼中的行號進(jìn)行對應(yīng),這一點我們從截圖中看它的形式便能夠非常輕松的理。在進(jìn)行前端編譯的時候我們可以選擇是否保留LineNumberTable,javac -g:none 選擇不保留,javac -g:lines選擇保留,當(dāng)然不保留時,我們就無法從源碼中設(shè)置斷點了。
LocalVariableTable
這是個非常重要的部分,它描述了運行時局部變量表中的變量和源碼中變量的關(guān)系,直觀的給我們展示了Java虛擬機運行時的組成。前面我們在說Java虛擬機內(nèi)存組成的時候提到過Java虛擬機棧,它的棧貞的每一個元素便包含了一個局部變量表,方法的調(diào)用對應(yīng)著虛擬機棧的進(jìn)棧操作,調(diào)用結(jié)束后返回對應(yīng)著一個出棧操作。這是我們Java虛擬機執(zhí)行的系統(tǒng)的核心之一!不過這里不是運行時的局部變量表,現(xiàn)在只是前端編譯階段,但是正如我們開始時所說的那樣,我們可以從class文件的結(jié)構(gòu)中窺探Java虛擬機運行時的樣貌。它和LineNumberTable一樣,也不是Java虛擬機執(zhí)行時必須的,可以在前端編譯時選擇javac -g:none或者javac -g:vars來選擇取消或者生成這個部分,但是Java虛擬機運行時一定會有對應(yīng)的局部變量表。
這里有個知識點:如果方法是實例方法,那么局部變量表的第一個變量就是this,代表實例本身,這也是為何我們可以在實例方法中可以使用this關(guān)鍵字的原因。
字段表和方法表中還包含了很多其他很重要的屬性,但是我們無法寫完整,主要原因是:
1.我也不是很了解~
2.那太多啦!
我們只說了幾個能很好反應(yīng)虛擬機運行機制的部分,能夠理解虛擬機的運行就達(dá)到了我們的目的了。
Java中的異??刂屏鞒獭猼ry catch finally在字節(jié)碼中的體現(xiàn)
下面我們還是先看一下我們的示例代碼中的方法:
然后是它轉(zhuǎn)成字節(jié)碼后的代碼:
Java中的異??刂剖峭ㄟ^Exception table實現(xiàn)的,以我們代碼中的Exception table為例,它定義了try catch finally代碼塊執(zhí)行的三個流程:
????1.0-8行指令執(zhí)行,當(dāng)出現(xiàn)java/lang/NumberFormatException異常時跳轉(zhuǎn)到18行的指令。
????2.0-8行指令執(zhí)行,當(dāng)出現(xiàn)任何異常時,跳轉(zhuǎn)至34行指令。
????3.18-24行的指令執(zhí)行,當(dāng)出現(xiàn)任何異常時,跳轉(zhuǎn)至34行指令。
正好對應(yīng)了try catch finally的語言。
Java中方法的調(diào)用指令
invokevirtual 調(diào)用虛方法,指調(diào)用實例的方法(公共的方法)。
invokeinterface 調(diào)用接口方法,在運行時找到實現(xiàn)接口方法的對象,調(diào)用對象的合適(方法的重載重寫)方法執(zhí)行。
invokespecial? ? 調(diào)用特殊的實例方法:實例初始化方法、私有方法、父方法
invokestatic? ? 調(diào)用類方法
invkedynamic? ? 這個比較特殊,是對動態(tài)語言的支持,并且在Java編譯器中無法看到
Java中方法的調(diào)用指令很重要,它搭建出了Java用語言方法調(diào)用的基本特性。
四、Java虛擬機類加載機制
類加載的過程
java虛擬機加載類的過程可以細(xì)分為以下7個階段:
加載(Class Loading)
這個過程是指:
1.使用類的全限定名來獲取此類的二進(jìn)制流。
2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構(gòu)。
3.生成一個代表這個類的lava.lang.Class對象,class對象雖然是一個對象,但是會存在方法區(qū)中(HotSpot虛擬機就是這么做的)。
Java虛擬機對類的加載是比較封閉的,字節(jié)流的獲取是我們少數(shù)能控制的部分,因此也產(chǎn)生了很多我們熟知的技術(shù):
從zip包中獲取,如我們熟知的jar、ear、war包
從網(wǎng)絡(luò)中獲取,如已經(jīng)沒落的Applet技術(shù)
代碼動態(tài)生成的,如jdk的動態(tài)代理cglib技術(shù)
由其他文件生成,如jsp(jsp生成的字節(jié)流的加載器有些特殊,每一次jsp文件的加載變會生成一個新的加載器,這個加載器生成的目的就是為了被廢棄)
假如我們看了jdk的類加載器的代碼,就會知道,實際上,每個類加載器都會有一個定義自己管轄的目錄的構(gòu)造方法,然后在這個路徑下取字節(jié)流。
這里有一個特殊的情況,那就是對于數(shù)組的加載,我們知道數(shù)組不屬于java的基本數(shù)據(jù)類型,也不是一個簡單的引用類型,沒有對應(yīng)的Class,實際上數(shù)組對象是由Java虛擬機直接創(chuàng)建的。數(shù)組去掉維度后的類型如果是引用類型則會觸發(fā)這個引用類型的加載數(shù)組類型的可見性和引用類型保持一致;如果是基礎(chǔ)數(shù)據(jù)類型,則可見性為public。
驗證
這個階段的目的就是為了保證Class字節(jié)流中包含的信息符合虛擬機的要求,并且不會危害到虛擬機的安全。這個過程我覺得只要知道大概意思即可,沒有必要過多研究。
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量的初始值的階段,類變量如果是基本數(shù)據(jù)類型或者String類型的數(shù)據(jù),則內(nèi)存劃分是在方法區(qū)中進(jìn)行的。這里我們?nèi)匀灰晕覀兊闹暗淖止?jié)碼文件證明一下:
在我們的字節(jié)碼文件中的最后部分,我們能看到一個static{}代碼塊,實際上這個便是虛擬機生成的<clinit>(class init)方法里的指令,用來初始化類。我們可以看到,首先第一條指令ldc是從常量池中獲取“我是類變量”這個常量,然后putstatic賦值給#4 (me/wxh/clazzstd/TestClass.CLASS_VARIABLE:Ljava/lang/String;)這個類變量。不過這個是初始化階段的代碼,我們只是用來證明下這種數(shù)據(jù)類型的類變量的內(nèi)存劃分的位置。
對于實例變量類型的類變量,自然不用說,它一定是在Java堆中劃分內(nèi)存的。
非引用類型的類變量的初始值都是0值,但是我們還是需要注意,常量和類變量的區(qū)別,同時被static和final修飾的變量也就是常量的初始值會被直接賦值為對應(yīng)的ConstantValue的值,這個我們在前面已經(jīng)說到過了。
解析
解析是將常量池中的符號引用翻譯為直接引用的過程。在之前我們已經(jīng)知道,常量池中包含兩種類型的數(shù)據(jù):字面量和符號引用。字面量包含基本數(shù)據(jù)類型和String類型的數(shù)值,符號引用引用開其他符號引用或者字面量。但是虛擬機執(zhí)行不會在執(zhí)行的時候去翻譯這些符號引用,而是在解析階段就將其翻譯為直接引用,即句柄或者指針。
解析過程的觸發(fā)是在虛擬機指令操作符號引用時觸發(fā)。
類變量的初始化
根據(jù)我的理解,解析和初始化過程是交替進(jìn)行的,應(yīng)該沒有嚴(yán)格的先后順序。
再次看一下我們之前的示例的字節(jié)碼的最后部分:
在之前的準(zhǔn)備階段,我們已經(jīng)提到,虛擬機會自動生成<clinit>方法,這個方法的作用是初始化類的,它里面的指令內(nèi)容包含兩種,順序由在源碼中出現(xiàn)的先后順序決定:
對類變量進(jìn)行賦值,這個我們在實例代碼中已經(jīng)可以清楚的看到。
第二種是源碼中的staic代碼塊中的內(nèi)容將會被生成到<clinit>方法中。
虛擬機啟動時并不會立刻將所有代碼里的所有類都加載到虛擬機中,而是在運行時動態(tài)加載的,當(dāng)運行中遇到下面情況時,虛擬機會加載類:
遇到new、getstatic、putstatic、invokestatic這4條指令時,如果類沒有加載則出發(fā)類的加載過程。注意:使用static final修飾的常量時,并不是使用getstatic指令,并不會觸發(fā)類的加載。
使用java.lang.reflect包的方法進(jìn)行反射調(diào)用時,如果類沒有被初始化
初始化一個類時,發(fā)現(xiàn)其父類還沒有被初始化,則先初始化其父類
虛擬機啟動時main方法所在的類會被優(yōu)先初始化。
Java支持動態(tài)語言時解析出的REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄對應(yīng)的類沒有初始化則先進(jìn)行初始化。
關(guān)于<clinit>方法:
首先它不是必要的,當(dāng)一個類中既沒有類變量,也沒有static代碼塊時,Java虛擬機不會產(chǎn)生<clinit>方法
當(dāng)執(zhí)行一個類的<clinit>方法時,如果父類的<clinit>方法還沒有被執(zhí)行,那么就先執(zhí)行父類的<clinit>方法。
<clinit>方法在并發(fā)執(zhí)行的時候時加鎖的。
類加載器:
首先,類加載器的作用是完成類的加載動作的,即類加載的第一個階段。Java中可以擁有很多個類加載器,其中有虛擬機提供的,也會有自定義的類加載器。每一個類加載器都有其類命名空間,當(dāng)一個Class的字節(jié)碼由不同類加載器加載時,那么它們就是不相同的類。
Java中的類加載器和雙親委派模型
虛擬機的類加載器可分為兩種:一種是虛擬機提供的加載器——啟動加載器,由C++實現(xiàn);另一種是由Java語言實現(xiàn)的類加載器,它們都繼承于抽象類java.lang.ClassLoader。
從另一個維度,按功能劃分,我們可以將Java中默認(rèn)提供的類加載器分為以下幾種:
啟動類加載器(Bootstrap ClassLoader),它的作用就是將javahome\lib目錄下或者指定的-Xbootclasspath目錄下的類庫按名字查找并加載到虛擬機中。比如我們的rt.jar,如果改名叫其他名字,那么,即使它在以上目錄下,那么它也不能正常被加載。它是由C++實現(xiàn)的,是Java虛擬機的組成部分。
擴展類加載器(ExtClassLoader),它是用來加載“java.ext.dirs”系統(tǒng)變量指定目錄下的類庫的。它是由Java語言實現(xiàn)的。
應(yīng)用加載器(AppClassLoader),它和ExtClassLoader一樣都是在sun.misc.Launcher類中的內(nèi)部類,負(fù)責(zé)加載classpath中的指定的類庫。
自定義的類加載器。我們自己用java代碼實現(xiàn)的類加載器。
加載器的雙親委派模型
如果一個類加載器收到了類加載的請求,它首先不會嘗試自己去加載這個類,而是委派給父類加載器去完成,所以每一次加載請求會先傳到頂部的啟動加載器,當(dāng)父類加載器無法完成加載請求時子類加載器才會去嘗試加載類。加載器雙親委派模型如下:
假如我們自己實現(xiàn)了一個java.lang.Object類,因為有雙親委派模型的存在,類加載請求最終會被轉(zhuǎn)到啟動加載器中去,而啟動加載器只會在自己管轄的路徑里去查找類,所以我們無法自己寫一個Object類放到classpath中去替換jdk提供的Object類(實際上我們可以下載Openjdk去修改代碼并編譯)。
五、虛擬機字節(jié)碼執(zhí)行引擎
?? ??? ?大學(xué)時候我們學(xué)習(xí)編譯原理時候,我們知道程序執(zhí)行分為兩種:解釋執(zhí)行和編譯執(zhí)行。解釋執(zhí)行不提前編譯代碼,通過解釋器去執(zhí)行代碼;編譯執(zhí)行則預(yù)先編譯好代碼產(chǎn)生本地代碼去執(zhí)行,而Java程序運行中時時進(jìn)行編譯和優(yōu)化的技術(shù)叫做JIT。我們將java代碼編譯成class文件的動作被稱為前端編譯,后期將class文件的內(nèi)容編譯為本地文件的工作叫做即時編譯(JIT)。總體來說,解釋執(zhí)行的有點是啟動速度快,而編譯執(zhí)行的優(yōu)點則是執(zhí)行效率快。
?? ?? ? 前面我們在說Java內(nèi)存模塊的時候提到過虛擬機棧,它是Java虛擬機執(zhí)行的根本構(gòu)造,它是屬于線程的,每個線程都擁有自己的方法棧。虛擬機棧中的一個棧幀對應(yīng)了一次方法的調(diào)用,所有方法的調(diào)用在一起便是我們程序的執(zhí)行!?Java中運行時內(nèi)存模型是工作內(nèi)存——主內(nèi)存的模型,主內(nèi)存負(fù)責(zé)存儲數(shù)據(jù),工作內(nèi)存從主內(nèi)存獲取數(shù)據(jù)的副本在工作內(nèi)存中運算,結(jié)束后將變量的值存儲到主內(nèi)存 。虛擬機棧就是我們的工作內(nèi)存,下圖展示了棧幀的基本結(jié)構(gòu):
這個圖簡單的示意了虛擬機棧的結(jié)構(gòu),實際上虛擬機棧實現(xiàn)的時候,相鄰棧幀的操作數(shù)棧和局部變量表會設(shè)計成相交的,以實現(xiàn)方法調(diào)用的返回值。
局部變量表
?? ??? ?這個我們在解釋class文件結(jié)構(gòu)時便介紹過,在此我們可以前后照應(yīng)。局部變量表的作用是存放方法的參數(shù)和方法內(nèi)定義的局部變量,局部變量表的最小單位以solt計算,每一個slot中存放著boolean、byte、char、short、int、float、reference和returnAddress類型的數(shù)據(jù),long和double類型的數(shù)據(jù)則分配兩個連續(xù)的slot存儲。reference就是我們通常說的引用,它分為句柄和指針兩種。returnAdress現(xiàn)在不怎么使用了,最初被用來實現(xiàn)異常處理,現(xiàn)在已經(jīng)被異常表代替。
?? ?? ?如果我們讀過《effectiv java》這本書,應(yīng)該會對書中有一章有所印象,這章提到要盡量最小化變量的作用域,在這里我們可以得到印證。因為局部變量表的每個slot是可以復(fù)用的減少變量的作用域不僅可以減少局部變量表的長度,而且確定不用的變量會被垃圾回收器回收掉。
?? ?? 如果方法是實例方法,那么局部變量表的第0位存放則是這個實例的reference,也就是我們一直用的this!
操作數(shù)棧
? ? ??前面我們解釋class文件方法中的Code屬性時介紹過,字節(jié)碼指令有的是不帶參數(shù)的,而不帶參數(shù)的指令則操作的目標(biāo)則是操作數(shù)棧中的數(shù)據(jù),例如字節(jié)碼中的iadd則是將操作數(shù)棧棧頂?shù)膬蓚€int數(shù)據(jù)相加,ldc指令則是將常量放入操作數(shù)棧的棧頂。
方法的調(diào)用
Java中方法的調(diào)用指令有以下5種:
invokevirtual 調(diào)用虛方法,指調(diào)用實例的方法(公共的方法)。
invokeinterface 調(diào)用接口方法,在運行時找到實現(xiàn)接口方法的對象,調(diào)用對象的合適(方法的重載重寫)方法執(zhí)行。
invokespecial? ? 調(diào)用特殊的實例方法:實例初始化方法、私有方法、父方法
invokestatic? ? 調(diào)用類方法
invkedynamic? ? 這個比較特殊,是對動態(tài)語言的支持,支持了lambda表達(dá)式的語法。
在我們之前說的類加載過程的解釋過程中,invokespecial和invokestatic指令帶的符號引用已經(jīng)被翻譯成方法的入口地址,它們的調(diào)用時固定的,因此它們被稱為非虛方法的調(diào)用。invokeinterface和invokevirtual調(diào)用的時候需要根據(jù)操作數(shù)棧棧頂?shù)膶ο髞慝@取調(diào)用方法的實際入口地址,它們則被稱為虛方法的調(diào)用。
方法的靜態(tài)分派
接下來,我們用書上的例子來解釋下方法的靜態(tài)分派:
這個例子的執(zhí)行結(jié)果大家應(yīng)該沒有什么異議的,不論方法的實際類型時什么,虛擬機執(zhí)行的結(jié)果都是“hello,guy”。這是因為兩個sayHello方法調(diào)用的方法在編譯期間已經(jīng)確定,我們傳入的參數(shù)woman和man的靜態(tài)類型(Static Type)都是Human,因此調(diào)用的都是參數(shù)類型為Human的方法。
我們來看下這個代碼編譯后調(diào)用sayHello方法時的字節(jié)碼指令:
從后面對#13符號引用的備注我們可以很明顯的看到調(diào)用的方法在前端編譯期已經(jīng)被確定是參數(shù)類型是Human的sayHello方法。這個跟我們后面要說的動態(tài)分配可以做個對比,可以很清晰地從字節(jié)碼層面理解動態(tài)分配和靜態(tài)分配的區(qū)別。
方法的靜態(tài)分配按照靜態(tài)類型分配是它叫做靜態(tài)分配的主要原因,不過我覺得這個分配是在編譯期已經(jīng)確定了的也是它叫做這個名字的另一個主要原因吧。
關(guān)于方法的重載overload,靜態(tài)分配會在編譯期決定了到底調(diào)用哪個版本的代碼,不過如果方法的參數(shù)個數(shù)一致,編譯期在確定版本的時候會按照一定的規(guī)則來確定死亡(這個規(guī)則比較難用語言描述,編譯器會選擇最合適的版本),例如上面的例子,我們把參數(shù)類型為Man的sayHello方法注釋掉,然后main方法里man的類型聲明為Man,則代碼執(zhí)行的結(jié)果會是這樣的:
這里,我們把參數(shù)類型為Man的方法已經(jīng)注釋掉了,按照靜態(tài)類型分配,已經(jīng)無法分配到參數(shù)類型為Man的方法了,于是編譯期給我們分配到了參數(shù)類型為Human的方法。
再用《深入理解Java虛擬機》這本書上的例子來更好地演示下編譯期在靜態(tài)分配上做的最合適的選擇:
這里我們重寫了很多sayHello方法,當(dāng)我們調(diào)用時使用’a’作為參數(shù)時,編譯器給我們選擇的最優(yōu)方法是sayHello(char arg),這時候我們看編譯后的結(jié)果:
方法調(diào)用選擇的是sayHello:(C)V,這里的C便是char類型。但是當(dāng)我們把sayHello(char arg)這個方法注釋掉,那么編譯的結(jié)果就會是這樣:
編譯期把同一段代碼編譯成了sayHello:(I)V這個不一樣的結(jié)果(I表示int),有興趣的可以逐個注釋掉這些方法,看看編譯器選擇的優(yōu)先級。
方法的動態(tài)分派
前面說的靜態(tài)分派是前端編譯期已經(jīng)決定的,動態(tài)分派則不是,它是在虛擬機運行期間對象的實際類型來確定執(zhí)行的方法的,這也是它名字的由來。
下面我們還是用《深入理解Java虛擬機》這本書上的例子結(jié)合編譯后的字節(jié)碼來演示下什么叫動態(tài)分派:
注意:我們這里的sayHello方法都是沒有參數(shù)的,或者說參數(shù)一樣的,無法通過靜態(tài)分配區(qū)分出。
相信任何Java程序員對這段代碼的結(jié)果應(yīng)該都沒有異議,這非常面向?qū)ο蟆?那么接下來我們需要從Java虛擬機的角度來考慮,為什么結(jié)果會是這樣的!首先,我們還是來看一下這段代碼中main函數(shù)編譯后的字節(jié)碼:
?? ?從字節(jié)碼的LineNumberTable中可以看出,源碼中三句sayHello方法——23行、24行、26行的調(diào)用分別對應(yīng)著Code屬性里的16-17行、20-21行、32-33行。這里的一行源碼被編譯成了兩行字節(jié)碼指令,這是為什么能?
?? ?我們之前說過Java虛擬機運行時內(nèi)存模型時說過操作數(shù)棧這個概念,它存儲了方法執(zhí)行過程中的臨時數(shù)據(jù)。aload_<n>這個字節(jié)碼指令的意思是將局部變量表中的第n個slot中局部變量加載到操作數(shù)棧中,對應(yīng)代碼里,就是將我們聲明并實例化的man、woman的變量引用存儲到操作數(shù)棧中(man是第一個生聲明的變量,n是1,woman是第二個聲明的變量,n是2)。緊接著是invokevirtual指令,它是對實例方法的調(diào)用,但我們只看到了它的參數(shù)只有一個對方法的描述,卻沒有對象,對的,它執(zhí)行的對象就是操作數(shù)棧棧頂?shù)淖兞俊?/p>
這個時候,我們看到的對方法的描述都是“Method me/wxh/clazzstd/DynamicDispatch$Human.sayHello:()V”,但是卻因為棧頂元素的不同,執(zhí)行了不同的方法。實際上我們在介紹靜態(tài)分配時舉的第一個例子編譯的字節(jié)碼也是invokevirtual,不過那里是同一個對象,()V里面的參數(shù)類型限定符的不同,這里是限定符相同,對象不同。
?? ?
?? ?對于invokevirtual指令,它的解析過程大致是這樣的:
找到棧頂?shù)牡谝粋€元素所指的對象的實際類型,記做C
如果在類型C中找到常量的中描述符和簡單名稱都相符的方法,則進(jìn)行方法的權(quán)限檢驗,如果通過則返回這個方法的直接引用,查找過程結(jié)束;如果不通過則返回IllealAccessError
否則按照繼承關(guān)系從下往上對C的各個父類進(jìn)行第2步查找
如果最終沒有找到合適的方法,則拋出java.lang.AstractMethodError
?? ?
? ? 順便我們利用這個代碼在驗證下我們之前說的Java虛擬機運行時的java虛擬機棧的概念,代碼里我們new里三個對象,我們拿第一個new的man的對象來解釋下,代碼很簡單:Human man = new Man();它編譯后的字節(jié)碼指令對應(yīng)為:
我們來解釋下這四行字節(jié)碼:
0行:new指令創(chuàng)建me/wxh/clazzstd/DynamicDispatch$Man類的實例,這個時候操作數(shù)棧棧頂會有一個這個實例的引用。
3行:dup指令將棧頂?shù)脑貜?fù)制一份再壓回棧頂,這個時候操作數(shù)棧有兩個一個一樣的me/wxh/clazzstd/DynamicDispatch$Man類的實例的引用
4行:invokespecial指令調(diào)用類實例的<init>方法,操作數(shù)棧棧頂出棧,只剩下一個me/wxh/clazzstd/DynamicDispatch$Man類的實例的引用
7行:astore_1指令將操作數(shù)棧棧頂?shù)淖兞看鎯Φ骄植孔兞勘淼牡?位(實例方法的第0位是this保留的,可能靜態(tài)方法也保留了只不過沒有值,這個有待考證,不過不影響我們介紹流程),這個時候操作數(shù)棧第二個me/wxh/clazzstd/DynamicDispatch$Man類的實例的引用也出棧了。
花了這么多時間來舉這個例子,我覺得是非常值得的,通過它,我們知道了局部變量表和操作數(shù)棧是如何協(xié)同工作的了
動態(tài)語言支持
首先對動態(tài)語言的理解:拿javascript舉例,變量的聲明都是“var”,變量是不明確類型的,變量的值才具有類型,方法的調(diào)用在運行時才去判斷。
之前說過的invokedynamic指令是java對動態(tài)語言的支持,但是在java7及以下版本是看不到invokedynamic,并且invokedynamic指令設(shè)計的目的也是提高Java虛擬機對動態(tài)語言的支持,使得其他在java虛擬機上能支持動態(tài)語言的執(zhí)行。
java7的動態(tài)語言支持——java.lang.invoke.MethodHandle
下面是一個java.lang.invoke.MethodHandle的使用例子,來自于《深入理解java虛擬機》這本書:
但是這個類的字節(jié)碼中我們無法找到invokedynamic指令,有興趣的可以用javap看一下,不需要用java7去編譯。
java8后的lambda表達(dá)式
下面我們用一個lambda表達(dá)式的例子來看一下invokedynamic指令是什么樣子的。
源代碼:
javap查看字節(jié)碼:
第一個紅圈對應(yīng)的是
Arrays.sort(names, new Comparator<String>() {
??? @Override
??? public int compare(String o1, String o2) {
??????? return o1.compareTo(o2);
??? }
});
這個代碼,我們可以清楚看到,產(chǎn)生了一個LambdaTest$1這個匿名內(nèi)部類,并new了一個它的對象。
第二個紅圈對應(yīng)的代碼是
list.forEach((String name) -> {
??? System.out.println(name);
});
這個用lambda表達(dá)式產(chǎn)生的字節(jié)碼,這里我可以看出,編譯器產(chǎn)生的是一個invokedynamic指令。
五、虛擬機運行期間優(yōu)化
?? ??? ?首先我們得有個大的概念:虛擬機的優(yōu)化分為兩個階段,第一個階段是前端編譯階段,這個期間Java編譯器將我們的源碼編譯成字節(jié)碼,字節(jié)碼與平臺無關(guān),為后期進(jìn)一步解釋編譯提供基礎(chǔ);第二個階段是運行時解釋/編譯,這時候Java虛擬機會進(jìn)一步將字節(jié)碼翻譯成機器碼后執(zhí)行。
學(xué)過編譯原理,我們都知道,程序的執(zhí)行分為解釋執(zhí)行和編譯執(zhí)行兩種:解釋執(zhí)行是使用解釋器執(zhí)行,不提前編譯代碼,優(yōu)點是啟動速度快、省去編譯的時間,缺點是解釋執(zhí)行的效率相對編譯執(zhí)行會慢很多;編譯執(zhí)行則是提前將代碼編譯成機器碼后執(zhí)行,相對于解釋執(zhí)行的缺點就是啟動慢,優(yōu)點則是執(zhí)行效率高。編譯器在前端編譯期間就在生成字節(jié)碼上進(jìn)行了優(yōu)化,不過這個不是我們要討論的內(nèi)容。
主流的Java虛擬機都同時包含解釋器和編譯期,并不會單一的使用解釋執(zhí)行或者編譯器。虛擬機會統(tǒng)計代碼的執(zhí)行頻度,當(dāng)代碼反復(fù)執(zhí)行后,虛擬機就知道這段代碼是一段“熱點代碼”,這時候就會對這段代碼進(jìn)行重新優(yōu)化編譯,這種技術(shù)就是JIT(Just In Time Compiler)技術(shù)。
熱點代碼:
多次被調(diào)用的方法
多次被執(zhí)行的循環(huán)體里的代碼塊
client模式和server模式
?? ??? ?Java虛擬機(HotSpot)在啟動的時候可以選擇-client以客戶端模式啟動,-server以服務(wù)端模式啟動。
?? ?? ? Hotspot虛擬機中包含了兩個編譯器,分別稱為Client Compiler和Server Compiler,也分別被簡稱為C1和C2編譯器。虛擬機中一般是默認(rèn)采用解釋器和編譯器混合執(zhí)行的策略。虛擬機啟動時可以通過-Xint指定為只使用解釋執(zhí)行,那么虛擬機將完全使用解釋執(zhí)行;-Xcomp指定為編譯執(zhí)行,這時候虛擬機優(yōu)先采用編譯器執(zhí)行程序,但是在編譯器無法進(jìn)行的情況下進(jìn)行解釋執(zhí)行。
?? ?? ? Server模式在虛擬機中被默認(rèn)開啟。分層編譯分為:
第0層,程序解釋執(zhí)行,解釋器不開啟性能監(jiān)控,可觸發(fā)第1層編譯
第1層,被稱為C1編譯,將字節(jié)碼編譯為本地代碼,進(jìn)行簡單、可靠的優(yōu)化,如有必要將加入性能監(jiān)控的邏輯。
第2層,也被稱為C2編譯,也是將字節(jié)碼轉(zhuǎn)位本地代碼,但是會啟用一些耗時比較長的優(yōu)化,甚至?xí)鶕?jù)性能監(jiān)控i 信息進(jìn)行一些不可靠的激進(jìn)優(yōu)化。
六、Java的內(nèi)存模型(JMM)與線程
前面我們已經(jīng)提到過java的工作空間和主內(nèi)存的概念,下圖示意里線程、工作內(nèi)存和主內(nèi)存的之間的交互關(guān)系:
?? ??? ?這個內(nèi)存模型實現(xiàn)了并發(fā)變成的基礎(chǔ),非常類似于物理機的內(nèi)存模型。線程在使用主內(nèi)存中的數(shù)據(jù)時,需要先將變量從主內(nèi)存中拷貝到工作內(nèi)存中形成變量的副本,然后在工作內(nèi)存中進(jìn)行賦值、讀取等操作。這種內(nèi)存模型使線程對數(shù)據(jù)的操作都是在工作內(nèi)存中進(jìn)行的,虛擬機能夠?qū)⒐ぷ鲀?nèi)存優(yōu)先存儲于比物理內(nèi)存更快的高速緩存和寄存器中,從而提高了程序的運行速度。
?? ?? ? 但是我們思考下,這樣做也會產(chǎn)生一個不好的后果:我們同時會有主內(nèi)存中變量的多個副本,多個線程對器進(jìn)行讀取、賦值操作也就造成,某些副本的值已經(jīng)失去失效,然后用失效的值進(jìn)行運算,再將錯誤的結(jié)果寫入主內(nèi)存,這都導(dǎo)致了我們常說的“多線程安全”問題。正式因為出于這個考慮虛擬機則在主內(nèi)存和工作內(nèi)存進(jìn)行交互時定義了一些列規(guī)定和協(xié)議,正確使用則能保證相對的線程安全(沒有絕對的線程安全)。
Java內(nèi)存模型定義了一些基本操作,這些操作都是原子的、不可再分的(操作不代表指令):
lock(鎖定):用于主內(nèi)存的變量,它把變量標(biāo)識為一條線程獨占的狀態(tài)。
Unlock(解鎖):作用于主內(nèi)存的變量,它把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定。
read(讀?。鹤饔糜谥鲀?nèi)存的變量,它把一個變量的值傳輸?shù)骄€程的工作內(nèi)存中,以便隨后的load動作使用。
load(載入):作用于工作內(nèi)存的變量,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存變量副本中。
use(使用):作用于工作內(nèi)存的變量,它把工作內(nèi)存中的一個變量的值傳輸給執(zhí)行引擎,每當(dāng)虛擬機遇到一個需要使用到變量的值的字節(jié)碼指令時將執(zhí)行這個操作。
assign(賦值):作用于工作內(nèi)存中的變量,它把一個從執(zhí)行引擎中接收到的值賦工作內(nèi)存中的變量,每當(dāng)虛擬機遇到一個給變量賦值的字節(jié)碼時執(zhí)行這個操作。
store(存儲):作用于工作內(nèi)存的變量,它將工作中一個變量的值傳輸?shù)街鲀?nèi)存中,以便隨后的write操作使用。
write(寫入):作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中得到的變量的值放入到主工作內(nèi)存中。
?? ??? ?這些操作本身的意義非常好理解,無非是圍繞著變量的鎖定解鎖、變量值復(fù)制、傳遞、賦值過程進(jìn)行定義的。圍繞著這些定義的基本操作,Java虛擬機規(guī)范提出了必須要遵守的規(guī)則,然后通過這些規(guī)則限定,保重了數(shù)據(jù)在工作內(nèi)存和主內(nèi)存之間交互的線程安全性。
volatile修飾符的語意
volatile修飾符保證了變量讀寫的多線程可見性
? ? 基于上面的知識,我們考慮一下這種情況:在多線程環(huán)境下,我們一個線程A從主內(nèi)存中read、load了一個變量的值,然后另一個線程B store、wtrite修改了主內(nèi)存中這個變量的值,這時候已經(jīng)拿了變量值的線程A在use變量副本的值的時候是不知道線程B對變量的賦值操作的。但是如果我們使用volatile來修飾我們的變量,那么過程就不是這樣了,虛擬機規(guī)范中對volatile修飾的變量作出了以下規(guī)定:
線程對volitale變量的副本的load、use操作必須是連續(xù)在一起的,也就是說每次使用volitale變量時,肯定是從主內(nèi)存中最新同步的。
線程對工作內(nèi)存中的副本變量進(jìn)行sotre、write操作前的前一條操作必須是assign操作,也就是說,賦值完必須馬上存儲到主內(nèi)存中去。
由于這兩條規(guī)定,虛擬機就保證了線程執(zhí)行過程對volitale變量的賦值和使用時都是類似于原子的操作,也就保證了它多線程讀寫的可見性!
volatile修飾符修飾的代碼不會被指令重排序
我們知道,Java虛擬機在運行期間會進(jìn)行代碼的優(yōu)化,中間會對結(jié)果不變的多個指令操作進(jìn)行重新排序,但是這可能對其執(zhí)行的先后順序進(jìn)行了修改,如果我們的線程B的代碼依賴于線程A指令執(zhí)行的先后順序而產(chǎn)生的結(jié)果,那么就可能導(dǎo)致產(chǎn)生錯誤的判斷結(jié)果,這個是非??膳碌摹?span >如果使用了voatile修飾一個變量,那么虛擬機將不會對volatile修飾的變量進(jìn)行指令重排序優(yōu)化。注意:線程A代碼依賴于自身代碼指令的先后順序而產(chǎn)生的結(jié)果,那么即使進(jìn)行了重新排序,那么也不會影響判斷,這是重排序優(yōu)化自己做的限制。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。