Java字節(jié)碼介紹
學(xué)習(xí) Java 的都知道,我們所編寫的?.java?代碼文件通過編譯將會(huì)生成?.class?文件,最初的方式就是通過 JDK 的?javac?指令來編譯,再通過?java?命令執(zhí)行 main 方法所在的類,從而執(zhí)行我們的 Java 程序。而在這中間所生成的 .class 文件中的內(nèi)容,就是 JVM 可以處理運(yùn)行的字節(jié)碼(Byte Code),它由 JVM 解釋為對應(yīng)系統(tǒng)可運(yùn)行的機(jī)器指令,這也是我們的 Java 程序能夠做到一處編譯處處執(zhí)行的原理。
什么是字節(jié)碼
Java之所以可以“一次編譯,到處運(yùn)行”。
-
??一是因?yàn)镴VM針對各種操作系統(tǒng)、平臺(tái)都進(jìn)行了定制。
-
??二是因?yàn)闊o論在什么平臺(tái),都可以編譯生成固定格式的字節(jié)碼(.class文件)供JVM使用。
因此,也可以看出字節(jié)碼對于Java生態(tài)的重要性。之所以被稱之為字節(jié)碼,是因?yàn)樽止?jié)碼文件由十六進(jìn)制值組成,而JVM以兩個(gè)十六進(jìn)制值為一組,即以字節(jié)為單位進(jìn)行讀取。在Java中一般是用javac命令編譯源代碼為字節(jié)碼文件,一個(gè).java文件從編譯到運(yùn)行的示例如下:
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)對于 Java 開發(fā)人員來說,平時(shí)需要閱讀 Byte Code 的場景比較少,但和閱讀框架源碼能夠了解到框架的設(shè)計(jì)思路一樣,閱讀 Java Byte Code 也有利于我們理解 Java 一些深層的東西,提高我們解決問題的能力。能夠閱讀 Byte Code 也有利于我們?nèi)ダ斫?Kotlin 或其它運(yùn)行在 JVM 上的語言,是如何擴(kuò)展 Java 所沒有的特性或語法。
字節(jié)碼文件結(jié)構(gòu)
首先我們先編寫一個(gè)簡單的 Java 代碼作為演示例子,然后編譯這個(gè)?Hello.java?文件得到?Hello.class?文件。我們知道 .class 是二進(jìn)制文件,它無法被直接查看,當(dāng)然我們可以通過一些二進(jìn)制文件查看工具來閱讀里面的內(nèi)容。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)編譯后生成.class文件,打開后是一堆十六進(jìn)制數(shù),按字節(jié)為單位進(jìn)行分割后展示如上圖右側(cè)部分所示。上文提及過,JVM對于字節(jié)碼是有規(guī)范要求的,那么看似雜亂的十六進(jìn)制符合什么結(jié)構(gòu)呢?JVM規(guī)范要求每一個(gè)字節(jié)碼文件都要由十部分按照固定的順序組成,整體結(jié)構(gòu)如下圖:
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)魔數(shù)
一個(gè)符合標(biāo)準(zhǔn)的?.class?文件是以?CA FE BA BE?開頭,這個(gè)四個(gè)字節(jié)均為魔數(shù),JVM 根據(jù)這個(gè)開頭來判斷一個(gè)文件是否可能為?.class?文件,如果是才會(huì)繼續(xù)執(zhí)行。
有趣的是,魔數(shù)的固定值是Java之父James Gosling制定的,為CafeBabe(咖啡寶貝),而Java的圖標(biāo)為一杯咖啡。
版本號
魔數(shù)后面四個(gè)字節(jié)?00 00 00 34?是版本號,前兩個(gè)字節(jié)為次版本號,后兩個(gè)字節(jié)為主版本號,在對主版本號進(jìn)行轉(zhuǎn)換可以得到 52,該序號對應(yīng)的Java 版本為1.8。
常量池(Constant Pool)
在版本號后面則是常量池(Constant Pool),它包含常量池計(jì)數(shù)器和常量池?cái)?shù)據(jù)區(qū)兩個(gè)部分。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)-
??常量池計(jì)數(shù)器(constant_pool_count):由于常量的數(shù)量不固定,所以需要先放置兩個(gè)字節(jié)來表示常量池容量計(jì)數(shù)值。圖2中示例代碼的字節(jié)碼前10個(gè)字節(jié)如下圖所示,將十六進(jìn)制的24轉(zhuǎn)化為十進(jìn)制值為36,排除掉下標(biāo)“0”,也就是說,這個(gè)類文件中共有35個(gè)常量。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)-
??常量池?cái)?shù)據(jù)區(qū):數(shù)據(jù)區(qū)是由(constant_pool_count-1)個(gè)cp_info結(jié)構(gòu)組成,一個(gè)cp_info結(jié)構(gòu)對應(yīng)一個(gè)常量。在字節(jié)碼中共有14種類型的cp_info,每種類型的結(jié)構(gòu)都是固定的。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)
訪問標(biāo)志
常量池結(jié)束之后的兩個(gè)字節(jié),描述該Class是類還是接口,以及是否被public、abstract、final等修飾符修飾。JVM規(guī)范規(guī)定了如下圖的訪問標(biāo)志(Access_Flag)。
需要注意的是,JVM并沒有窮舉所有的訪問標(biāo)志,而是使用按位或操作來進(jìn)行描述的,比如某個(gè)類的修飾符為public final,則對應(yīng)的訪問修飾符的值為ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)當(dāng)前類名
訪問標(biāo)志后的兩個(gè)字節(jié),描述的是當(dāng)前類的全限定名。這兩個(gè)字節(jié)保存的值為常量池中的索引值,根據(jù)索引值就能在常量池中找到這個(gè)類的全限定名。
父類名稱
當(dāng)前類名后的兩個(gè)字節(jié),描述父類的全限定名,同上,保存的也是常量池中的索引值。
接口信息
父類名稱后為兩字節(jié)的接口計(jì)數(shù)器,描述了該類或父類實(shí)現(xiàn)的接口數(shù)量。緊接著的n個(gè)字節(jié)是所有接口名稱的字符串常量的索引值。
字段表
字段表用于描述類和接口中聲明的變量,包含類級別的變量以及實(shí)例變量,但是不包含方法內(nèi)部聲明的局部變量。
字段表也分為兩部分,
-
??第一部分為兩個(gè)字節(jié),描述字段個(gè)數(shù);
-
??第二部分是每個(gè)字段的詳細(xì)信息fields_info。
字段表結(jié)構(gòu)如下圖
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)方法表
字段表結(jié)束后為方法表,方法表也是由兩部分組成,第一部分為兩個(gè)字節(jié)描述方法的個(gè)數(shù);第二部分為每個(gè)方法的詳細(xì)信息。方法的詳細(xì)信息較為復(fù)雜,包括方法的訪問標(biāo)志、方法名、方法的描述符以及方法的屬性,如下圖所示:
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)方法的權(quán)限修飾符依然可以通過訪問標(biāo)志查詢得到,方法名和方法的描述符都是常量池中的索引值,可以通過索引值在常量池中找到。
當(dāng)我們擁有一個(gè)?.class?文件時(shí),我們可以通過?javap?來將字節(jié)碼指令轉(zhuǎn)換為助記符,這個(gè)命令有一些參數(shù),你可以通過?javap -help?來查看所有參數(shù)的說明,這里為了顯示盡量詳細(xì)的內(nèi)容,使用?javap -verbose,其效果如下,但由于內(nèi)容太長,我們不一次性展示所有內(nèi)容,而是分區(qū)域來進(jìn)行閱讀。
而“方法的屬性”這一部分較為復(fù)雜,直接借助javap -verbose將其反編譯為人可以讀懂的信息進(jìn)行解讀,如下圖所示??梢钥吹綄傩灾邪ㄒ韵氯齻€(gè)部分:
-
??Code區(qū):源代碼對應(yīng)的JVM指令操作碼,在進(jìn)行字節(jié)碼增強(qiáng)時(shí)重點(diǎn)操作的就是“Code區(qū)”這一部分。 args_size 是參數(shù)數(shù)量,在主函數(shù)中,因?yàn)橛?args 這個(gè)參數(shù),所以在這里 args_size 為 1; locals 是該方法中的本地變量有多少個(gè),在我們的主函數(shù)里面有定義了 3 個(gè)變量,加上一個(gè)參數(shù),因此有 4 個(gè)變量; stack 是方法在執(zhí)行過程中,操作數(shù)棧中最大深度,這個(gè)在之后講解指令執(zhí)行過程時(shí)可以看出。 在這一行信息之后是字節(jié)碼指令,一條指令包括偏移量以及執(zhí)行的指令碼,PC Register 利用偏移量來判斷指令執(zhí)行位置。
-
??LineNumberTable:行號表,將Code區(qū)的操作碼和源代碼中的行號對應(yīng),Debug時(shí)會(huì)起到作用(源代碼走一行,需要走多少個(gè)JVM指令操作碼)。
LineNumberTable:?line 3: 0 代表 Java 源碼文件中的第三行代碼從偏移量為 0 的位置開始,而繼續(xù)往下看可以看到第四行代碼從偏移量為 2 的位置開始,也就是說第三行代碼所對應(yīng)的字節(jié)碼指令有 iconst_1 和 istore_1 兩條。這也可以讓 JVM 執(zhí)行指令出現(xiàn)錯(cuò)誤時(shí),幫助我們定位到對應(yīng)的源碼位置。
??line?3:?0
??line?4:?2
??line?5:?4
??line?6:?10
??line?7:?17? -
??LocalVariableTable:本地變量表,包含This和局部變量,之所以可以在每一個(gè)方法內(nèi)部都可以調(diào)用This,是因?yàn)镴VM將This作為每一個(gè)方法的第一個(gè)參數(shù)隱式進(jìn)行傳入。當(dāng)然,這是針對非Static方法而言。 第一個(gè)屬性 start 為這個(gè)變量可見的起始偏移位置,它的值必須是在Code 中存在的偏移量值。 第二個(gè)屬性 length 為該變量的有效長度,在這個(gè)例子中,我們的變量直到方法末尾都有效,因此你會(huì)發(fā)現(xiàn) start + lenth 的值都是 18 (方法中執(zhí)行的指令數(shù))。當(dāng)我們在一個(gè)局部的代碼塊里面聲明一個(gè)變量,那么它的有效期長度將會(huì)更短。 Slot 為變量在 local variable 中的位置,這可以幫助我們在指令中確定對應(yīng)的變量,而 Name 則是變量名,Signature 為該變量的類型。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)附加屬性表
字節(jié)碼的最后一部分,該項(xiàng)存放了在該文件中類或接口所定義屬性的基本信息。
操作數(shù)棧和字節(jié)碼
JVM的指令集是基于棧而不是寄存器,基于??梢跃邆浜芎玫目缙脚_(tái)性(因?yàn)榧拇嫫髦噶罴陀布煦^),但缺點(diǎn)在于,要完成同樣的操作,基于棧的實(shí)現(xiàn)需要更多指令才能完成(因?yàn)闂V皇且粋€(gè)FILO結(jié)構(gòu),需要頻繁壓棧出棧)。另外,由于棧是在內(nèi)存實(shí)現(xiàn)的,而寄存器是在CPU的高速緩存區(qū),相較而言,基于棧的速度要慢很多,這也是為了跨平臺(tái)性而做出的犧牲。
我們在上文所說的操作碼或者操作集合,其實(shí)控制的就是這個(gè)JVM的操作數(shù)棧。為了更直觀地感受操作碼是如何控制操作數(shù)棧的,以及理解常量池、變量表的作用,將add()方法的對操作數(shù)棧的操作制作為GIF,如下圖14所示,圖中僅截取了常量池中被引用的部分,以指令iconst_2開始到ireturn結(jié)束。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)JVM 內(nèi)存結(jié)構(gòu)
我們的Java程序在運(yùn)行時(shí)是通過?main()?方法啟動(dòng),它是程序的入口,我們的進(jìn)程在啟動(dòng)時(shí)會(huì)為該方法創(chuàng)建一個(gè)主線程來執(zhí)行代碼。當(dāng)我們使用多線程時(shí),那么程序的進(jìn)程將會(huì)擁有多個(gè)線程。每個(gè)線程的資源都擁有獨(dú)自的資源,當(dāng)然它們也可以共享進(jìn)程的資源,那么在 JVM 中,根據(jù)資源的可用范圍,可將內(nèi)存區(qū)域分為線程獨(dú)占和線程共享兩個(gè)類別。JVM內(nèi)存布局
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)對于每一個(gè)線程,都可將其擁有的內(nèi)存空間分為 PC Register、Native Method Stack、JVM Stack 這3個(gè)區(qū)域,這3個(gè)區(qū)域?qū)τ诰€程來說都是獨(dú)占的,其它線程無法進(jìn)行訪問。
-
??PC Register?用于記錄當(dāng)前線程指令的執(zhí)行位置。由于一個(gè)進(jìn)程可能有多個(gè)線程,而CPU會(huì)在不同線程之間切換,為了能夠記錄各個(gè)線程的當(dāng)前執(zhí)行的指令,每個(gè)線程都需要有一個(gè) PC Register,來保證各個(gè)線程都可以進(jìn)行獨(dú)立運(yùn)算。
-
??JVM Stack?用于存放調(diào)用方法時(shí)壓入棧的棧幀。相信學(xué)過數(shù)據(jù)結(jié)構(gòu)的對棧應(yīng)該不陌生,JVM Stack 壓入的單位為棧幀(Frame),用于存儲(chǔ)數(shù)據(jù)、動(dòng)態(tài)鏈接、方法返回值和調(diào)度異常等。每次調(diào)用一個(gè)方法都會(huì)創(chuàng)建一個(gè)新的棧幀壓入 JVM Stack 來存儲(chǔ)該方法的信息,當(dāng)該方法調(diào)用完成時(shí),對應(yīng)的棧幀也會(huì)跟著被銷毀。一個(gè)棧幀都有自己的局部變量數(shù)組、操作數(shù)棧、對當(dāng)前方法類的運(yùn)行常量池的引用。
-
??Native Method Stack?則是用于調(diào)用操作系統(tǒng)本地方法時(shí)使用的棧空間。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)每個(gè)線程都可用訪問的內(nèi)存空間為線程共享區(qū)域,它包含 Head 和 Method Area 兩個(gè)部分,Head 用于存放實(shí)例對象,也是 GC 回收的主要區(qū)域,而 Method Area 用于存放類結(jié)構(gòu)與靜態(tài)變量。
現(xiàn)在我們初步了解了 JVM 內(nèi)存的布局,那么接下來可以繼續(xù)看指令的執(zhí)行過程了。
指令的執(zhí)行過程由于 Java 程序從?main()?方法開始,我們也是從這個(gè)方法的指令開始進(jìn)行分析。
假設(shè)程序運(yùn)行 0 號指令前的狀態(tài)如下,在 mian 方法棧幀里面,有著 operand stack(操作數(shù)棧),它的最大長度為 2(與 Code 下的 stack 的值一致),此外還有一個(gè) local variable(本地變量表)來存放變量的值,其中下標(biāo)為 0 的變量為主方法的參數(shù) args,我們直接用這個(gè)字符串填充在那里來做一個(gè)標(biāo)識(shí)(實(shí)際的值可能是一個(gè)空數(shù)組)。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)接下來我們一步步執(zhí)行方法中的指令,在這里我們先對出現(xiàn)的幾個(gè)指令做一個(gè)簡單的介紹:
-
??
iconst_<i>?放一個(gè) int 常量(-1, 0, 1, 2, 3, 4 or 5) 到 operand stack 中 -
??
istore_<n>?從 operand stack 中獲取一個(gè) int 到 local variable 的 n 中 -
??
iload_<n>?從 local variable 中讀取 int 變量 n 的值到操作數(shù)棧中 -
??
invokestatic?調(diào)用一個(gè) class 的 static 方法 -
??
getstatic?從 class 中獲取一個(gè) static 字段 -
??
invokevirtual?調(diào)用一個(gè)實(shí)例方法,基于類的調(diào)度 -
??
return?從方法中返回一個(gè) void,ireturn?從方法中返回 operand stack 棧頂?shù)?int
更多的指令與詳細(xì)的說明請查看文章最后參考中的官方指令文檔
現(xiàn)在我們開始分析指令的執(zhí)行,我們在上面知道了,我們的 Java 代碼所對應(yīng)的指令分別是偏移量為 0 和 1 的兩個(gè),最開始執(zhí)行的是?0: iconst_1,該指令會(huì)把 int 常量 1 放置到 operand stack 中,之后執(zhí)行的是?1: istore_1,把 operand stack 棧頂?shù)?int 常量取出放到 local variable 下標(biāo)為 1 的變量中,該過程圖示如下。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)我們可以通過查看 LocalVariableTable 得知下標(biāo)為 1 的變量在我們的 Java 程序中是 int 變量 a,因此上面這兩條指令常量 1 賦值給變量 a。同樣的,后面兩條指令則是將常量 1 賦值給變量 b。這里要注意,操作數(shù)棧的數(shù)是被取出操作,被取出的數(shù)將不會(huì)繼續(xù)在 operand stack 里面。
執(zhí)行完 0~3 這 4 條指令后,就來到了本例中最為關(guān)鍵的方法調(diào)用了。在執(zhí)行?iload_1?和?iload_2?后,operand stack 中將會(huì)存放著變量 a 和 b 的值,作為?invokestatic?調(diào)用函數(shù)時(shí)傳入的參數(shù)。
而執(zhí)行到?invokestatic #2?這個(gè)指令的時(shí)候,該指令為調(diào)用一個(gè) class 的 static 方法,也就是調(diào)用常量池中?#2?的方法,該方法為?Hello.add:(II)I?。
當(dāng)執(zhí)行 invokestatic 時(shí)會(huì)依次讀取 operand stack 的數(shù)據(jù)作為方法的參數(shù),并創(chuàng)建一個(gè)新的棧幀來執(zhí)行方法,將數(shù)據(jù)放到 local variable 對應(yīng)變量位置。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)之后開始執(zhí)行?add()?方法中的指令,首先執(zhí)行的是兩個(gè)?iload?指令,將 loca variable 對應(yīng)下標(biāo)的變量的值放到 operand stack 中,之后執(zhí)行?iadd?取出 operand stack 中的值并進(jìn)行加法運(yùn)算,再把結(jié)果放到,最后執(zhí)行 ireturn 取出 operand stack 頂部的 int 值進(jìn)行返回。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)當(dāng)執(zhí)行完?ireturn?后,add 方法也就執(zhí)行完成了,對應(yīng)的棧幀也會(huì)跟著銷毀。之后回到 main 方法中繼續(xù)往下執(zhí)行,到?istore_3?指令,該指令將棧頂?shù)?int 值取出放到了 local variable 中 Solt 為 3 的地方,這樣執(zhí)行完 4~9 這幾條指令后就完成了我們代碼中的?int c = add(a, b);?這一行代碼。那么接下來就是執(zhí)行?System.out.println(c);?對應(yīng)的指令將 2 打印到控制臺(tái)了。
到這里其實(shí)我們就已經(jīng)知道如何去閱讀我們代碼生成的 Byte Code 了,這里我就不繼續(xù)往下分析本文例子的代碼了,閱讀過程中如果遇到了沒見過的指令,我們可以在 Oracle 官方指令文檔里面查閱對應(yīng)的說明。
查看字節(jié)碼工具如果每次查看反編譯后的字節(jié)碼都使用javap命令的話,好非常繁瑣。這里推薦一個(gè)Idea插件:jclasslib[1]?。使用效果如圖15所示,代碼編譯后在菜單欄”View”中選擇”Show Bytecode With jclasslib”,可以很直觀地看到當(dāng)前字節(jié)碼文件的類信息、常量池、方法區(qū)等信息。
Java字節(jié)碼介紹 - Java技術(shù)債務(wù)--------------------------------------歡迎叨擾此地址---------------------------------------
參考本文作者:Java技術(shù)債務(wù)?
原文鏈接:https://cuizb.top/myblog/article/1671634067?
版權(quán)聲明: 本博客所有文章除特別聲明外,均采用 CC BY 3.0 CN協(xié)議進(jìn)行許可。轉(zhuǎn)載請署名作者且注明文章出處。
-
1.?字節(jié)碼增強(qiáng)技術(shù)探索:https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html
-
2.?一文看懂 JVM 內(nèi)存布局及 GC 原理:https://www.infoq.cn/article/3wyretkqrhivtw4frmr3
-
3.?Oracle 官方說明文檔:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-4.html#jvms-4.10
-
4.?Oracle 官方指令文檔:https://docs.oracle.com/javase/specs/jvms/se16/html/jvms-6.html
引用鏈接
[1]?jclasslib:?https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer
請各位看官大人,多點(diǎn)點(diǎn)贊、在看以及原文。給小的一個(gè)鼓勵(lì),謝謝您。。。
更多請移駕
??????????????????????????????????
本文作者:Java技術(shù)債務(wù)?
原文鏈接:https://cuizb.top/myblog/article/1671634067?
版權(quán)聲明:?本博客所有文章除特別聲明外,均采用 CC BY 3.0 CN協(xié)議進(jìn)行許可。轉(zhuǎn)載請署名作者且注明文章出處。
??????????????????????????????????

JVM內(nèi)存泄漏和內(nèi)存溢出的原因
JVM常用監(jiān)控工具解釋以及使用
ClickHouse之MaterializeMySQL引擎(十)
三種實(shí)現(xiàn)分布式鎖的實(shí)現(xiàn)與區(qū)別
喜歡就 分享
認(rèn)同就 點(diǎn)贊
支持就 在看
一鍵四連,你的offer也四連

