Skip to content

值得您信賴的旅遊品牌 | 團體旅遊、自由行的專家‎

機場接送

Menu
  • 首頁
  • 旅遊天地
  • 裝潢設計
  • 環保清潔
  • 發燒車訊
Menu

【JVM故事】一個Java字節碼文件的誕生記

Posted on 2021-03-242021-03-24 by admin

萬字長文,完全虛構。

 

(一)

 

組裡來了個實習生,李大胖面完之後,覺得水平一般,但還是留了下來,為什麼呢?各自猜去吧。

李大胖也在心裏開導自己,學生嘛,不能要求太高,只要肯上進,慢慢來。就稱呼為小白吧。

小白每天來的很早,走的很晚,都在用功學習,時不時也向別人請教。只是好像天資差了點。

都快一周了,才能寫些“簡單”的代碼,一個註解,一個接口,一個類,都來看看吧:

public @interface Health {

    String name() default "";
}


public interface Fruit {

    String getName();

    void setName(String name);

    int getColor();

    void setColor(int color);
}


@Health(name = "健康水果")
public class Apple implements Fruit {

    private String name;
    private int color;
    private double weight = 0.5;

    @Override
    public String getName() {
        return name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getColor() {
        return color;
    }

    @Override
    public void setColor(int color) {
        this.color = color;
    }

    public double weight() {
        return weight;
    }

    public void weight(double weight) {
        this.weight = weight;
    }
}

與周圍人比起來,小白進步很慢,也許是自己不夠聰明,也許是自己不適合干這個,小白好像有點動搖了。

這幾天,小白明顯沒有一開始那麼上進了,似乎有點想放棄,這不,趴在桌子上竟然睡着了。

 

(二)

 

在夢中,小白來到一個奇怪又略顯陰森的地方,眼前有一個破舊的小房子,從殘缺不全的門縫裡折射出幾束光線。

小白有些害怕,但還是鎮定了下,深呼吸幾口,徑直朝着小房子走去。

小白推開門,屋裡沒有人。只有一個“機器”在桌子旁大口大口“吃着”東西,背後也不時的“拉出”一些東西。

小白很好奇,就湊了上去,準備仔細打量一番。

“你要幹嘛,別影響我工作”。突然冒出一句話,把小白嚇了一大跳,慌忙後退三步,媽呀,心都快蹦出來了。

“你是誰呀?”,驚慌中小白說了句話。

“我是編譯器”,哦,原來這個機器還會說話,小白這才緩了過來。

“編譯器”,小白好像聽說過,但一時又想不起,於是猜測到。

“網上評論留言里說的小編是不是就是你啊”?

“你才是呢”,編譯器白了一眼,沒好聲氣的說到。

要不是看在長得還行的份上,早就把你趕走了,編譯器心想。

“哦,我想起來了,編譯器嘛,就是編譯代碼的那個東西”,小白恍然大悟到。

“請注意你的言詞,我不是個東西,哦,不對,我是個東西,哦,好像也不對,我。。我。。”,編譯器自己也快暈了。

編譯器一臉的無奈,遇上這樣的人,今天我認栽了。

小白才不管呢,心想,今天我竟然見到了編譯器,我得好好請教請教他。

那編譯器會幫助她嗎?

 

 

(三)

 

小白再次走上前來,定睛一看,才看清楚,編譯器吃的是Java源碼,拉的是class(字節碼)文件。

咦,為啥這個代碼這麼熟悉呢,不就是我剛剛寫的那些。“停,停,快停下來了”。編譯器被小白叫停了。

“你又要幹嘛啊”?編譯器到。

“嘻嘻,這個代碼是我寫的,我想看看它是怎麼被編譯的”,小白到。

編譯器看了看這個代碼,這麼“簡單”,她絕對是個菜鳥。哎,算了,還是讓她看看吧。

不過編譯器又到,“整個編譯過程是非常複雜的,想要搞清楚裏面的門道是不可能的,今天也就只能看個熱鬧了”。

“編譯后的內容都是二進制數據,再通俗點說,就是一個長長的字節數組(byte[])”,編譯器繼續說,“通常把它寫入文件,就是class文件了”。

“但這不是必須的,也可以通過網絡傳到其它地方,或者保存在內存中,用完之後就丟棄”。

“哇,還可以這樣”,小白有些驚訝。編譯器心想,你是山溝里出來的,沒見過世面,大驚小怪。

繼續到,“從數據結構上講,數組就是一段連續的空間,是‘沒有結構’的,就像一個線段一樣,唯一能做的就是按索引訪問”。

小白到,“編譯后的內容一定很繁多,都放到一個數組裡面,怎麼知道什麼東西都在哪呢?不都亂套了嘛”。

編譯器覺得小白慢慢上道了,心裏有一絲安慰,至少自己的講解不會完全白費。於是繼續到。

“所以JVM的那些大牛們早就設計好了字節碼的格式,而且還把它們放入到了一個字節數組裡面”。

小白很好奇到,“那是怎麼實現的呢”?

“其實也沒有太高深的內容,既然數組是按位置的,那就規定好所有內容的先後順序,一個接一個往數組裡放唄”。

“如果內容的長度是固定(即定長)的,那最簡單,直接放入即可”。

“如果內容長度是不固定(即變長)的,也很簡單,在內容前用一到兩個字節存一下內容的長度不就OK了”。

 

 

(四)

 

“字節碼的前4個字節必須是一個固定的数字,它的十進制是3405691582,大部分人更熟悉的是它的十六進制,0xCAFEBABE”。

“通常稱之為魔術数字(Magic),它主要是用來區分文件類型的”,編譯器到。

“擴展名(俗稱後綴名)不是用來區分文件類型的嗎”?小白說到,“如.java是Java文件,.class是字節碼文件”。

“擴展名確實可以區分,但大部分是給操作系統用的,或給人看到。如我們看到.mp3時知道是音頻、.mp4是知道是視頻、.txt是文本文件”。

“操作系統可以用擴展名來關聯打開它的軟件,比如.docx就會用word來打開,而不會用文本文件”。編譯器繼續到。

“還有一個問題就是擴展名可以很容易被修改,比如把一個.java手動改為.class,此時讓JVM來加載這個假的class文件會怎樣呢”?

“那JVM先讀取開頭4個字節,發現它不是剛剛提到的那個魔數,說明它不是合法的class文件,就直接拋異常唄”,小白說到。

“很好,真是孺子可教”,編譯器說道,“不過還有一個問題,不知你是否注意到?4個字節對應Java的int類型,int類型的最大值是2147483647”。

“但是魔數的值已經超過了int的最大值,那怎麼放得下呢,難道不會溢出嗎”?

“確實啊,我怎麼沒發現呢,那它到底是怎麼放的呢”?小白到。

“其實說穿了不值得一提,JVM是把它當作無符號數對待的。而Java是作為有符號數對待的。無符號數的最大值基本上是有符號數最大值的兩倍”。

“接下來的4個字節是版本號,不同版本的字節碼格式可能會略有差異,其次在運行時會校驗,如JDK8編譯后的字節碼是不能放到JDK7上運行的”。

“這4個字節中的前2個是次(minor)版本,后2個是主(major)版本”。編譯器繼續到,“比如我現在用的JDK版本是1.8.0_211,那次版本就是0,主版本就是52”。

“所以前8個字節的內容是,0xCAFEBABE,0,52,它們並不是源代碼里的內容”。

Magic [getMagic()=0xcafebabe]
MinorVersion [getVersion()=0]
MajorVersion [getVersion()=52]

 

(五)

 

當編譯器讀到源碼中的public class的時候,然後就就去查看一個表格,如下圖:

自顧自的說著,“public對應的是ACC_PUBLIC,值為0x0001,class默認就是,然後又讀ACC_SUPER的值0x0020”。

“最後把它倆合起來(按位或操作),0x0001 | 0x0020 => 0x0021,然後把這個值存起來,這就是這個類的訪問控制標誌”。

小白這次算是開了眼界了,只是還有一事不明,“這個ACC_SUPER是個什麼鬼”?

編譯器解釋到,“這是歷史遺留問題,它原本表達在調用父類方法時會特殊處理,不過現在已經不再管它了,直接忽略”。

接着讀到了Apple,它是類名。編譯器首先要獲取類的全名,org.cnt.java.Apple。

然後對它稍微轉換一下形式,變成了,org/cnt/java/Apple,“這就是類名在字節碼中的表示”。

編譯器發現這個Apple類沒有顯式繼承父類,表明它繼承自Object類,於是也獲取它的全名,java/lang/Object。

接着讀到了implements Fruit,說明該類實現了Fruit接口,也獲取全名,org/cnt/java/Fruit。

小白說到,“這些比較容易理解,全名中把點號(.)替換為正斜線(/)肯定也是歷史原因了。但是這些信息如何存到數組裡呢”?

“把點號替換為正斜線確實是歷史原因”,編譯器繼續到,“這些字符串雖然都是類名或接口名,但本質還是字符串,類名、接口名只是賦予它的意義而已”。

“除此之外,像字段名、方法名也都是字符串,同理,字段名、方法名也是賦予它的意義。所以字符串是一種基本的數據,需要得到支持”。

“除了字符串之外,還有整型数字,浮點数字,這些也是基本的數據,也需要得到支持”。

因此,設計者們就設計出了以下幾種類型,如圖:

“左邊是類型名稱,方便理解,右邊是對應的值,用於存儲”,編譯器繼續到。

“這裏的Integer/Long/Float/Double和Utf8都是具體保存數據用的,表示整型數/浮點數和字符串。其它的類型大都是對字符串的引用,並賦予它一定的意義”。

“所以類名首先被存儲為一個字符串,也就是Utf8,它的值對應的是1”。編譯器接着到,“由於字符串是一個變長的,所以就先用兩個字節存儲字符串的長度,接着跟上具體的字符串內容”。

所以字符串的結構就是這樣,如圖:

“類名字符串的存儲數據為,1、18、org/cnt/java/Apple。第一個字節為1,表明是Utf8類型,第2、3兩個字節存儲18,表示字符串長度是18,接着存儲真正的字符串。所以共用去1 + 2 + 18 => 21個字節”。

“父類名字符串存儲為,1、16、java/lang/Object。共用去19個字節”。

“接口名字符串存儲為,1、18、org/cnt/java/Fruit。共用去21個字節”。

小白聽的不住點頭,編譯器喘口氣,繼續講解。

“字符串存好后,就該賦予它們意義了,在後續的操作中肯定涉及到對這些字符串的引用,所以還要給每個字符串分配一個編號”。

如Apple為#2,即2號,Object為#4,Fruit為#6。

“由於這三個字符串都是類名或接口名,按照設計規定應該使用Class表示,對應的值為7,然後再指定一個字符串的編號即可”。

因此類或接口的表示如下圖:

“先用1個字節指明是類(接口),然後再用2個字節存儲一個字符串的編號。整體意思很直白,就是把這個編號的字符串當作類名或接口名”。

“類就表示為,7、#2。7表示是Class,#2表示類名稱那個字符串的存儲編號。共用去3個字節”。

“父類就表示,7、#4。共用去3個字節。接口就表示為,7、#6。共用去3個字節”。

其實這三個Class也分別給它們一個編號,方便別的地方再引用它們。

 

 

(六)

 

“其實上面這些內容都是常量,它們都位於常量池中,它們的編號就是自己在常量池中的索引”。編譯器說到。

“常量池很多人都知道,起碼至少是聽說過。但絕大多數人對它並不十分熟悉,因為很少有人見過它”。

編譯器繼續到,“今天你可算是來着了”,說著就把小白寫的類編譯後生成的常量池擺到了桌子上。

“這是什麼東西啊,這麼多,又很奇怪”,小白說到,這也是她第一次見。

ConstantPoolCount [getCount()=46]
ConstantPool [
#0 = null
#1 = ConstantClass [getNameIndex()=2, getTag()=7]
#2 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Apple, getTag()=1]
#3 = ConstantClass [getNameIndex()=4, getTag()=7]
#4 = ConstantUtf8 [getLength()=16, getString()=java/lang/Object, getTag()=1]
#5 = ConstantClass [getNameIndex()=6, getTag()=7]
#6 = ConstantUtf8 [getLength()=18, getString()=org/cnt/java/Fruit, getTag()=1]
#7 = ConstantUtf8 [getLength()=4, getString()=name, getTag()=1]
#8 = ConstantUtf8 [getLength()=18, getString()=Ljava/lang/String;, getTag()=1]
#9 = ConstantUtf8 [getLength()=5, getString()=color, getTag()=1]
#10 = ConstantUtf8 [getLength()=1, getString()=I, getTag()=1]
#11 = ConstantUtf8 [getLength()=6, getString()=weight, getTag()=1]
#12 = ConstantUtf8 [getLength()=1, getString()=D, getTag()=1]
#13 = ConstantUtf8 [getLength()=6, getString()=<init>, getTag()=1]
#14 = ConstantUtf8 [getLength()=3, getString()=()V, getTag()=1]
#15 = ConstantUtf8 [getLength()=4, getString()=Code, getTag()=1]
#16 = ConstantMethodRef [getClassIndex()=3, getNameAndTypeIndex()=17, getTag()=10]
#17 = ConstantNameAndType [getNameIndex()=13, getDescriptorIndex()=14, getTag()=12]
#18 = ConstantDouble [getDouble()=0.5, getTag()=6]
#19 = null
#20 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=21, getTag()=9]
#21 = ConstantNameAndType [getNameIndex()=11, getDescriptorIndex()=12, getTag()=12]
#22 = ConstantUtf8 [getLength()=15, getString()=LineNumberTable, getTag()=1]
#23 = ConstantUtf8 [getLength()=18, getString()=LocalVariableTable, getTag()=1]
#24 = ConstantUtf8 [getLength()=4, getString()=this, getTag()=1]
#25 = ConstantUtf8 [getLength()=20, getString()=Lorg/cnt/java/Apple;, getTag()=1]
#26 = ConstantUtf8 [getLength()=7, getString()=getName, getTag()=1]
#27 = ConstantUtf8 [getLength()=20, getString()=()Ljava/lang/String;, getTag()=1]
#28 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=29, getTag()=9]
#29 = ConstantNameAndType [getNameIndex()=7, getDescriptorIndex()=8, getTag()=12]
#30 = ConstantUtf8 [getLength()=7, getString()=setName, getTag()=1]
#31 = ConstantUtf8 [getLength()=21, getString()=(Ljava/lang/String;)V, getTag()=1]
#32 = ConstantUtf8 [getLength()=16, getString()=MethodParameters, getTag()=1]
#33 = ConstantUtf8 [getLength()=8, getString()=getColor, getTag()=1]
#34 = ConstantUtf8 [getLength()=3, getString()=()I, getTag()=1]
#35 = ConstantFieldRef [getClassIndex()=1, getNameAndTypeIndex()=36, getTag()=9]
#36 = ConstantNameAndType [getNameIndex()=9, getDescriptorIndex()=10, getTag()=12]
#37 = ConstantUtf8 [getLength()=8, getString()=setColor, getTag()=1]
#38 = ConstantUtf8 [getLength()=4, getString()=(I)V, getTag()=1]
#39 = ConstantUtf8 [getLength()=3, getString()=()D, getTag()=1]
#40 = ConstantUtf8 [getLength()=4, getString()=(D)V, getTag()=1]
#41 = ConstantUtf8 [getLength()=10, getString()=SourceFile, getTag()=1]
#42 = ConstantUtf8 [getLength()=10, getString()=Apple.java, getTag()=1]
#43 = ConstantUtf8 [getLength()=25, getString()=RuntimeVisibleAnnotations, getTag()=1]
#44 = ConstantUtf8 [getLength()=21, getString()=Lorg/cnt/java/Health;, getTag()=1]
#45 = ConstantUtf8 [getLength()=12, getString()=健康水果, getTag()=1]
]

“在常量池前面會用2個字節來存儲常量池的大小,需要記住的是,這個大小不一定就是池中常量的個數。但它減去1一定是最大的索引”。

“因為,常量池中為0的位置(#0)永遠不使用,還有Long和Double類型一個常量佔2個連續索引(沒錯,又是歷史原因),實際只是用了第1個索引,第2個索引永遠空着(參見#18、#19)”。

編譯器繼續到,“#0是特殊的,用來表示‘沒有’的意思,其它地方如果想表達沒有的話,可以指向它。如Object是沒有父類的,所以它的父類指向#0,即沒有”。

“所以常量都是從#1開始。可以看看#1到#6的內容,就是剛剛上面講的”。編譯器說到。

“真是學到不少知識啊”,小白說到,“關於常量池能不能再多講點”?編譯器只好繼續講。

 

 

(七)

 

“常量池就是一個容器,它裏面放了各種各樣的所有信息,並且為每個信息分配一個編號(即索引),如果想要在其它地方使用這些信息,直接使用這個編號就行了”。

編譯器繼續到,“這個常量池在一些語言中也被稱為‘符號表’,通過編號來使用的這種方式也被稱為‘符號引用’”。

相信很多愛學習的同學對符號表和符號引用這兩個詞都很熟悉,不管之前是不是真懂,至少現在應該是真的搞懂了。因為你已經看到了。

“採用這種常量池和常量引用方式的好處其實很多,就說個最容易想到的,就是重複利用,節省空間,便於管理”。編譯器繼續說。

“比如一個類里有10個方法,每個方法里都定義一個length的局部變量,那麼length這個名字就會出現在常量池裡面,且只會出現一次,那10個方法都是對它的引用而已”。

“如果有一個方法的名字也叫length的話,那也是對同一個常量的引用,因為這個length常量只是個字符串數據而已,本身沒有明確含義,它的含義來自於引用它的常量”。

“哦,原來如此”,小白開悟到,“‘符號表、符號引用’這些‘高大上’的叫法,不過就是根據索引去列表裡獲取元素罷了”,哈哈。

編譯器看到小白這麼開心,就準備拋出一個問題,“打壓”一下她。於是說到。

“常量池看上去和數組/列表非常相似,都是容器且都是基於索引訪問的。為啥常量池只被稱為符號表,而不是符號數組或符號列表呢”?

小白自然回答不上來。編譯器繼續說,“表的英文單詞是Table。它和數組/列表的唯一區別就是,數組/列表裡的元素長度都是固定的。表裡的元素長度是不固定的”。

“常量池中的好幾種常量的長度都是變長的,所以自然是表了”。

小白點了點頭,心裏想,這編譯器就是厲害,我這輩子看來都無法達到他的高度了。

編譯器繼續說到,“字節碼的前8個字節存儲魔數和版本,接着的2個(9和10)字節存儲常量池的大小,後面接着(從11開始)就是整個常量池的內容了”。

“之所以把常量池放這麼靠前,是因為後面的所有內容都要依賴它、引用它”。

緊跟在常量池之後的就是這個類的基本信息,如下:

“首先用2個字節存儲上面已經計算好的訪問控制標誌,即0x0021”。

“然後用2個字節存儲這個類在常量池中的索引,就是#1”。

“然後用2個字節存儲該類的父類在常量池中的索引,就是#3”。

“由於接口可以有多個,所以再用2個字節存儲接口的個數,因為只實現了1個接口,所以就存儲数字1”。

“接着存儲所有接口在常量池中的索引,每個接口用2個字節。因為只實現了1個接口,所以存儲的索引就是#5”。

AccessFlags [getAccessFlags()=0x21, getAccessFlagsString()=[ACC_PUBLIC, ACC_SUPER]]
ThisClass [getClassIndex()=1, getClassName()=org/cnt/java/Apple]
SuperClass [getClassIndex()=3, getClassName()=java/lang/Object]
InterfacesCount [getCount()=1]
Interfaces [getClassIndexes()=[5], getClassNames()=[org/cnt/java/Fruit]]

 

 

 

(八)

 

編譯器繼續到,“接下來該讀取字段信息了”。當讀到private時,就去下面這張表裡找:

找到ACC_PRIVATE,把它的值0x0002保存以下,這就是該字段的訪問控制標誌。

接着讀到的是String,這是字段的類型,然後會把這個String類型存入常量池,對應的索引是#8。

可以看到是一個Utf8,說明是字符串,內容是 Ljava/lang/String; ,以大寫L開頭,已分號;結尾,中間是類型全名,這是在字節碼中表示類(對象)類型的方式。

接着讀到的是name,這是字段名稱,也是個字符串,同樣也把它放入常量池,對應的索引是#7。

編譯器說到,“現在一個字段的信息已經讀取完畢,按照相同的方式把剩餘的兩個字段也讀取完畢”。

“那字段的信息又該怎麼存儲呢”?小白問到。“不要着急嘛”,編譯器說著就拿出了字段的存儲格式:

首先2個字節是訪問控制標誌,接着2個字節是字段名稱在常量池中的索引,接着2個字節是字段描述(即類型)在常量池中的索引。

接着2個字節就是屬性個數,然後就是具體的屬性信息了。例如字段上標有註解的話,這個註解信息就會放入屬性信息里。

編譯器繼續說到,“屬性信息是字節碼中比較複雜的內容,這裏就不說太多了”。接着就可以按格式整理數據了。

因為一個類的字段可以有多個,所以先用2個字節存儲一下字段數目,本類有3個字段,所以就存儲個3。

第一個字段,0x0002、#7、#8、0。共用去8個字節,因為自動沒有屬性內容。

第二個字段,0x0002、#9、#10、0。共用去8個字節。

第二個字段,0x0002、#11、#12、0。共用去8個字節。

編譯器接着說,“所以存儲這3個字段信息共用去2 + 8 + 8 + 8 => 26個字節”。

小白說到,“我現在基本已經搞明白套路了。其實有些東西沒有想象中的那麼複雜啊”。

“複雜的東西還是有的,我們現在先不考慮”,編譯器說到,“還有一個問題,不知你發現了沒有”。

字段color的類型是int,但是在常量池中卻變為大寫字母I,同樣weight的類型是double,常量池中卻是大寫字母D。

小白說到,“我來猜測一下吧,int、double是Java中的數據類型,I、D是與之對應的在JVM中的表示形式。對吧”?

“算你聰明”,編譯器說到,“其實Java和JVM之間關於類型這塊有一個映射表”,如下:

有兩個需要注意。“第一點上面已經說過了,就是類都會映射成LClassName;這種形式,如Object映射為Ljava/lang/Object;”。

第二點是數組,“數組在Java中用一對中括號([])表示,在JVM中只用左中括號([)表示。也就是[]映射為[”。

“多維數組也一樣,[][][]映射為[[[”。然後還有類型,“Java是把類型放到前面,JVM是把類型放到後面”。如double[]映射為[D。

“double[][][]映射為[[[D”。同理,“String[]映射為[Ljava/lang/String;,Object[][]映射為[[Ljava/lang/Object;”。

“我似乎又明白了一些,Java有自己的規範,字節碼也有自己的規範,它們之間的映射關係早都已經定義好了”。小白繼續到。

“只要按照這種映射關係,就能把Java源碼給轉換為字節碼。是吧”?

“粗略來說,可以這麼理解,其實這就是編譯了,但一定要清楚,真正的編譯是非常複雜的一個事情”,編譯器到。

小白說到,“字段完了之後,肯定該方法了,就交給我吧,讓我也試試”。

“年輕人啊,就是生猛,你來試試吧”。編譯器說到。

FieldsCount [getCount()=3]
Fields [
#0 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=7, getName()=name, getDescriptorIndex()=8, getDescriptor()=Ljava/lang/String;, getAttributesCount()=0, getAttributes()=[]]
#1 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=9, getName()=color, getDescriptorIndex()=10, getDescriptor()=I, getAttributesCount()=0, getAttributes()=[]]
#2 = FieldInfo [getAccessFlags()=FieldAccessFlags [getAccessFlags()=0x2, getAccessFlagsString()=[ACC_PRIVATE]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=12, getDescriptor()=D, getAttributesCount()=0, getAttributes()=[]]
]

 

 

 

(九)

 

小白說,“方法呢肯定也有自己的格式,你把它找出來我看看”。

“好好,我這就找”,編譯器苦笑到。我堂堂一個編譯器,今天竟然成了小白的助手,慚愧啊。

說著編譯器就找到了,於是放到了桌子上:

“咦,怎麼和字段的一模一樣”,小白到。那這就更簡單了。

先是訪問控制標誌,接着是方法名稱索引,然後是方法描述索引,最後是和方法關聯的屬性。於是照貓畫虎,小白就開始了。

先讀到public關鍵字,這是個訪問控制修飾符,肯定也有一張表和它對應,可以找到這個關鍵字對應的數值。

還沒等小白開口,編譯器就趕緊把表找出來了:

小白繼續,ACC_PUBLIC對應的值是0x0001,就把這個值先保存起來。

然後是方法的名字,getName,是一個字符串,照例把它存入常量池,並且有一個索引,就是#26。

接着該方法的描述了,小白認為方法和字段是不同的,除了有返回類型之外,還有參數呢,這該咋整呢?

於是就問編譯器,“方法的描述應該也有格式吧”?

“你越來越聰明了”,編譯器說到,“其實也很簡單,我來簡單說下吧”。

“在Java中如果把訪問控制符、方法名、參數名、方法體都去掉,其實就剩下‘方法簽名’了”。

例如,沒有入參沒有返回值的,就是這個樣子,void()。

返回值為String,入參為int,double,String的,其實就是這樣個子,String(int, double, String)。

“這個方法簽名其實就是在Java中對方法的描述,在字節碼中和它差不多,就是把返回類型放到後面,把參數間的逗號去掉”。

因此void()映射為()V,這裏要注意的是void對應的是大寫字母V。

String(int, double, String)映射為(IDLjava/lang/String;)Ljava/lang/String;

“不難,不難”,小白說到,於是又繼續開始了。

小白按照這種格式,把剛剛的那個方法描述也存入了常量池,得到的索引就是#27。

小白按這個套路把6個方法都整理好了,接下來該按格式把數據寫入字節數組了。

編程新說注:方法的代碼對應的是JVM的指令,這裏就忽略不談了,後續可能會單獨再說。

編譯器提醒小白說,“你是不是還漏掉了一個方法啊”?

小白又看了一遍Java源碼,仔細數了數,是6個呀,沒錯啊。

編譯器說到,你在學習時有沒有見過這樣一句話,“當類沒有定義構造函數時,編譯器會為它生成一個默認的無參構造函數”。

小白連忙點頭,“嗯嗯嗯,見過的”。

“這就是了”,編譯器說道,“不過需要注意的是,在字節碼中構造方法的名字都是<init>,返回類型都是V”。

“這也是規定的吧”,小白說到,編譯器點了點頭。

編譯器又說到,“其實還有方法的參數信息,如參數位置,參數類型,參數名稱,參數的訪問控制標誌等”。

“這些信息都是放在方法格式里最後的屬性信息中的,咱們也暫時不說它們了”。

編程新說注:

在JDK7及以前,字節碼中不包含方法的參數名。因為JVM執行指令時,參數是按位置傳入的,所以參數名對代碼的執行沒有用處。

由於越來越多的框架採用按方法參數名進行數值綁定,Java也只好在JDK8時加入了對參數名的支持。

不過需要設置一下編譯器的–parameters參數,這樣才能把方法參數名也放入字節碼中。

可以看看常量池中的#32是“MethodParameters”字符串,說明字節碼中已經包含參數名了。

常量池中#7、#9、#11三個字符串就是參數名,同時也是字段名,這就是復用的好處。

編程新說注:方法的格式和字段的格式完全一樣,就不再演示寫入過程了。

因此這個類共有7個方法。

MethodsCount [getCount()=7]
Methods [
#0 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=13, getName()=<init>, getDescriptorIndex()=14, getDescriptor()=()V, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=1, getCodeLength()=12, getJvmCode()=JvmCode [getCode()=12], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=3, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=8], LineNumTable [getStartPc()=4, getLineNumber()=12], LineNumTable [getStartPc()=11, getLineNumber()=8]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=12, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#1 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=26, getName()=getName, getDescriptorIndex()=27, getDescriptor()=()Ljava/lang/String;, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=16]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#2 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=30, getName()=setName, getDescriptorIndex()=31, getDescriptor()=(Ljava/lang/String;)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=21], LineNumTable [getStartPc()=5, getLineNumber()=22]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=7, getDescriptorIndex()=8, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=7, getAccessFlags()=0x0]]]]]
#3 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=33, getName()=getColor, getDescriptorIndex()=34, getDescriptor()=()I, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=1, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=26]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#4 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=37, getName()=setColor, getDescriptorIndex()=38, getDescriptor()=(I)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=2, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=31], LineNumTable [getStartPc()=5, getLineNumber()=32]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=9, getDescriptorIndex()=10, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=9, getAccessFlags()=0x0]]]]]
#5 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=39, getDescriptor()=()D, getAttributesCount()=1, getAttributes()=[Code [getMaxStack()=2, getMaxLocals()=1, getCodeLength()=5, getJvmCode()=JvmCode [getCode()=5], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=1, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=35]]], LocalVariableTable [getLocalVarTableLength()=1, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=5, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0]]]]]]]
#6 = MethodInfo [getAccessFlags()=MethodAccessFlags [getAccessFlags()=0x1, getAccessFlagsString()=[ACC_PUBLIC]], getNameIndex()=11, getName()=weight, getDescriptorIndex()=40, getDescriptor()=(D)V, getAttributesCount()=2, getAttributes()=[Code [getMaxStack()=3, getMaxLocals()=3, getCodeLength()=6, getJvmCode()=JvmCode [getCode()=6], getExceptionTableLength()=0, getExceptionTables()=[], getAttributesCount()=2, getAttributes()=[LineNumberTable [getLineNumTableLength()=2, getLineNumTables()=[LineNumTable [getStartPc()=0, getLineNumber()=39], LineNumTable [getStartPc()=5, getLineNumber()=40]]], LocalVariableTable [getLocalVarTableLength()=2, getLocalVarTables()=[LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=24, getDescriptorIndex()=25, getIndex()=0], LocalVarTable [getStartPc()=0, getLength()=6, getNameIndex()=11, getDescriptorIndex()=12, getIndex()=1]]]]], MethodParameters [getParametersCount()=1, getParameters()=[Parameter [getNameIndex()=11, getAccessFlags()=0x0]]]]]
]

編程新說注:方法部分的輸出內容很多,是因為包含了方法體的代碼的信息。

 

 

(十)

 

“真是後生可畏啊”,編譯器感慨到。“小白竟然也能按照套路去在做點事情了”。

不過編譯器並不自危,因為最核心的內容是,可執行代碼如何轉換為JVM指令集中的指令,這可是“壓箱底”的乾貨,可不能隨便告訴別人,長得再好看也不行。哈哈,O(∩_∩)O。

接着編譯器拿出一個完整的字節碼文件格式圖給小白看:

小白看完后說,“和剛剛講的一樣,只是最後也有這個屬性信息啊”。

編譯器說,“屬性信息是字節碼文件中非常複雜的內容,可以暫時不管用了”。

上面已經說了,至少註解的相關內容是放在屬性信息里的。

那就看看你寫的這個類的屬性信息都是什麼吧:

AttributesCount [getCount()=2]
Attributes [
#0 = SourceFile [getSourcefileIndex()=42]
#1 = RuntimeVisibleAnnotations [getNumAnnotations()=1, getAnnotations()=[Annotation [getTypeIndex()=44, getNumElementValuePairs()=1, getElementValuePairs()=[ElementValuePair [getElementNameIndex()=7, getElementValue()=ElementValue [getTag()=ElementValueTag [getTagChar()=s], getUnion()=ElementValueUnion [getConstValueIndex()=45]]]]]]]
]

編譯器繼續說,共有2條屬性信息,第一條是源代碼文件的名字,在常量池中的#42。其實就是Apple.java了。

第二條是運行時可見的註解信息,本類共有1個註解,註解類型是常量池中的#44。其實就是Lorg/cnt/java/Health;了。

該註解共顯式設置了1對屬性值。屬性名稱是常量池中的#7,就是name了,類型是小寫的s,表示String類型,屬性值是#45,也就是“健康水果”了。

下圖中的這些類型,都是可以用於註解屬性的類型:

最後,編譯器打印出一行信息:

—–bytes=1085—–

小白說,“這是什麼意思”?“這是編譯后產生的字節碼的總長度,是1085個字節”,編譯器到。

小白剛想表達對編譯器的感謝,忽然聞到一陣香味,而且是肉香。

PS:最後幾句話就不寫了,請你來補充完整吧,嘻嘻。

 

 

>>> 熱門文章集錦 <<<

 

畢業10年,我有話說

【面試】我是如何面試別人List相關知識的,深度有點長文

我是如何在畢業不久只用1年就升為開發組長的

爸爸又給Spring MVC生了個弟弟叫Spring WebFlux

【面試】我是如何在面試別人Spring事務時“套路”對方的

【面試】Spring事務面試考點吐血整理(建議珍藏)

【面試】我是如何在面試別人Redis相關知識時“軟懟”他的

【面試】吃透了這些Redis知識點,面試官一定覺得你很NB(乾貨 | 建議珍藏)

【面試】如果你這樣回答“什麼是線程安全”,面試官都會對你刮目相看(建議珍藏)

【面試】迄今為止把同步/異步/阻塞/非阻塞/BIO/NIO/AIO講的這麼清楚的好文章(快快珍藏)

【面試】一篇文章幫你徹底搞清楚“I/O多路復用”和“異步I/O”的前世今生(深度好文,建議珍藏)

【面試】如果把線程當作一個人來對待,所有問題都瞬間明白了

Java多線程通關———基礎知識挑戰

品Spring:帝國的基石

 

 

 

作者是工作超過10年的碼農,現在任架構師。喜歡研究技術,崇尚簡單快樂。追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂並記住。下面是公眾號的二維碼,歡迎關注!

 

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

※回頭車貨運收費標準

好站推薦

  • 健康醫療 減重知識專區
  • 婚紗世界 婚紗攝影寫真網
  • 成人話題 未滿18請勿進入
  • 流行時尚 時下流行愛美情報
  • 理財資訊 當舖借貸信用卡各式理財方法
  • 生活情報 各行各業情報資訊
  • 科技資訊 工業電子3C產品
  • 網路資訊 新奇趣味爆笑內容
  • 美食分享 全台各式名產 伴手禮
  • 裝潢設計 買屋賣屋裝修一羅框
  • 視覺設計 T恤、團體服、制服、polo衫

近期文章

  • 疑似小米手環 6 規格與新功能現身!加入血氧飽和偵測、GPS、更多運動模式_網頁設計公司
  • Microsoft 365 還是 Google Workspace?一文看懂企業生產力工具選哪套_如何寫文案
  • Sony Mobile 全新 Xperia Compact 小尺寸手機回歸?爆料大神釋出高清晰渲染圖_網頁設計公司
  • 微軟最新的廣告,直白地跟你說 Surface Pro 7 就是比 MacBook Pro 好_網頁設計
  • MagSafe 會干擾心律調節器?蘋果支援頁面有正式解答了_貨運

標籤

USB CONNECTOR  南投搬家公司費用 古典家具推薦 台中室內設計 台中室內設計師 台中搬家 台中搬家公司 台中電動車 台北網頁設計 台東伴手禮 台東名產 地板施工 大圖輸出 如何寫文案 婚禮錄影 宜蘭民宿 家具工廠推薦 家具訂製工廠推薦 家具訂製推薦 實木地板 床墊 復刻家具推薦 新竹婚宴會館 木地板 木質地板 柚木地板 桃園機場接送 桃園自助婚紗 沙發修理 沙發換皮 海島型木地板 潭子電動車 牛軋糖 租車 網站設計 網頁設計 網頁設計公司 貨運 超耐磨木地板 銷售文案 隱形鐵窗 電動車 馬賽克拼貼 馬賽克磁磚 馬賽克磚

彙整

  • 2021 年 4 月
  • 2021 年 3 月
  • 2021 年 2 月
  • 2021 年 1 月
  • 2020 年 12 月
  • 2020 年 11 月
  • 2020 年 10 月
  • 2020 年 9 月
  • 2020 年 8 月
  • 2020 年 7 月
  • 2020 年 6 月
  • 2020 年 5 月
  • 2020 年 4 月
  • 2020 年 3 月
  • 2020 年 2 月
  • 2020 年 1 月
  • 2019 年 12 月
  • 2019 年 11 月
  • 2019 年 10 月
  • 2019 年 9 月
  • 2019 年 8 月
  • 2019 年 7 月
  • 2019 年 6 月
  • 2019 年 5 月
  • 2019 年 4 月
  • 2019 年 3 月
  • 2019 年 2 月
  • 2019 年 1 月
  • 2018 年 12 月
©2021 值得您信賴的旅遊品牌 | 團體旅遊、自由行的專家‎ | Built using WordPress and Responsive Blogily theme by Superb