學(xué)習(xí)java虛擬機(jī)已經(jīng)很久了,最近有空,于是將我所知道的一些關(guān)于java虛擬機(jī)的知識(shí)寫出來。首先當(dāng)做是重新復(fù)習(xí)一下,其次是給想了解java虛擬機(jī)的朋友一些參考。筆記內(nèi)容大量參看《深入理解java虛擬機(jī)》這本書。
我們提供的服務(wù)有:成都網(wǎng)站建設(shè)、網(wǎng)站建設(shè)、微信公眾號開發(fā)、網(wǎng)站優(yōu)化、網(wǎng)站認(rèn)證、潼關(guān)ssl等。為上1000家企事業(yè)單位解決了網(wǎng)站和推廣的問題。提供周到的售前咨詢和貼心的售后服務(wù),是有科學(xué)管理、有技術(shù)的潼關(guān)網(wǎng)站制作公司
一、虛擬機(jī)內(nèi)存組成模塊
java虛擬機(jī)規(guī)范中規(guī)定了以下組成部分:程序計(jì)數(shù)器、虛擬機(jī)方法棧、本地方法棧(Hotspot中將虛擬機(jī)方法棧和本地方法棧合并成方法棧)、java堆、方法區(qū)(java8以后將方法區(qū)移到了虛擬機(jī)外)、運(yùn)行常量池。
另外java虛擬機(jī)還可以額外分配直接內(nèi)存,不過這不屬于java虛擬機(jī)內(nèi)存組成。整體組成如下圖:
程序計(jì)數(shù)器
java虛擬機(jī)之所以被稱為虛擬機(jī)是因?yàn)樗7挛锢頇C(jī)運(yùn)行實(shí)現(xiàn)的,它的程序計(jì)數(shù)器也類似于操作系統(tǒng)中的程序計(jì)數(shù)器,是線程私有的,作用是存儲(chǔ)線程將要執(zhí)行的下一個(gè)操作指令(java模仿物理機(jī),也自己實(shí)現(xiàn)了多種操作指令)。程序計(jì)數(shù)器只占用了一小塊內(nèi)存區(qū)域。
方法棧和本地方法棧
java中每一次方法調(diào)用都對應(yīng)了方法棧的進(jìn)棧和出站操作,方法棧中每一個(gè)棧幀都對應(yīng)著java代碼中相應(yīng)的方法調(diào)用,棧幀中局部變量表存儲(chǔ)了基礎(chǔ)數(shù)據(jù)類型(boolean、byte、char、int、long、float、double、)和reference(reference包括兩種:句柄和指針,各自有各自的好處,使用句柄則在改變對象位置時(shí)不改變局部變量表里的引用只用改變句柄本身的指針即可,指針的優(yōu)點(diǎn)則是查詢效率快)。
這里有個(gè)知識(shí)點(diǎn),實(shí)際上Java中的數(shù)組是Java虛擬機(jī)動(dòng)態(tài)生成的一個(gè)對象,不屬于基礎(chǔ)數(shù)據(jù)類型,我們常用的數(shù)組的length屬性其實(shí)就是它的對象的一個(gè)public屬性。
java堆
java堆是虛擬機(jī)中最大的內(nèi)存組成部分,用來存儲(chǔ)程序執(zhí)行中產(chǎn)生的對象(不包括常量、靜態(tài)常量引用的對象)。java堆會(huì)因?yàn)槔厥找约皩?yīng)的垃圾回收器的不同而采用不用的劃分方式,但整體還是劃分為新生代和老年代兩個(gè)部分。新生代又分為eden區(qū)(伊甸區(qū))和survivor區(qū)域(幸存區(qū)域)。java默認(rèn)Eden區(qū)域是survivor區(qū)域的8倍大?。ɡ厥諒?fù)制算法執(zhí)行過程統(tǒng)計(jì)出來的合適倍數(shù))。不過存在survivor區(qū)域又分為兩塊相同大小的survivor區(qū)域:from?survivor區(qū)域和to?survivor區(qū)域,作為輪轉(zhuǎn)備用。簡單的說,java程序運(yùn)行中對象就是Eden區(qū)域survivor區(qū)域和老年代中創(chuàng)建、清理、復(fù)制、整理。
方法區(qū)
方法區(qū)用于存儲(chǔ)虛擬機(jī)加載的類信息、常量、靜態(tài)常量等,也被稱為永久代。Hotspot在java8之前用永久代來實(shí)現(xiàn)方法區(qū),java8后永久代被移出虛擬機(jī)內(nèi)存,使用native?memory存儲(chǔ)。
運(yùn)行時(shí)常量池
運(yùn)行時(shí)常量池屬于方法區(qū)的一部分,用來存儲(chǔ)常量的值,存儲(chǔ)內(nèi)容分為兩種:字面量和符號引用。這個(gè)的理解需要結(jié)合Class類的前端編譯來解讀。在虛擬機(jī)加載類的時(shí)候比如類的名字、字段的名字、常量等的值需要存儲(chǔ)下來,而且會(huì)頻繁使用。Java中的基本數(shù)據(jù)類型和String類型都可以在虛擬機(jī)加載類的時(shí)候理解為虛擬機(jī)可以描述的值,并不是程序員自己定義的對象。這些值是需要并可以存儲(chǔ)在虛擬機(jī)中并供后期使用的,這些值便是運(yùn)行時(shí)常量池中的字面量。另外如string.intern()方法也可以在運(yùn)行期間將一個(gè)String的值放到常量池中并返回常量池的引用,只不過這個(gè)不是在虛擬機(jī)加載類時(shí)候放入的。常量池中另一種數(shù)據(jù)類型是符號引用,這個(gè)跟class的結(jié)構(gòu)也是相關(guān)的,前端編譯階段,對class結(jié)構(gòu)的描述過程中,一個(gè)字面量是可以反復(fù)被使用的,于是便可以給字面量編一個(gè)索引,在符號引用中引用這個(gè)索引去得到值,當(dāng)然,符號引用本身也會(huì)被索引供其他符號引用使用。這個(gè)便是常量池中的內(nèi)容,需要結(jié)合對class結(jié)構(gòu)的了解才能更好的理解為什么會(huì)有常量池以及常量池中 存儲(chǔ)的內(nèi)容,不能錯(cuò)誤的直接理解為我們在開發(fā)時(shí)在class中自己定義的 “常量”,它包含了我們通常理解的“常量”,但遠(yuǎn)不止如此。另外,對于我自己常說的自己定義的“常量”,只有static 和 final修飾的基礎(chǔ)類型和string類型才屬于constantvalue,對象不屬于constantvalue。對象的內(nèi)存是分配在Java堆中,常量是分配在方法區(qū)中的運(yùn)行時(shí)常量池中的。舉一個(gè)常量的特殊性的例子:如在使用ClassA.CONSTANT_VALUEA時(shí),這個(gè)時(shí)候虛擬機(jī)使用的是常量,假如這個(gè)時(shí)候ClassA還沒有被加載,使用這個(gè)ClassA.CONSTANT_VALUEA的值時(shí)是不會(huì)觸發(fā)ClassA的加載的。
直接內(nèi)存
java直接allocat出來的內(nèi)存。NIO使用的緩沖區(qū)就是直接內(nèi)存。
二、虛擬機(jī)的垃圾回收
虛擬機(jī)的垃圾回收基本可等同于對Java堆的垃圾回收。
虛擬機(jī)中判斷對象是否死亡的算法——可達(dá)性算法
可達(dá)性算法的描述非常簡單 :對象是否被GC Roots所直接引用,是則存活;是否被GC Roots直接引用的對象所直接或通過其他對象間接引用,是則存活;不滿足則被標(biāo)記為死亡。
以下是從網(wǎng)上找的可達(dá)性算法示意:
GC Roots包含:
1.虛擬機(jī)棧
2.方法區(qū)中的靜態(tài)屬性
3.方法區(qū)中的常量
4.本地方法棧
對象的finalize方法
finalize方法經(jīng)常會(huì)在面試中被問到,它提供了類似C/C++中析構(gòu)函數(shù)的功能,當(dāng)Java中的對象將要被回收時(shí),如果對象有重寫finalize方法,那么finalize方法將會(huì)被調(diào)用一次,當(dāng)?shù)诙我换厥諘r(shí)則不會(huì)被觸發(fā)調(diào)用。我們可以嘗試在finalize方法中拯救對象本身不被虛擬機(jī)回收,例如將對象被GC Roots引用,那樣便可以使對象免于被回收。但是finalize方法并不能一定保證這種操作一定能成功,成功的關(guān)鍵在于finalize方法中的代碼執(zhí)行的要比虛擬機(jī)垃圾回收要快,因此finalize方法中拯救對象本身不具備確定性。finalize方法所Java早期為贏得使用者的產(chǎn)物,建議不使用,它完全能被finally和其他方式代替。
垃圾回收算法——標(biāo)記清除算法
下圖是從網(wǎng)上找的標(biāo)記清除算法的示意圖,其原理非常簡單:首先對對象的可達(dá)性進(jìn)行標(biāo)記,然后清除掉不可達(dá)的對象。
標(biāo)記清除的算法的問題是清除之后留下的可用的存儲(chǔ)空間非常零碎,當(dāng)我們需要一個(gè)比較大的存儲(chǔ)空間來存儲(chǔ)大對象時(shí),這將是個(gè)災(zāi)難。虛擬機(jī)不直接使用標(biāo)記清除算法來回收垃圾,但是標(biāo)記清除算法是其他優(yōu)化過的算法的基礎(chǔ)。
垃圾回收算法——復(fù)制算法
復(fù)制算法的原理也很簡單:將內(nèi)存劃分為兩塊相同大小的區(qū)域,只使用其中一塊,當(dāng)進(jìn)行垃圾回收時(shí),將還存活的對象移至另一塊內(nèi)存中,本身則全部清除掉,這樣就不會(huì)產(chǎn)生內(nèi)存碎片。
下圖是復(fù)制算法的示意圖,圖片來自網(wǎng)上:
我們可以看出,復(fù)制算法是基于標(biāo)記清除算法的思想進(jìn)行的,復(fù)制算法的缺陷是浪費(fèi)了太多內(nèi)存,Java虛擬機(jī)使用復(fù)制算法時(shí)當(dāng)然不會(huì)直接這樣去做。實(shí)際上復(fù)制算法是java堆中新生代的基本算法思想(實(shí)際上并沒有這么直接使用)。
Java虛擬機(jī)根據(jù)對象的存活時(shí)間不同的特點(diǎn)將Java堆分成新生代和老年代。新生代的對象“朝生夕死”,存活時(shí)間短,內(nèi)存重新分配頻繁,適合使用復(fù)制算法進(jìn)行垃圾回收。Java虛擬機(jī)將新生代劃分為eden區(qū)和survivor區(qū)(survivor區(qū)域有兩塊,一塊from區(qū)域,一塊to區(qū)域,輪轉(zhuǎn)備用),對應(yīng)復(fù)制算法需要的兩塊內(nèi)存區(qū)域。因?yàn)榻?jīng)過垃圾回收后剩下的對象其實(shí)是少數(shù),所以survivor區(qū)域并不需要和eden區(qū)域一樣大,那樣太浪費(fèi)內(nèi)存空間,虛擬機(jī)默認(rèn)的大小是eden區(qū)域是survivor區(qū)域的8倍大小,虛擬機(jī)啟動(dòng)時(shí)支持配置。
另外,復(fù)制算法只是基礎(chǔ),虛擬的不同回收器實(shí)際執(zhí)行時(shí)還進(jìn)行了優(yōu)化。
垃圾回收算法——標(biāo)記整理算法
標(biāo)記整理算法也是基于標(biāo)記清除算法實(shí)現(xiàn)的,不同點(diǎn)是在標(biāo)記之后不是將對象直接清除,而是將存活對象前移,清除存活對象內(nèi)存空間之外的內(nèi)存空間。
下圖也是從網(wǎng)上找的示意圖:
當(dāng)內(nèi)存大對象多,且對象頻繁產(chǎn)生死亡的時(shí)候,效率是非常低下的,因此不適合新生代的垃圾回收。但是老年代的對象存活率高,內(nèi)存相對較小,很適合標(biāo)記整理算法。
三種算法總結(jié):標(biāo)記清除算法是其他兩種算法以及其他優(yōu)化過的垃圾回收算法的基礎(chǔ),復(fù)制算法適用于新生代,標(biāo)記整理算法適用于老年代,實(shí)際上,Java虛擬機(jī)也確實(shí)是分代進(jìn)行垃圾回收的。
概念——STW
STW:stop the world。Java虛擬機(jī)進(jìn)行垃圾回收時(shí)是需要中斷工作線程的執(zhí)行的,期間Java程序出現(xiàn)了短暫的停頓。當(dāng)然,現(xiàn)在虛擬機(jī)對垃圾回收的不斷優(yōu)化,幾乎可以忽略STW時(shí)間了。
垃圾回收器——CMS(current mark sweep)回收器
從名字就可以看出CMS回收器是基于標(biāo)記清除算法的回收器,它的運(yùn)作過程分為四步驟:
1.初始標(biāo)記
初始標(biāo)記的作用是標(biāo)記出那些被GC Roots直接引用的對象。這個(gè)期間或產(chǎn)生短暫的STW時(shí)間
2.并發(fā)標(biāo)記
并發(fā)標(biāo)記是同時(shí)和用戶線程執(zhí)行的,標(biāo)記出被所有被引用的對象。不會(huì)產(chǎn)生STW。
3.重新標(biāo)記
并發(fā)標(biāo)記的時(shí)間相對長一些,這個(gè)期間可能用于用戶線程的操作,并發(fā)標(biāo)記的結(jié)果可能已經(jīng)跟實(shí)際產(chǎn)生了偏差,重新標(biāo)記便是糾正這個(gè)偏差的。期間會(huì)停止用戶線程,產(chǎn)生STW。
4.并發(fā)清除
并發(fā)清除就很好理解了,就是垃圾的清除工作是和用戶線程一起進(jìn)行的,不會(huì)導(dǎo)致用戶線程的停頓。
在進(jìn)行以上四步后并不能保證所有的垃圾都被清除掉了,因?yàn)橛脩艟€程是在并發(fā)進(jìn)行的。遺漏的垃圾對象需要依賴于下次垃圾回收進(jìn)行清除。
CMS回收器是多線程并發(fā)執(zhí)行的,因此是對CPU敏感的,比較占用CPU資源。
CMS回收器因?yàn)槭腔跇?biāo)記清除算法的,單純的進(jìn)行這種算法也會(huì)產(chǎn)生內(nèi)存碎片。當(dāng)無法分配大的內(nèi)存空間時(shí),會(huì)導(dǎo)致Full GC來整理內(nèi)存空間。
垃圾回收器——G1回收器
G1回收器和CMS在過程上有很多類似之處,只是稍有不同,但是兩個(gè)回收器的目的和實(shí)現(xiàn)方式時(shí)完全不一樣的。
G1回收器更專注于對于CPU資源的使用,充分發(fā)揮現(xiàn)代多核超線程CPU的優(yōu)勢。G1收集器不能確切的劃分為標(biāo)記清除算法、復(fù)制算法或者標(biāo)記整理算法,它在原來新生代老年代的基礎(chǔ)上將內(nèi)存劃分為多個(gè)區(qū)域Region,新生代和老年代都是由多個(gè)Region組成的集合。同時(shí),它會(huì)跟各個(gè)Region垃圾的多少對各個(gè)Region進(jìn)行優(yōu)先級劃分,這種將內(nèi)存化整為零的做法避免來對全部內(nèi)存的操作。
G1回收器的實(shí)現(xiàn)細(xì)節(jié)遠(yuǎn)比上面描述的要復(fù)雜,但是其過程也可以劃分為以下四步:
1.初始標(biāo)記
2.并發(fā)標(biāo)記
3.最終標(biāo)記
4.篩選回收
前面3個(gè)步驟都和CMS很類似,只不過是分Region進(jìn)行的,篩選回收則是對所有Region進(jìn)行篩選,只選擇對那些有必要的Region進(jìn)行垃圾回收。
Minor GC 和 Major GC / Full GC
這三種稱呼其實(shí)有點(diǎn)混亂,而且也只是對Java虛擬機(jī)垃圾回收的一種思考角度,不能代表虛擬機(jī)的垃圾回收算法的劃分。
Minor GC 和 Major GC、Full GC的分界還是很清晰的。Minor GC是指對年新生代的垃圾回收動(dòng)作,它的執(zhí)行頻率非常頻繁,回收速度也比較快。
Major GC是指對老年代的劃分,一般會(huì)伴隨一次Minor GC,一般速度較慢。Full GC可以理解為對整個(gè)堆的垃圾回收,其實(shí)和Major GC語意有點(diǎn)重復(fù),它的另一個(gè)語意是產(chǎn)生了STW。
對象的一生
我們現(xiàn)在已經(jīng)知道,從整體來說,對象是被分配在新生代和老年代中,新生代又被分為eden區(qū)域和survivo區(qū)域。當(dāng)一個(gè)創(chuàng)建時(shí),它優(yōu)先是被分配在新生代的eden區(qū)域的,但是大的對象(默認(rèn)3M,可以在JVM啟動(dòng)時(shí)設(shè)置)直接會(huì)被分配到老年代。JVM會(huì)為每個(gè)對象的“年齡”計(jì)數(shù)。當(dāng)存在于eden區(qū)域的對象經(jīng)歷過一次垃圾回收后,它就被移到survivor區(qū)域,同時(shí)它的年齡就被+1,當(dāng)它的年齡達(dá)到15(虛擬機(jī)啟動(dòng)時(shí)可通過參數(shù)配置)的時(shí)候就會(huì)被移到老年代。另外,虛擬機(jī)會(huì)survivor區(qū)域的大小是否充足,如果內(nèi)存不足,對象也將直接移至老年代。
三、類文件結(jié)構(gòu)
在分析類文件結(jié)構(gòu)前,我們先寫一個(gè)簡單的類:
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."":()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??????????????? ??#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??????????????? ??#46?=?Utf8???????????????SourceFile ??#47?=?Utf8???????????????TestClass.java ??#48?=?NameAndType????????#25:#26????????//?" ":()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." ":()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"
接下開,我們利用這兩個(gè)文件來簡單解釋下類文件的結(jié)構(gòu),順便會(huì)涉及到部分class加載的過程解析和虛擬機(jī)內(nèi)存模型的知識(shí)。實(shí)際上,我們開發(fā)過程中并不太可能需要去閱讀class文件,但了解class文件的結(jié)構(gòu)有助于我們理解和驗(yàn)證Java虛擬機(jī)的執(zhí)行的過程結(jié)構(gòu)。
常量池——constant pool
類的開始是一些基礎(chǔ)的描述,相信并不需要過多的解讀,真正需要理解的地方便是從constant pool這里開始,constant pool其實(shí)便是我經(jīng)常所說的常量池。
下面我們來解讀下常量池里內(nèi)容。
常量池中每一行便是一個(gè)常量,最左邊的#1、#2、#3……是常量的索引。常量分為字面量和符號引用兩種,符號引用會(huì)引用其他符號引用和字面量最終也可以解析成一個(gè)固定格式的值。
?“=” 號后的值如“Utf8”、“NameAndType”等都是常量的類型描述,常量的類型有很多種,需要了解更多的可結(jié)合資料和書籍去了解,我們只能注重于理解。在這里舉一個(gè)例子:索引#7的常量是一個(gè)String類型的常量,它的第三列是#56,這時(shí)我們看第二張圖,索引#56的常量是一個(gè)“Utf8”類型的常量,表示一個(gè)Uft8編碼的文本,也就是我們代碼里的“public final static String CONSTANT_VALUE = "我才是常量”;”這行中的值,這里編譯器是把“我才是常量”這個(gè)值生成了一個(gè)字面量,然后被#7引用,定義成了一個(gè)String類型的符號引用。
第三列是常量的值,如果是字面量,則會(huì)是“我才是常量”、1….這樣的值,如果是符號引用,則會(huì)是對其他常量的索引引用。
最后“//”后面的是對常量的注釋,如果是字面量,則沒有這一列,如果是符號引用則備注了符號引用的實(shí)際值。
常量池在虛擬機(jī)中非常重要,javac編譯(前端編譯)出的字節(jié)碼中代碼中大量引用到常量池中的值,如我們的代碼中:
這里的字節(jié)碼指令中#3、#4….等等都是對常量池中的常量的引用。而前端編譯是為了后面的類加載提供的基礎(chǔ)的。
字段表
字段表緊跟常量池之后,包含了我們在類中定義的類變量和實(shí)例變量,在我們的代碼中如下:
我們可以看到它描述了我們定義的CLASS_VARIABLE、CONSTANT_VALUE、CONSTANT_INT 這三個(gè)字段,表示出了它們的訪問權(quán)限、類型、返回值等等。同時(shí)在我們的常量池中,我們也可以看到它們的字段名被分別定義成#19、#21、#23這三個(gè)Utf8常量。
比較下CLASS_VARIABLE和CONSTANT_VALUE、CONSTANT_INT的區(qū)別,我們可以發(fā)現(xiàn)后面兩個(gè)變量多了一個(gè)ConstantValue屬性,這就是我們之前在介紹虛擬機(jī)內(nèi)存組成模塊時(shí)介紹常量池時(shí)所說的,只有同時(shí)被static和final修飾的才是常量,這一點(diǎn)很重要,常量的賦值是虛擬機(jī)自動(dòng)執(zhí)行,而類變量的賦值是在
方法表
方法表存在于字段表之后,我們可以注意到,正如我們才學(xué)Java時(shí)所知道的一樣,當(dāng)我們沒有寫構(gòu)造方法時(shí),編譯器會(huì)為我們默認(rèn)實(shí)現(xiàn)一個(gè)構(gòu)造方法(雖然當(dāng)時(shí)不知道原理,但確實(shí)是這樣的):
下面我們用我們代碼中的main函數(shù)來解析下方法表中方法的構(gòu)造。main函數(shù)的代碼如下:
它經(jīng)過前端編譯后的代碼如下:
我們來對應(yīng)著看,首先在字節(jié)碼的最上部描述出了方法的名稱、參數(shù)、返回信息、拋出的異常、訪問權(quán)限等等,這些都非常的直觀,我們不多做贅述。
Code屬性
Code屬性是方法表里核心信息,它將我們方法里的代碼描述成Java的字節(jié)碼指令,然后在虛擬機(jī)中執(zhí)行這些指令便是我們代碼的執(zhí)行(實(shí)際上還需要翻譯成匯編指令,翻譯行為又分為解釋執(zhí)行和編譯執(zhí)行兩種)。指令后不帶參數(shù)的都是對操作數(shù)棧(后面會(huì)解釋到)棧頂元素的操作,帶參數(shù)的指令需要結(jié)合常量池的索引翻譯成完整的指令。invoke*這樣的指令是對方法的調(diào)用,不過方法分很多種,invokevirtual指令是帶參數(shù)的,但也就是對棧頂對象實(shí)例方法的調(diào)用,調(diào)用棧頂實(shí)例的指定方法(我們這里是System.out的println方法,System.out就是我們的棧頂?shù)膶?shí)例)。這里又個(gè)很重要的知識(shí)點(diǎn),invokevirtual這樣的調(diào)用形式,雖然我們的指令形式都是一樣的,但是我們的棧頂對象是可變的,如果我們父類和子類都有同樣名字的方法,那么在棧頂?shù)膶ο笫歉割愡€是子類將決定我們調(diào)用的實(shí)際方法的不同,實(shí)際上,這便是Java中多態(tài)的實(shí)現(xiàn)基礎(chǔ)!
簡單介紹下這里的指令:
getstatic? ? 訪問類字段
invokevirtual?? ?? 調(diào)用虛方法,這只是方法調(diào)用的一種,后面我們會(huì)知道所有的方法調(diào)用指令。
invokestatic? ? 調(diào)用靜態(tài)方法
iconst_1? ? 將int類型的常量1加載到操作數(shù)棧
ldc? ? 將一個(gè)常量加載到操作數(shù)棧
return? ? 方法返回void
LineNumberTable
不知道大家有沒有想過這樣的問題:為什么我們在debug時(shí)候,開發(fā)工具能夠找到我們對應(yīng)的代碼的源碼呢?!有時(shí)候我們的class文件和我們的源碼版本不一樣,那debug時(shí)候就亂跑一通?!實(shí)際上就是這個(gè)LineNumberTable造成的,它的功能非常簡單,將方法中的指令的行號和源碼中的行號進(jìn)行對應(yīng),這一點(diǎn)我們從截圖中看它的形式便能夠非常輕松的理。在進(jìn)行前端編譯的時(shí)候我們可以選擇是否保留LineNumberTable,javac -g:none 選擇不保留,javac -g:lines選擇保留,當(dāng)然不保留時(shí),我們就無法從源碼中設(shè)置斷點(diǎn)了。
LocalVariableTable
這是個(gè)非常重要的部分,它描述了運(yùn)行時(shí)局部變量表中的變量和源碼中變量的關(guān)系,直觀的給我們展示了Java虛擬機(jī)運(yùn)行時(shí)的組成。前面我們在說Java虛擬機(jī)內(nèi)存組成的時(shí)候提到過Java虛擬機(jī)棧,它的棧貞的每一個(gè)元素便包含了一個(gè)局部變量表,方法的調(diào)用對應(yīng)著虛擬機(jī)棧的進(jìn)棧操作,調(diào)用結(jié)束后返回對應(yīng)著一個(gè)出棧操作。這是我們Java虛擬機(jī)執(zhí)行的系統(tǒng)的核心之一!不過這里不是運(yùn)行時(shí)的局部變量表,現(xiàn)在只是前端編譯階段,但是正如我們開始時(shí)所說的那樣,我們可以從class文件的結(jié)構(gòu)中窺探Java虛擬機(jī)運(yùn)行時(shí)的樣貌。它和LineNumberTable一樣,也不是Java虛擬機(jī)執(zhí)行時(shí)必須的,可以在前端編譯時(shí)選擇javac -g:none或者javac -g:vars來選擇取消或者生成這個(gè)部分,但是Java虛擬機(jī)運(yùn)行時(shí)一定會(huì)有對應(yīng)的局部變量表。
這里有個(gè)知識(shí)點(diǎn):如果方法是實(shí)例方法,那么局部變量表的第一個(gè)變量就是this,代表實(shí)例本身,這也是為何我們可以在實(shí)例方法中可以使用this關(guān)鍵字的原因。
字段表和方法表中還包含了很多其他很重要的屬性,但是我們無法寫完整,主要原因是:
1.我也不是很了解~
2.那太多啦!
我們只說了幾個(gè)能很好反應(yīng)虛擬機(jī)運(yùn)行機(jī)制的部分,能夠理解虛擬機(jī)的運(yùn)行就達(dá)到了我們的目的了。
Java中的異??刂屏鞒獭猼ry catch finally在字節(jié)碼中的體現(xiàn)
下面我們還是先看一下我們的示例代碼中的方法:
然后是它轉(zhuǎn)成字節(jié)碼后的代碼:
Java中的異??刂剖峭ㄟ^Exception table實(shí)現(xiàn)的,以我們代碼中的Exception table為例,它定義了try catch finally代碼塊執(zhí)行的三個(gè)流程:
????1.0-8行指令執(zhí)行,當(dāng)出現(xiàn)java/lang/NumberFormatException異常時(shí)跳轉(zhuǎn)到18行的指令。
????2.0-8行指令執(zhí)行,當(dāng)出現(xiàn)任何異常時(shí),跳轉(zhuǎn)至34行指令。
????3.18-24行的指令執(zhí)行,當(dāng)出現(xiàn)任何異常時(shí),跳轉(zhuǎn)至34行指令。
正好對應(yīng)了try catch finally的語言。
Java中方法的調(diào)用指令
invokevirtual 調(diào)用虛方法,指調(diào)用實(shí)例的方法(公共的方法)。
invokeinterface 調(diào)用接口方法,在運(yùn)行時(shí)找到實(shí)現(xiàn)接口方法的對象,調(diào)用對象的合適(方法的重載重寫)方法執(zhí)行。
invokespecial? ? 調(diào)用特殊的實(shí)例方法:實(shí)例初始化方法、私有方法、父方法
invokestatic? ? 調(diào)用類方法
invkedynamic? ? 這個(gè)比較特殊,是對動(dòng)態(tài)語言的支持,并且在Java編譯器中無法看到
Java中方法的調(diào)用指令很重要,它搭建出了Java用語言方法調(diào)用的基本特性。
四、Java虛擬機(jī)類加載機(jī)制
類加載的過程
java虛擬機(jī)加載類的過程可以細(xì)分為以下7個(gè)階段:
加載(Class Loading)
這個(gè)過程是指:
1.使用類的全限定名來獲取此類的二進(jìn)制流。
2.將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)。
3.生成一個(gè)代表這個(gè)類的lava.lang.Class對象,class對象雖然是一個(gè)對象,但是會(huì)存在方法區(qū)中(HotSpot虛擬機(jī)就是這么做的)。
Java虛擬機(jī)對類的加載是比較封閉的,字節(jié)流的獲取是我們少數(shù)能控制的部分,因此也產(chǎn)生了很多我們熟知的技術(shù):
從zip包中獲取,如我們熟知的jar、ear、war包
從網(wǎng)絡(luò)中獲取,如已經(jīng)沒落的Applet技術(shù)
代碼動(dòng)態(tài)生成的,如jdk的動(dòng)態(tài)代理cglib技術(shù)
由其他文件生成,如jsp(jsp生成的字節(jié)流的加載器有些特殊,每一次jsp文件的加載變會(huì)生成一個(gè)新的加載器,這個(gè)加載器生成的目的就是為了被廢棄)
假如我們看了jdk的類加載器的代碼,就會(huì)知道,實(shí)際上,每個(gè)類加載器都會(huì)有一個(gè)定義自己管轄的目錄的構(gòu)造方法,然后在這個(gè)路徑下取字節(jié)流。
這里有一個(gè)特殊的情況,那就是對于數(shù)組的加載,我們知道數(shù)組不屬于java的基本數(shù)據(jù)類型,也不是一個(gè)簡單的引用類型,沒有對應(yīng)的Class,實(shí)際上數(shù)組對象是由Java虛擬機(jī)直接創(chuàng)建的。數(shù)組去掉維度后的類型如果是引用類型則會(huì)觸發(fā)這個(gè)引用類型的加載數(shù)組類型的可見性和引用類型保持一致;如果是基礎(chǔ)數(shù)據(jù)類型,則可見性為public。
驗(yàn)證
這個(gè)階段的目的就是為了保證Class字節(jié)流中包含的信息符合虛擬機(jī)的要求,并且不會(huì)危害到虛擬機(jī)的安全。這個(gè)過程我覺得只要知道大概意思即可,沒有必要過多研究。
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量的初始值的階段,類變量如果是基本數(shù)據(jù)類型或者String類型的數(shù)據(jù),則內(nèi)存劃分是在方法區(qū)中進(jìn)行的。這里我們?nèi)匀灰晕覀兊闹暗淖止?jié)碼文件證明一下:
在我們的字節(jié)碼文件中的最后部分,我們能看到一個(gè)static{}代碼塊,實(shí)際上這個(gè)便是虛擬機(jī)生成的
對于實(shí)例變量類型的類變量,自然不用說,它一定是在Java堆中劃分內(nèi)存的。
非引用類型的類變量的初始值都是0值,但是我們還是需要注意,常量和類變量的區(qū)別,同時(shí)被static和final修飾的變量也就是常量的初始值會(huì)被直接賦值為對應(yīng)的ConstantValue的值,這個(gè)我們在前面已經(jīng)說到過了。
解析
解析是將常量池中的符號引用翻譯為直接引用的過程。在之前我們已經(jīng)知道,常量池中包含兩種類型的數(shù)據(jù):字面量和符號引用。字面量包含基本數(shù)據(jù)類型和String類型的數(shù)值,符號引用引用開其他符號引用或者字面量。但是虛擬機(jī)執(zhí)行不會(huì)在執(zhí)行的時(shí)候去翻譯這些符號引用,而是在解析階段就將其翻譯為直接引用,即句柄或者指針。
解析過程的觸發(fā)是在虛擬機(jī)指令操作符號引用時(shí)觸發(fā)。
類變量的初始化
根據(jù)我的理解,解析和初始化過程是交替進(jìn)行的,應(yīng)該沒有嚴(yán)格的先后順序。
再次看一下我們之前的示例的字節(jié)碼的最后部分:
在之前的準(zhǔn)備階段,我們已經(jīng)提到,虛擬機(jī)會(huì)自動(dòng)生成
對類變量進(jìn)行賦值,這個(gè)我們在實(shí)例代碼中已經(jīng)可以清楚的看到。
第二種是源碼中的staic代碼塊中的內(nèi)容將會(huì)被生成到
虛擬機(jī)啟動(dòng)時(shí)并不會(huì)立刻將所有代碼里的所有類都加載到虛擬機(jī)中,而是在運(yùn)行時(shí)動(dòng)態(tài)加載的,當(dāng)運(yùn)行中遇到下面情況時(shí),虛擬機(jī)會(huì)加載類:
遇到new、getstatic、putstatic、invokestatic這4條指令時(shí),如果類沒有加載則出發(fā)類的加載過程。注意:使用static final修飾的常量時(shí),并不是使用getstatic指令,并不會(huì)觸發(fā)類的加載。
使用java.lang.reflect包的方法進(jìn)行反射調(diào)用時(shí),如果類沒有被初始化
初始化一個(gè)類時(shí),發(fā)現(xiàn)其父類還沒有被初始化,則先初始化其父類
虛擬機(jī)啟動(dòng)時(shí)main方法所在的類會(huì)被優(yōu)先初始化。
Java支持動(dòng)態(tài)語言時(shí)解析出的REF_getstatic、REF_putstatic、REF_invokestatic的方法句柄對應(yīng)的類沒有初始化則先進(jìn)行初始化。
關(guān)于
首先它不是必要的,當(dāng)一個(gè)類中既沒有類變量,也沒有static代碼塊時(shí),Java虛擬機(jī)不會(huì)產(chǎn)生
當(dāng)執(zhí)行一個(gè)類的
類加載器:
首先,類加載器的作用是完成類的加載動(dòng)作的,即類加載的第一個(gè)階段。Java中可以擁有很多個(gè)類加載器,其中有虛擬機(jī)提供的,也會(huì)有自定義的類加載器。每一個(gè)類加載器都有其類命名空間,當(dāng)一個(gè)Class的字節(jié)碼由不同類加載器加載時(shí),那么它們就是不相同的類。
Java中的類加載器和雙親委派模型
虛擬機(jī)的類加載器可分為兩種:一種是虛擬機(jī)提供的加載器——啟動(dòng)加載器,由C++實(shí)現(xiàn);另一種是由Java語言實(shí)現(xiàn)的類加載器,它們都繼承于抽象類java.lang.ClassLoader。
從另一個(gè)維度,按功能劃分,我們可以將Java中默認(rèn)提供的類加載器分為以下幾種:
啟動(dòng)類加載器(Bootstrap ClassLoader),它的作用就是將javahome\lib目錄下或者指定的-Xbootclasspath目錄下的類庫按名字查找并加載到虛擬機(jī)中。比如我們的rt.jar,如果改名叫其他名字,那么,即使它在以上目錄下,那么它也不能正常被加載。它是由C++實(shí)現(xiàn)的,是Java虛擬機(jī)的組成部分。
擴(kuò)展類加載器(ExtClassLoader),它是用來加載“java.ext.dirs”系統(tǒng)變量指定目錄下的類庫的。它是由Java語言實(shí)現(xiàn)的。
應(yīng)用加載器(AppClassLoader),它和ExtClassLoader一樣都是在sun.misc.Launcher類中的內(nèi)部類,負(fù)責(zé)加載classpath中的指定的類庫。
自定義的類加載器。我們自己用java代碼實(shí)現(xiàn)的類加載器。
加載器的雙親委派模型
如果一個(gè)類加載器收到了類加載的請求,它首先不會(huì)嘗試自己去加載這個(gè)類,而是委派給父類加載器去完成,所以每一次加載請求會(huì)先傳到頂部的啟動(dòng)加載器,當(dāng)父類加載器無法完成加載請求時(shí)子類加載器才會(huì)去嘗試加載類。加載器雙親委派模型如下:
假如我們自己實(shí)現(xiàn)了一個(gè)java.lang.Object類,因?yàn)橛须p親委派模型的存在,類加載請求最終會(huì)被轉(zhuǎn)到啟動(dòng)加載器中去,而啟動(dòng)加載器只會(huì)在自己管轄的路徑里去查找類,所以我們無法自己寫一個(gè)Object類放到classpath中去替換jdk提供的Object類(實(shí)際上我們可以下載Openjdk去修改代碼并編譯)。
五、虛擬機(jī)字節(jié)碼執(zhí)行引擎
?? ??? ?大學(xué)時(shí)候我們學(xué)習(xí)編譯原理時(shí)候,我們知道程序執(zhí)行分為兩種:解釋執(zhí)行和編譯執(zhí)行。解釋執(zhí)行不提前編譯代碼,通過解釋器去執(zhí)行代碼;編譯執(zhí)行則預(yù)先編譯好代碼產(chǎn)生本地代碼去執(zhí)行,而Java程序運(yùn)行中時(shí)時(shí)進(jìn)行編譯和優(yōu)化的技術(shù)叫做JIT。我們將java代碼編譯成class文件的動(dòng)作被稱為前端編譯,后期將class文件的內(nèi)容編譯為本地文件的工作叫做即時(shí)編譯(JIT)??傮w來說,解釋執(zhí)行的有點(diǎn)是啟動(dòng)速度快,而編譯執(zhí)行的優(yōu)點(diǎn)則是執(zhí)行效率快。
?? ?? ? 前面我們在說Java內(nèi)存模塊的時(shí)候提到過虛擬機(jī)棧,它是Java虛擬機(jī)執(zhí)行的根本構(gòu)造,它是屬于線程的,每個(gè)線程都擁有自己的方法棧。虛擬機(jī)棧中的一個(gè)棧幀對應(yīng)了一次方法的調(diào)用,所有方法的調(diào)用在一起便是我們程序的執(zhí)行!?Java中運(yùn)行時(shí)內(nèi)存模型是工作內(nèi)存——主內(nèi)存的模型,主內(nèi)存負(fù)責(zé)存儲(chǔ)數(shù)據(jù),工作內(nèi)存從主內(nèi)存獲取數(shù)據(jù)的副本在工作內(nèi)存中運(yùn)算,結(jié)束后將變量的值存儲(chǔ)到主內(nèi)存 。虛擬機(jī)棧就是我們的工作內(nèi)存,下圖展示了棧幀的基本結(jié)構(gòu):
這個(gè)圖簡單的示意了虛擬機(jī)棧的結(jié)構(gòu),實(shí)際上虛擬機(jī)棧實(shí)現(xiàn)的時(shí)候,相鄰棧幀的操作數(shù)棧和局部變量表會(huì)設(shè)計(jì)成相交的,以實(shí)現(xiàn)方法調(diào)用的返回值。
局部變量表
?? ??? ?這個(gè)我們在解釋class文件結(jié)構(gòu)時(shí)便介紹過,在此我們可以前后照應(yīng)。局部變量表的作用是存放方法的參數(shù)和方法內(nèi)定義的局部變量,局部變量表的最小單位以solt計(jì)算,每一個(gè)slot中存放著boolean、byte、char、short、int、float、reference和returnAddress類型的數(shù)據(jù),long和double類型的數(shù)據(jù)則分配兩個(gè)連續(xù)的slot存儲(chǔ)。reference就是我們通常說的引用,它分為句柄和指針兩種。returnAdress現(xiàn)在不怎么使用了,最初被用來實(shí)現(xiàn)異常處理,現(xiàn)在已經(jīng)被異常表代替。
?? ?? ?如果我們讀過《effectiv java》這本書,應(yīng)該會(huì)對書中有一章有所印象,這章提到要盡量最小化變量的作用域,在這里我們可以得到印證。因?yàn)榫植孔兞勘淼拿總€(gè)slot是可以復(fù)用的減少變量的作用域不僅可以減少局部變量表的長度,而且確定不用的變量會(huì)被垃圾回收器回收掉。
?? ?? 如果方法是實(shí)例方法,那么局部變量表的第0位存放則是這個(gè)實(shí)例的reference,也就是我們一直用的this!
操作數(shù)棧
? ? ??前面我們解釋class文件方法中的Code屬性時(shí)介紹過,字節(jié)碼指令有的是不帶參數(shù)的,而不帶參數(shù)的指令則操作的目標(biāo)則是操作數(shù)棧中的數(shù)據(jù),例如字節(jié)碼中的iadd則是將操作數(shù)棧棧頂?shù)膬蓚€(gè)int數(shù)據(jù)相加,ldc指令則是將常量放入操作數(shù)棧的棧頂。
方法的調(diào)用
Java中方法的調(diào)用指令有以下5種:
invokevirtual 調(diào)用虛方法,指調(diào)用實(shí)例的方法(公共的方法)。
invokeinterface 調(diào)用接口方法,在運(yùn)行時(shí)找到實(shí)現(xiàn)接口方法的對象,調(diào)用對象的合適(方法的重載重寫)方法執(zhí)行。
invokespecial? ? 調(diào)用特殊的實(shí)例方法:實(shí)例初始化方法、私有方法、父方法
invokestatic? ? 調(diào)用類方法
invkedynamic? ? 這個(gè)比較特殊,是對動(dòng)態(tài)語言的支持,支持了lambda表達(dá)式的語法。
在我們之前說的類加載過程的解釋過程中,invokespecial和invokestatic指令帶的符號引用已經(jīng)被翻譯成方法的入口地址,它們的調(diào)用時(shí)固定的,因此它們被稱為非虛方法的調(diào)用。invokeinterface和invokevirtual調(diào)用的時(shí)候需要根據(jù)操作數(shù)棧棧頂?shù)膶ο髞慝@取調(diào)用方法的實(shí)際入口地址,它們則被稱為虛方法的調(diào)用。
方法的靜態(tài)分派
接下來,我們用書上的例子來解釋下方法的靜態(tài)分派:
這個(gè)例子的執(zhí)行結(jié)果大家應(yīng)該沒有什么異議的,不論方法的實(shí)際類型時(shí)什么,虛擬機(jī)執(zhí)行的結(jié)果都是“hello,guy”。這是因?yàn)閮蓚€(gè)sayHello方法調(diào)用的方法在編譯期間已經(jīng)確定,我們傳入的參數(shù)woman和man的靜態(tài)類型(Static Type)都是Human,因此調(diào)用的都是參數(shù)類型為Human的方法。
我們來看下這個(gè)代碼編譯后調(diào)用sayHello方法時(shí)的字節(jié)碼指令:
從后面對#13符號引用的備注我們可以很明顯的看到調(diào)用的方法在前端編譯期已經(jīng)被確定是參數(shù)類型是Human的sayHello方法。這個(gè)跟我們后面要說的動(dòng)態(tài)分配可以做個(gè)對比,可以很清晰地從字節(jié)碼層面理解動(dòng)態(tài)分配和靜態(tài)分配的區(qū)別。
方法的靜態(tài)分配按照靜態(tài)類型分配是它叫做靜態(tài)分配的主要原因,不過我覺得這個(gè)分配是在編譯期已經(jīng)確定了的也是它叫做這個(gè)名字的另一個(gè)主要原因吧。
關(guān)于方法的重載overload,靜態(tài)分配會(huì)在編譯期決定了到底調(diào)用哪個(gè)版本的代碼,不過如果方法的參數(shù)個(gè)數(shù)一致,編譯期在確定版本的時(shí)候會(huì)按照一定的規(guī)則來確定死亡(這個(gè)規(guī)則比較難用語言描述,編譯器會(huì)選擇最合適的版本),例如上面的例子,我們把參數(shù)類型為Man的sayHello方法注釋掉,然后main方法里man的類型聲明為Man,則代碼執(zhí)行的結(jié)果會(huì)是這樣的:
這里,我們把參數(shù)類型為Man的方法已經(jīng)注釋掉了,按照靜態(tài)類型分配,已經(jīng)無法分配到參數(shù)類型為Man的方法了,于是編譯期給我們分配到了參數(shù)類型為Human的方法。
再用《深入理解Java虛擬機(jī)》這本書上的例子來更好地演示下編譯期在靜態(tài)分配上做的最合適的選擇:
這里我們重寫了很多sayHello方法,當(dāng)我們調(diào)用時(shí)使用’a’作為參數(shù)時(shí),編譯器給我們選擇的最優(yōu)方法是sayHello(char arg),這時(shí)候我們看編譯后的結(jié)果:
方法調(diào)用選擇的是sayHello:(C)V,這里的C便是char類型。但是當(dāng)我們把sayHello(char arg)這個(gè)方法注釋掉,那么編譯的結(jié)果就會(huì)是這樣:
編譯期把同一段代碼編譯成了sayHello:(I)V這個(gè)不一樣的結(jié)果(I表示int),有興趣的可以逐個(gè)注釋掉這些方法,看看編譯器選擇的優(yōu)先級。
方法的動(dòng)態(tài)分派
前面說的靜態(tài)分派是前端編譯期已經(jīng)決定的,動(dòng)態(tài)分派則不是,它是在虛擬機(jī)運(yùn)行期間對象的實(shí)際類型來確定執(zhí)行的方法的,這也是它名字的由來。
下面我們還是用《深入理解Java虛擬機(jī)》這本書上的例子結(jié)合編譯后的字節(jié)碼來演示下什么叫動(dòng)態(tài)分派:
注意:我們這里的sayHello方法都是沒有參數(shù)的,或者說參數(shù)一樣的,無法通過靜態(tài)分配區(qū)分出。
相信任何Java程序員對這段代碼的結(jié)果應(yīng)該都沒有異議,這非常面向?qū)ο蟆?那么接下來我們需要從Java虛擬機(jī)的角度來考慮,為什么結(jié)果會(huì)是這樣的!首先,我們還是來看一下這段代碼中main函數(shù)編譯后的字節(jié)碼:
?? ?從字節(jié)碼的LineNumberTable中可以看出,源碼中三句sayHello方法——23行、24行、26行的調(diào)用分別對應(yīng)著Code屬性里的16-17行、20-21行、32-33行。這里的一行源碼被編譯成了兩行字節(jié)碼指令,這是為什么能?
?? ?我們之前說過Java虛擬機(jī)運(yùn)行時(shí)內(nèi)存模型時(shí)說過操作數(shù)棧這個(gè)概念,它存儲(chǔ)了方法執(zhí)行過程中的臨時(shí)數(shù)據(jù)。aload_ 這個(gè)時(shí)候,我們看到的對方法的描述都是“Method me/wxh/clazzstd/DynamicDispatch$Human.sayHello:()V”,但是卻因?yàn)闂m斣氐牟煌?,?zhí)行了不同的方法。實(shí)際上我們在介紹靜態(tài)分配時(shí)舉的第一個(gè)例子編譯的字節(jié)碼也是invokevirtual,不過那里是同一個(gè)對象,()V里面的參數(shù)類型限定符的不同,這里是限定符相同,對象不同。 ?? ? ?? ?對于invokevirtual指令,它的解析過程大致是這樣的: 找到棧頂?shù)牡谝粋€(gè)元素所指的對象的實(shí)際類型,記做C 如果在類型C中找到常量的中描述符和簡單名稱都相符的方法,則進(jìn)行方法的權(quán)限檢驗(yàn),如果通過則返回這個(gè)方法的直接引用,查找過程結(jié)束;如果不通過則返回IllealAccessError 否則按照繼承關(guān)系從下往上對C的各個(gè)父類進(jìn)行第2步查找 如果最終沒有找到合適的方法,則拋出java.lang.AstractMethodError ?? ? ? ? 順便我們利用這個(gè)代碼在驗(yàn)證下我們之前說的Java虛擬機(jī)運(yùn)行時(shí)的java虛擬機(jī)棧的概念,代碼里我們new里三個(gè)對象,我們拿第一個(gè)new的man的對象來解釋下,代碼很簡單:Human man = new Man();它編譯后的字節(jié)碼指令對應(yīng)為: 我們來解釋下這四行字節(jié)碼: 0行:new指令創(chuàng)建me/wxh/clazzstd/DynamicDispatch$Man類的實(shí)例,這個(gè)時(shí)候操作數(shù)棧棧頂會(huì)有一個(gè)這個(gè)實(shí)例的引用。 3行:dup指令將棧頂?shù)脑貜?fù)制一份再壓回棧頂,這個(gè)時(shí)候操作數(shù)棧有兩個(gè)一個(gè)一樣的me/wxh/clazzstd/DynamicDispatch$Man類的實(shí)例的引用 4行:invokespecial指令調(diào)用類實(shí)例的 7行:astore_1指令將操作數(shù)棧棧頂?shù)淖兞看鎯?chǔ)到局部變量表的第1位(實(shí)例方法的第0位是this保留的,可能靜態(tài)方法也保留了只不過沒有值,這個(gè)有待考證,不過不影響我們介紹流程),這個(gè)時(shí)候操作數(shù)棧第二個(gè)me/wxh/clazzstd/DynamicDispatch$Man類的實(shí)例的引用也出棧了。 花了這么多時(shí)間來舉這個(gè)例子,我覺得是非常值得的,通過它,我們知道了局部變量表和操作數(shù)棧是如何協(xié)同工作的了 動(dòng)態(tài)語言支持 首先對動(dòng)態(tài)語言的理解:拿javascript舉例,變量的聲明都是“var”,變量是不明確類型的,變量的值才具有類型,方法的調(diào)用在運(yùn)行時(shí)才去判斷。 之前說過的invokedynamic指令是java對動(dòng)態(tài)語言的支持,但是在java7及以下版本是看不到invokedynamic,并且invokedynamic指令設(shè)計(jì)的目的也是提高Java虛擬機(jī)對動(dòng)態(tài)語言的支持,使得其他在java虛擬機(jī)上能支持動(dòng)態(tài)語言的執(zhí)行。 java7的動(dòng)態(tài)語言支持——java.lang.invoke.MethodHandle 下面是一個(gè)java.lang.invoke.MethodHandle的使用例子,來自于《深入理解java虛擬機(jī)》這本書: 但是這個(gè)類的字節(jié)碼中我們無法找到invokedynamic指令,有興趣的可以用javap看一下,不需要用java7去編譯。 java8后的lambda表達(dá)式 下面我們用一個(gè)lambda表達(dá)式的例子來看一下invokedynamic指令是什么樣子的。 源代碼: javap查看字節(jié)碼: 第一個(gè)紅圈對應(yīng)的是 Arrays.sort(names, new Comparator ??? @Override ??? public int compare(String o1, String o2) { ??????? return o1.compareTo(o2); ??? } }); 這個(gè)代碼,我們可以清楚看到,產(chǎn)生了一個(gè)LambdaTest$1這個(gè)匿名內(nèi)部類,并new了一個(gè)它的對象。 第二個(gè)紅圈對應(yīng)的代碼是 list.forEach((String name) -> { ??? System.out.println(name); }); 這個(gè)用lambda表達(dá)式產(chǎn)生的字節(jié)碼,這里我可以看出,編譯器產(chǎn)生的是一個(gè)invokedynamic指令。
當(dāng)前題目:java虛擬機(jī)學(xué)習(xí)筆記
轉(zhuǎn)載源于:http://weahome.cn/article/ijheeg.html