1. 程式人生 > >JVM詳解之:java class檔案的密碼本

JVM詳解之:java class檔案的密碼本

[toc] # 簡介 一切的一切都是從javac開始的。從那一刻開始,java檔案就從我們肉眼可分辨的文字檔案,變成了冷冰冰的二進位制檔案。 變成了二進位制檔案是不是意味著我們無法再深入的去了解java class檔案了呢?答案是否定的。 機器可以讀,人為什麼不能讀?只要我們掌握java class檔案的密碼錶,我們可以把二進位制轉成十六進位制,將十六進位制和我們的密碼錶進行對比,就可以輕鬆的解密了。 下面,讓我們開始這個激動人心的過程吧。 # 一個簡單的class 為了深入理解java class的含義,我們首先需要定義一個class類: ~~~java public class JavaClassUsage { private int age=18; public void inc(int number){ this.age=this.age+ number; } } ~~~ 很簡單的類,我想不會有比它更簡單的類了。 在上面的類中,我們定義了一個age欄位和一個inc的方法。 接下來我們使用javac來進行編譯。 IDEA有沒有?直接開啟編譯後的class檔案,你會看到什麼? 沒錯,是反編譯過來的java程式碼。但是這次我們需要深入瞭解的是class檔案,於是我們可以選擇 view->Show Bytecode: ![](https://img-blog.csdnimg.cn/20200615232536371.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 當然,還是少不了最質樸的javap命令: ~~~java javap -verbose JavaClassUsage ~~~ 對比會發現,其實javap展示的更清晰一些,我們暫時選用javap的結果。 編譯的class檔案有點長,我一度有點不想都列出來,但是又一想只有對才能講述得更清楚,還是貼在下面: ~~~java public class com.flydean.JavaClassUsage minor version: 0 major version: 58 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #2.#3 // java/lang/Object."":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "":()V #4 = Utf8 java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Fieldref #8.#9 // com/flydean/JavaClassUsage.age:I #8 = Class #10 // com/flydean/JavaClassUsage #9 = NameAndType #11:#12 // age:I #10 = Utf8 com/flydean/JavaClassUsage #11 = Utf8 age #12 = Utf8 I #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 Lcom/flydean/JavaClassUsage; #18 = Utf8 inc #19 = Utf8 (I)V #20 = Utf8 number #21 = Utf8 SourceFile #22 = Utf8 JavaClassUsage.java { public com.flydean.JavaClassUsage(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: aload_0 5: bipush 18 7: putfield #7 // Field age:I 10: return LineNumberTable: line 7: 0 line 9: 4 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/flydean/JavaClassUsage; public void inc(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=3, locals=2, args_size=2 0: aload_0 1: aload_0 2: getfield #7 // Field age:I 5: iload_1 6: iadd 7: putfield #7 // Field age:I 10: return LineNumberTable: line 12: 0 line 13: 10 LocalVariableTable: Start Length Slot Name Signature 0 11 0 this Lcom/flydean/JavaClassUsage; 0 11 1 number I } SourceFile: "JavaClassUsage.java" ~~~ # ClassFile的二進位制檔案 慢著,上面javap的結果好像並不是二進位制檔案! 對的,javap是對二進位制檔案進行了解析,方便程式設計師閱讀。如果你真的想直面最最底層的機器程式碼,就直接用支援16進位制的文字編譯器把編譯好的class檔案開啟吧。 你準備好了嗎? 來吧,展示吧! ![](https://img-blog.csdnimg.cn/2020061608593763.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 上圖左邊是16進位制的class檔案程式碼,右邊是對16進位制檔案的適當解析。大家可以隱約的看到一點點熟悉的內容。 是的,沒錯,你會讀機器語言了! # class檔案的密碼本 如果你要了解class檔案的結構,你需要這個密碼本。 如果你想解析class檔案,你需要這個密碼本。 學好這個密碼本,走遍天下都......沒啥用! 下面就是密碼本,也就是classFile的結構。 ~~~java ClassFile { u4 magic; u2 minor_version; u2 major_version; u2 constant_pool_count; cp_info constant_pool[constant_pool_count-1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attributes_count]; } ~~~ 其中u2,u4表示的是無符號的兩個位元組,無符號的4個位元組。 java class檔案就是按照上面的格式排列下來的,按照這個格式,我們可以自己實現一個反編譯器(大家有興趣的話,可以自行研究)。 我們對比著上面的二進位制檔案一個一個的來理解。 ## magic 首先,class檔案的前4個位元組叫做magic word。 看一下十六進位制的第一行的前4個位元組: ~~~java CA FE BA BE 00 00 00 3A 00 17 0A 00 02 00 03 07 ~~~ 0xCAFEBABE就是magic word。所有的java class檔案都是以這4個位元組開頭的。 來一杯咖啡吧,baby! 多麼有詩意的畫面。 ## version 這兩個version要連著講,一個是主版本號,一個是次版本號。 ~~~java 00 00 00 3A ~~~ ![](https://img-blog.csdnimg.cn/20200615235345167.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 對比一下上面的表格,我們的主版本號是3A=58,也就是我們使用的是JDK14版本。 ## 常量池 接下來是常量池。 首先是兩個位元組的constant_pool_count。對比一下,constant_pool_count的值是: ~~~java 00 17 ~~~ 換算成十進位制就是23。也就是說常量池的大小是23-1=22。 > 這裡有兩點要注意,第一點,常量池陣列的index是從1開始到constant_pool_count-1結束。 > > 第二點,常量池陣列的第0位是作為一個保留位,表示“不引用任何常量池專案”,為某些特殊的情況下使用。 接下來是不定長度的cp_info:constant_pool[constant_pool_count-1]常量池陣列。 常量池陣列中存了些什麼東西呢? 字串常量,類和介面名字,欄位名,和其他一些在class中引用的常量。 具體的constant_pool中儲存的常量型別有下面幾種: ![](https://img-blog.csdnimg.cn/20200616085115439.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 每個常量都是以一個tag開頭的。用來告訴JVM,這個到底是一個什麼常量。 好了,我們對比著來看一下。在constant_pool_count之後,我們再取一部分16進位制資料: ![](https://img-blog.csdnimg.cn/20200616090131493.png) 上面我們講到了17是常量池的個數,接下來就是常量陣列。 ~~~java 0A 00 02 00 03 ~~~ 首先第一個位元組是常量的tag, 0A=10,對比一下上面的表格,10表示的是CONSTANT_Methodref方法引用。 CONSTANT_Methodref又是一個結構體,我們再看一下方法引用的定義: ~~~java CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } ~~~ 從上面的定義我們可以看出,CONSTANT_Methodref是由三部分組成的,第一部分是一個位元組的tag,也就是上面的0A。 第二部分是2個位元組的class_index,表示的是類在常量池中的index。 第三部分是2個位元組的name_and_type_index,表示的是方法的名字和型別在常量池中的index。 先看class_index,0002=2。 常量池的第一個元素我們已經找到了就是CONSTANT_Methodref,第二個元素就是跟在CONSTANT_Methodref後面的部分,我們看下是什麼: ~~~java 07 00 04 ~~~ 一樣的解析步驟,07=7,查表,表示的是CONSTANT_Class。 我們再看下CONSTANT_Class的定義: ~~~java CONSTANT_Class_info { u1 tag; u2 name_index; } ~~~ 可以看到CONSTANT_Class佔用3個位元組,第一個位元組是tag,後面兩個位元組是name在常量池中的索引。 00 04 = 4, 表示name在常量池中的索引是4。 然後我們就這樣一路找下去,就得到了所有常量池中常量的資訊。 這樣找起來,眼睛都花了,有沒有什麼簡單的辦法呢? 當然有,就是上面的javap -version, 我們再回顧一下輸出結果中的常量池部分: ~~~java Constant pool: #1 = Methodref #2.#3 // java/lang/Object."":()V #2 = Class #4 // java/lang/Object #3 = NameAndType #5:#6 // "":()V #4 = Utf8 java/lang/Object #5 = Utf8 #6 = Utf8 ()V #7 = Fieldref #8.#9 // com/flydean/JavaClassUsage.age:I #8 = Class #10 // com/flydean/JavaClassUsage #9 = NameAndType #11:#12 // age:I #10 = Utf8 com/flydean/JavaClassUsage #11 = Utf8 age #12 = Utf8 I #13 = Utf8 Code #14 = Utf8 LineNumberTable #15 = Utf8 LocalVariableTable #16 = Utf8 this #17 = Utf8 Lcom/flydean/JavaClassUsage; #18 = Utf8 inc #19 = Utf8 (I)V #20 = Utf8 number #21 = Utf8 SourceFile #22 = Utf8 JavaClassUsage.java ~~~ 以第一行為例,直接告訴你常量池中第一個index的型別是Methodref,它的classref是index=2,它的NameAndType是index=3。 並且直接在後面展示出了具體的值。 ## 描述符 且慢,在常量池中我好像看到了一些不一樣的東西,這些I,L是什麼東西? 這些叫做欄位描述符: ![](https://img-blog.csdnimg.cn/20200616092347300.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 上圖是他們的各項含義。除了8大基礎型別,還有2個引用型別,分別是物件的例項,和陣列。 ## access_flags 常量池後面就是access_flags:訪問描述符,表示的是這個class或者介面的訪問許可權。 先上密碼錶: ![](https://img-blog.csdnimg.cn/20200616092924600.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 再找一下我們16進位制的access_flag: ![](https://img-blog.csdnimg.cn/2020061609304082.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 沒錯,就是00 21。 參照上面的表格,好像沒有21,但是別怕: 21是ACC_PUBLIC和ACC_SUPER的並集。表示它有兩個access許可權。 ## this_class和super_class 接下來是this class和super class的名字,他們都是對常量池的引用。 ~~~java 00 08 00 02 ~~~ this class的常量池index=8, super class的常量池index=2。 看一下2和8都代表什麼: ~~~java #2 = Class #4 // java/lang/Object #8 = Class #10 // com/flydean/JavaClassUsage ~~~ 沒錯,JavaClassUsage的父類是Object。 > 大家知道為什麼java只能單繼承了嗎?因為class檔案裡面只有一個u2的位置,放不下了! ## interfaces_count和interfaces[] 接下來就是介面的數目和介面的具體資訊陣列了。 ~~~java 00 00 ~~~ 我們沒有實現任何介面,所以interfaces_count=0,這時候也就沒有interfaces[]了。 ## fields_count和fields[] 然後是欄位數目和欄位具體的陣列資訊。 這裡的欄位包括類變數和例項變數。 每個欄位資訊也是一個結構體: ~~~java field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } ~~~ 欄位的access_flag跟class的有點不一樣: ![](https://img-blog.csdnimg.cn/20200616121749390.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 這裡我們就不具體對比解釋了,感興趣的小夥伴可以自行體驗。 # methods_count和methods[] 接下來是方法資訊。 method結構體: ~~~java method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; } ~~~ method訪問許可權標記: ![](https://img-blog.csdnimg.cn/20200616122004356.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) # attributes_count和attributes[] attributes被用在ClassFile, field_info, method_info和Code_attribute這些結構體中。 先看下attributes結構體的定義: ~~~java attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; } ~~~ 都有哪些attributes, 這些attributes都用在什麼地方呢? ![](https://img-blog.csdnimg.cn/20200616123053552.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_0,text_aHR0cDovL3d3dy5mbHlkZWFuLmNvbQ==,size_35,color_8F8F8F,t_70) 其中有六個屬性對於Java虛擬機器正確解釋類檔案至關重要,他們是: ConstantValue,Code,StackMapTable,BootstrapMethods,NestHost和NestMembers。 九個屬性對於Java虛擬機器正確解釋類檔案不是至關重要的,但是對於通過Java SE Platform的類庫正確解釋類檔案是至關重要的,他們是: Exceptions,InnerClasses,EnclosingMethod,Synthetic,Signature,SourceFile,LineNumberTable,LocalVariableTable,LocalVariableTypeTable。 其他13個屬性,不是那麼重要,但是包含有關類檔案的元資料。 # 總結 最後留給大家一個問題,java class中常量池的大小constant_pool_count是2個位元組,兩個位元組可以表示2的16次方個常量。很明顯已經夠大了。 但是,萬一我們寫了超過2個位元組大小的常量怎麼辦?歡迎大家留言給我討論。 > 本文連結:[http://www.flydean.com/jvm-class-file-structure/ ](http://www.flydean.com/jvm-class-file-structure/) > > 最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現! > > 歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你! ![](https://img-blog.csdnimg.cn/20200709152618916.png)