認識Class -- 終于不在慫

引子

 ? ? 本是新年,怎奈新冠肆掠,路上行人,男女老少幾乎是全副口罩,形色匆匆;偶爾有一兩個裸露口鼻的,估計都是沒囤到口罩的,這幾天藥店幾乎都是貼上大字:口罩沒貨。看著網(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)容。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,914評論 2 372

推薦閱讀更多精彩內(nèi)容