萬字長文,完全虛構,
(一)
組里來了個實習生,李大胖面完之后,覺得水平一般,但還是留了下來,為什么呢?各自猜去吧,
李大胖也在心里開導自己,學生嘛,不能要求太高,只要肯上進,慢慢來,就稱呼為小白吧,
小白每天來的很早,走的很晚,都在用功學習,時不時也向別人請教,只是好像天資差了點,
都快一周了,才能寫些“簡單”的代碼,一個注解,一個介面,一個類,都來看看吧:
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年的碼農,現在任架構師,喜歡研究技術,崇尚簡單快樂,追求以通俗易懂的語言解說技術,希望所有的讀者都能看懂并記住,下面是公眾號的二維碼,歡迎關注!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/172511.html
標籤:Java
