引子
? ? 本是新年,怎奈新冠肆掠,路上行人,男女老少幾乎是全副口罩,形色匆匆;偶爾有一兩個裸露口鼻的,估計都是沒囤到口罩的,這幾天藥店幾乎都是貼上大字:口罩沒貨。看著網(wǎng)絡(luò)上病毒消息滿天飛,我也響應(yīng)在家做貢獻的號召。上班時,都是早出晚歸,幾乎只有早上能看到娃,出門時,娃每次都說:see you tomorrow 。趕上疫情,天天在家?guī)蓿K于可以多多陪伴了;別說,帶娃還真比上班費神。想著小時候,特別想有一個玩具小船,動手給娃做了一個,附圖一張。把娃帶好了,也得思考下學(xué)習(xí)的事兒。學(xué)習(xí)java有段時間了,想起之前學(xué)習(xí)java時,看著Class<?> 這樣的符號就怵,不明白其表示的含義,又重讀《java編程思想》第14章, 趁著這樣的時間好好整理了一下,直面當(dāng)時的怵。
Class對象
Class<?> - 類的類型,是運行時類型信息,也就是 RTTI - RTTI - RunTime Type Infomation;所謂一切皆對象,類也是一個對象,而類的類型信息,就叫做Class對象。RTTI使得我們可以在運行時發(fā)現(xiàn)和使用類型信息。以前覺得RTTI離我很遠(java菜鳥),其實多態(tài)機制正是因為類對象攜帶了類的類型信息,在類型轉(zhuǎn)化時可以識別到對象的類型。舉個栗子,如下, ChildClassTest向上轉(zhuǎn)型為?SuperClassTest時,丟失了子類類型信息,而運行時,向下轉(zhuǎn)型時,又使用RTTI 獲取了實際類型,從而可以正常打印出?ChildClassTest。但是,為什么向上轉(zhuǎn)型丟失類型信息,再向下轉(zhuǎn)型時,可以獲取到實際的類型,這要從RTTI 的工作原理說起了。
publicclass SuperClassTest {
}
publicclassChildClassTestextends SuperClassTest {
}
SuperClassTest superClassTest = new ChildClassTest();
PrintTool.print(superClassTest);
#打印
com.hj.tool.klass.ChildClassTest@685f4c2e
RTTI的工作原理
前面的例子中,這種在運行時,確定類的實際類型是虛擬機的動態(tài)分派機制。 為啥對象可以找到類型信息呢,因為普通對象是被Class對象創(chuàng)建的,而Class對象包含了類的有關(guān)信息。下圖為Class對象的加載過程,當(dāng)我們在創(chuàng)建普通對象時,會先判斷此類的Class對象是否加載(每個類都有一個Class對象),如果已經(jīng)加載,就使用Class對象生成普通對象;如果未加載,就需要通過字節(jié)碼創(chuàng)建Class對象,再生成普通對象。在虛擬機層面,則是運行時,把變量 new ChildClassTest()的引用存放于?LocalVariableTable 的 slot中,執(zhí)行print時(其實就是執(zhí)行toString()方法),實際是執(zhí)行invokevirtual 指令,找到方法的實際接收者,再執(zhí)行toString()。而?invokevirtual 解析的過程,根據(jù)《深入理解java虛擬機》中的描述過程如下:
1)找到操作數(shù)棧頂?shù)牡谝粋€元素所指向的對象的實際類型,記作C。
2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權(quán)限校驗,如果通過則返回這個方法的直接引用,查找過程結(jié)束;如果不通過,則返回java.lang.IllegalAccessError異常。
3)否則,按照繼承關(guān)系從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
4)如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。由于invokevirtual指令執(zhí)行的第一步就是在運行期確定接收者的實際類型,所以兩次調(diào)用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,
這個過程就是Java語言中方法重寫的本質(zhì)。我們把這種在運行期根據(jù)實際類型確定方法執(zhí)行版本的分派過程稱為動態(tài)分派。
Class文件結(jié)構(gòu)
? ? ? 既然Class對象來源于字節(jié)碼,那就來分析下.class文件的內(nèi)容,引用《java虛擬機規(guī)范》中關(guān)于classFile的格式如下:“每個class文件都由字節(jié)流組成,每個字節(jié)含有8個二進制位。所有16位,32位,64位長度的數(shù)據(jù)將通過構(gòu)造成2個,4個,8個連續(xù)的8位字節(jié)來表示。”規(guī)范中定義了每個項的字節(jié)長度,以及結(jié)構(gòu),分析的過程還是挺有意思的:原來我們寫的代碼都被編譯成那樣的格式。說來也慚愧,java用了這么久,連一個簡單的.class文件都沒有分析過。
每個class文件都對應(yīng)如下結(jié)構(gòu)(JDK 8,不同版本結(jié)構(gòu)不是完全一樣),其中包括兩類數(shù)據(jù)類型:u(1/2/4), _info; u 后面的數(shù)字表示n個字節(jié),而 每個_info 又有特定的格式。 具體可以參看《java虛擬機規(guī)范 se 8》第4章內(nèi)容。
我們來看下具體的一個類,
package com.hj.tool.klass;
/** * @Description TODO
* @Author jijunjian
* @Date 2020-01-27 20:47
* @Version 1.0
*/publicclass ByteCodeTest {
? ? privateint m ;
? ? publicint inc(){
? ? ? ? returnm+1;
? ? }
}
? ? ? 使用xxd? ByteCodeTest.class 查看編譯后的.class文件(16進制),得到如下內(nèi)容。乍一看,是不是完全看不到,我們的類是如何組織的哇。等我們按class文件的格式整理后,情況就完全不一樣了。
cafe babe 0000 0034 0016 0a00 0400 12090003 0013 0700 1407 0015 0100 016d 01000149 0100 063c 696e 6974 3e01 0003 28295601 0004 436f 6465 0100 0f4c 696e 654e756d 6265 7254 6162 6c65 0100 124c 6f63616c 5661 7269 6162 6c65 5461 626c 65010004 7468 6973 0100 204c 636f 6d2f 686a2f74 6f6f 6c2f 6b6c 6173 732f 4279 7465436f 6465 5465 7374 3b01 0003 696e 63010003 2829 4901 000a 536f 7572 6365 46696c65 0100 1142 7974 6543 6f64 6554 6573742e 6a61 7661 0c00 0700 080c 0005 00060100 1e63 6f6d 2f68 6a2f 746f 6f6c 2f6b6c61 7373 2f42 7974 6543 6f64 6554 65737401 0010 6a61 7661 2f6c 616e 672f 4f626a65 6374 0021 0003 0004 0000 0001 00020005 0006 0000 0002 0001 0007 0008 00010009 0000 002f 0001 0001 0000 0005 2ab70001 b100 0000 0200 0a00 0000 0600 01000000 0900 0b00 0000 0c00 0100 0000 05000c00 0d00 0000 0100 0e00 0f00 0100 09000000 3100 0200 0100 0000 072a b400 020460ac 0000 0002 000a 0000 0006 0001 0000000e 000b 0000 000c 0001 0000 0007 000c000d 0000 0001 0010 0000 0002 0011
以下是整理后的結(jié)果,這個過程還是需要些耐心的。但是這個時間花得決絕物超所值。我解析了大部分內(nèi)容,基本都注釋了,其中常量池占了很多內(nèi)容,但其實是最簡單部分,method中關(guān)于code屬性是比較麻煩的。不同版本編譯得到的內(nèi)容可能會有不同。
#魔數(shù)
cafe babe
#版本 jdk 8
0000 0034
# 常量池有21 個,第一個,是保留
0016
# 第一個常量
CONSTANT_Methodref_info{
u1 tag //10
u2 class_index //指向CONSTANT_Class_info;表示類
u2 name_and_type_index //指向CONSTANT_NameAndType,表示方法名、方法描述符
}
0a? ? tag 10
0004? class_index 指向 4
0012? name_and_type_index 指向 18
# 第二個常量 tag=9
CONSTANT_Fieldref_info{
u1 tag //9
u2 class_index //指向CONSTANT_Class_info;既可以表示類、也可以表示接口
u2 name_and_type_index //指向CONSTANT_NameAndType,表示字段名、字段描述符
}
09? tag 9
0003? class_index? 指向 3
0013? name_and_type_index? 指向19
# 第三個常量 tag=7
CONSTANT_Class_info{
u1 tag //tag=7
u2 name_index // name_index是索引值,指向CONSTANT_Utf8_info
}
07 tag 7
0014 name_index 指向 20 com/hj/tool/klass/ByteCodeTest
# 第4個常量 tag=7
07
0015? name_index 指向 21
# 第5個常量 tag=01
CONSTANT_Utf8_info{
u1 tag //1
u2 length
u1 bytes[length] //長度為length的字符串?dāng)?shù)組
}
01 tag
0001 length
6d asc 109=m
# 第6個常量 tag=01
01
0001 length
49 asc 73 I 表示int
# 第7個常量 tag=01
01
0006
3c 69 6e 69 74 3e? <init>
# 第8個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0003
28 29 56? ()V
# 第9個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0004
43 6f 64 65? Code
# 第10個常量 tag=01 utf8 字符串?dāng)?shù)組
01
000f? length=15
4c 69 6e 65? Line
4e 75 6d 62 65 72? number
54 61 62 6c 65 Table
# 第11個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0012
4c 6f 63 LocalVariableTable
61 6c 56
61 72 69
61 62 6c
65 54 61
62 6c 65
# 第12個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0004
74 68 69 73? this
# 第13個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0020
4c 63 6f 6d
2f 68 6a 2f
74 6f 6f
6c 2f 6b 6c
61 73 73 2f
42 79 74 65
43 6f 64 65
54 65 73 74
3b
Lcom/hj/tool/klass/ByteCodeTest;
3b=;
# 第14個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0003
69 6e 63? inc
# 第15個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0003
28 29 49? ()I
# 第16個常量 tag=01 utf8 字符串?dāng)?shù)組
01
000a
53 6f 75 72 63 65 46 69
6c 65?
SourceFile
# 第17個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0011? 17個
42
79 74 65 43 6f 64 65 54 65 73
74 2e 6a 61 76 61
ByteCodeTest.java
# 第18個常量 tag=12? NameAndType
CONSTANT_NameAndType{
u1 tag //12
u2 name_index //指向CONSTANT_Utf8_info,表示名稱
u2 descriptor_index //指向CONSTANT_Utf8_info,表示描述符
}
0c tag 12 nameAndType
0007 name_index? 指向第7個常量? <init>
0008 descriptor_index 指向第8個常量 ()V
# 第19個常量 tag=12 NameAndType
0c
0005? m
0006? I
# 第20個常量 tag=01 utf8 字符串?dāng)?shù)組
01
001e
63 6f 6d 2f
68 6a 2f
74 6f 6f 6c 2f
6b
6c 61 73 73 2f 42 79 74 65 43 6f 64
65 54 65 73 74
com/hj/tool/klass/ByteCodeTest
# 第21個常量 tag=01 utf8 字符串?dāng)?shù)組
01
0010
6a 61 76 61 2f 6c 61 6e
67 2f 4f 62 6a 65 63 74
java/lang/Object
access_flags
0021? 表示是public ,是1.2以后所以21
類索引,父類索引,接口索引
0003? 類索引 2字節(jié) 指向第三個常量 class-info 又指向 和指向第20個
com/hj/tool/klass/ByteCodeTest
0004? 父類索引 2字節(jié) 同理指向 java/lang/Object
0000? 接口索引 無
0001 field_count u2 1個
field_info[1]
field_info{
u2 access_flags //表示字段的訪問權(quán)限、屬性
u2 name_index //對常量池的索引
u2 descriptor_index //對常量池的索引
u2 attributes_count //附加屬性的數(shù)量
attribute_info attributes[attributes_count] //每個成員是attribute_info結(jié)構(gòu)
}
0002? private
0005 name_index m
0006 descriptor_index I
0000 attributes_count 0
0002 method_count
method_info{
u2 access_flags //表示方法的訪問權(quán)限、屬性
u2 name_index //對常量池的索引
u2 descriptor_index //對常量池的索引
u2 attributes_count//附加屬性的數(shù)量
attribute_info attributes[attributes_count] //每個成員是attribute_info結(jié)構(gòu)
}
# 第一個 method init
0001? access_flags public
0007? name_index? <init>
0008? descriptor_index ()V
0001? attributes_count 1
attribute_info{
u2 attribute_name_index //常量池索引
u4 attribute_length
u1 info[attribute_length]
}
0009 attribute_name_index Code
0000 002f attribute_length 47
0001 max_stack
0001 max_locals
0000 0005 code_attribute_length
2a
b7
0001 b100
00 00 02 00 0a 00
00 00 06 00 01 00 00 00 09 00
0b 00 00 00 0c 00 01 00 00 00
05 00 0c 00 0d 00 00
# 第二個method
0001 access_flags? public
000e name_index 14 inc
000f descriptor_index 15 ()I
0001 attributes_count 1
attribute_info
0009 attribute_name_index Code
0000 0031 attribute_length 49
00 02? max_stack
00 01? max_locals 一個
00 00? 00 07? code_length 7
2a aload_0 將第一個引用類型的本地變量
b4 getfield 獲取指定類型的實例字段 m
#下面這兩個指令沒弄明白是啥意思,
00 nop 不做
02 iconst_ml 將-1 推到棧頂
04 iconst_1? 將1 推到棧頂
60 iadd 將棧頂兩個相加,結(jié)果壓入棧頂
ac ireturn 返回int
00 00? exception_table_length
00 02? attritutes_count 2
00 0a LineNumberTable
00 00 00 06 length=6
00 01 00 00 00 0e
00 0b LocalVariableTable
00 00 00 0c length =12
00 01 00
00 00 07
00 0c 00
0d 00 00
0001? attributes_count 1
0010? attribute_name_index 16 SourceFile
0000 0002 attribute_length 2
0011 sourcefile_index 17 指向常量池中 ByteCodeTest.java
結(jié)語
文章寫到這里,感覺非常艱難,一是感覺寫得不知所云,估計只有自己能明白,二是感覺自己的理解還很淺顯。沒動手之前,感覺啥都理解了,真正開始動手吧,又感覺啥都沒理解。這便是從輸入到輸出的真實過程;讀只是輸入,無法形成真正的理解,只有持續(xù)輸出才能真正領(lǐng)悟,而這個輸出的過程才是消化的過程。寫得過程中,又不斷翻閱資料,把原來點點的理解,連接成斷斷續(xù)續(xù)的線,希望以后可以再深入學(xué)習(xí),把這些點點的東西,連成線,匯成面。
?成為一名優(yōu)秀的程序員!
? ? ? 文章參考了很多《jjava編程思想》,《java虛擬機規(guī)范 se 8》,《深入理解java虛擬機》第二版中的內(nèi)容。