JVM詳解之:java class檔案的密碼本
阿新 • • 發佈:2020-07-15
[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)