認識Class -- 終於不在慫

引子   

     本是新年,怎奈新冠肆掠,路上行人,男女老少幾乎是全副口罩,形色匆匆;偶爾有一兩個裸露口鼻的,估計都是沒囤到口罩的,這幾天藥店幾乎都是貼上大字:口罩沒貨。看着網絡上病毒消息滿天飛,我也響應在家做貢獻的號召。上班時,都是早出晚歸,幾乎只有早上能看到娃,出門時,娃每次都說:see you tomorrow 。趕上疫情,天天在家帶娃,終於可以多多陪伴了;別說,帶娃還真比上班費神。想着小時候,特別想有一個玩具小船,動手給娃做了一個,附圖一張。把娃帶好了,也得思考下學習的事兒。學習java有段時間了,想起之前學習java時,看着Class<?> 這樣的符號就怵,不明白其表示的含義,又重讀《java編程思想》第14章, 趁着這樣的時間好好整理了一下,直面當時的怵。

                                                 

 

Class對象

  Class<?> - 類的類型,是運行時類型信息,也就是 RTTI - RTTI - RunTime Type Infomation;所謂一切皆對象,類也是一個對象,而類的類型信息,就叫做Class對象。RTTI使得我們可以在運行時發現和使用類型信息。以前覺得RTTI離我很遠(java菜鳥),其實多態機制正是因爲類對象攜帶了類的類型信息,在類型轉化時可以識別到對象的類型。舉個栗子,如下, ChildClassTest向上轉型爲 SuperClassTest時,丟失了子類類型信息,而運行時,向下轉型時,又使用RTTI 獲取了實際類型,從而可以正常打印出 ChildClassTest。但是,爲什麼向上轉型丟失類型信息,再向下轉型時,可以獲取到實際的類型,這要從RTTI 的工作原理說起了。

   

public class SuperClassTest {
}
 
public class ChildClassTest extends SuperClassTest {
}

SuperClassTest superClassTest = new ChildClassTest();
PrintTool.print(superClassTest);
#打印 

com.hj.tool.klass.ChildClassTest@685f4c2e

 

RTTI的工作原理

  前面的例子中,這種在運行時,確定類的實際類型是虛擬機的動態分派機制。 爲啥對象可以找到類型信息呢,因爲普通對象是被Class對象創建的,而Class對象包含了類的有關信息。下圖爲Class對象的加載過程,當我們在創建普通對象時,會先判斷此類的Class對象是否加載(每個類都有一個Class對象),如果已經加載,就使用Class對象生成普通對象;如果未加載,就需要通過字節碼創建Class對象,再生成普通對象。在虛擬機層面,則是運行時,把變量 new ChildClassTest()的引用存放於 LocalVariableTable 的 slot中,執行print時(其實就是執行toString()方法),實際是執行invokevirtual 指令,找到方法的實際接收者,再執行toString()。而 invokevirtual 解析的過程,根據《深入理解java虛擬機》中的描述過程如下:

1)找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C。
2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。
3)否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。
4)如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。

由於invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,所以兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,
這個過程就是Java語言中方法重寫的本質。我們把這種在運行期根據實際類型確定方法執行版本的分派過程稱爲動態分派。

 

  

 

 

 

Class文件結構

      既然Class對象來源於字節碼,那就來分析下.class文件的內容,引用《java虛擬機規範》中關於classFile的格式如下:“每個class文件都由字節流組成,每個字節含有8個二進制位。所有16位,32位,64位長度的數據將通過構造成2個,4個,8個連續的8位字節來表示。”規範中定義了每個項的字節長度,以及結構,分析的過程還是挺有意思的:原來我們寫的代碼都被編譯成那樣的格式。說來也慚愧,java用了這麼久,連一個簡單的.class文件都沒有分析過。

  每個class文件都對應如下結構(JDK 8,不同版本結構不是完全一樣),其中包括兩類數據類型:u(1/2/4), _info; u 後面的數字表示n個字節,而 每個_info 又有特定的格式。 具體可以參看《java虛擬機規範 se 8》第4章內容。

  

   我們來看下具體的一個類,

package com.hj.tool.klass;

/**
 * @Description TODO
 * @Author jijunjian
 * @Date 2020-01-27 20:47
 * @Version 1.0
 */
public class ByteCodeTest {

    private int m ;

    public int inc(){
        return m+1;
    }
}

      使用xxd  ByteCodeTest.class 查看編譯後的.class文件(16進制),得到如下內容。乍一看,是不是完全看不到,我們的類是如何組織的哇。等我們按class文件的格式整理後,情況就完全不一樣了。

  

cafe babe 0000 0034 0016 0a00 0400 1209
0003 0013 0700 1407 0015 0100 016d 0100
0149 0100 063c 696e 6974 3e01 0003 2829
5601 0004 436f 6465 0100 0f4c 696e 654e
756d 6265 7254 6162 6c65 0100 124c 6f63
616c 5661 7269 6162 6c65 5461 626c 6501
0004 7468 6973 0100 204c 636f 6d2f 686a
2f74 6f6f 6c2f 6b6c 6173 732f 4279 7465
436f 6465 5465 7374 3b01 0003 696e 6301
0003 2829 4901 000a 536f 7572 6365 4669
6c65 0100 1142 7974 6543 6f64 6554 6573
742e 6a61 7661 0c00 0700 080c 0005 0006
0100 1e63 6f6d 2f68 6a2f 746f 6f6c 2f6b
6c61 7373 2f42 7974 6543 6f64 6554 6573
7401 0010 6a61 7661 2f6c 616e 672f 4f62
6a65 6374 0021 0003 0004 0000 0001 0002
0005 0006 0000 0002 0001 0007 0008 0001
0009 0000 002f 0001 0001 0000 0005 2ab7
0001 b100 0000 0200 0a00 0000 0600 0100
0000 0900 0b00 0000 0c00 0100 0000 0500
0c00 0d00 0000 0100 0e00 0f00 0100 0900
0000 3100 0200 0100 0000 072a b400 0204
60ac 0000 0002 000a 0000 0006 0001 0000
000e 000b 0000 000c 0001 0000 0007 000c
000d 0000 0001 0010 0000 0002 0011 

 

以下是整理後的結果,這個過程還是需要些耐心的。但是這個時間花得決絕物超所值。我解析了大部分內容,基本都註釋了,其中常量池佔了很多內容,但其實是最簡單部分,method中關於code屬性是比較麻煩的。不同版本編譯得到的內容可能會有不同。

#魔數
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的字符串數組
}

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 字符串數組
01
0003 
28 29 56  ()V

# 第9個常量 tag=01 utf8 字符串數組
01 
0004 
43 6f 64 65  Code 

# 第10個常量 tag=01 utf8 字符串數組
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 字符串數組
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 字符串數組

01
0004 
74 68 69 73  this

# 第13個常量 tag=01 utf8 字符串數組


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 字符串數組

01 
0003 
69 6e 63  inc

# 第15個常量 tag=01 utf8 字符串數組
01
0003 
28 29 49  ()I

# 第16個常量 tag=01 utf8 字符串數組
01 
000a 
53 6f 75 72 63 65 46 69
6c 65  
SourceFile

# 第17個常量 tag=01 utf8 字符串數組
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 字符串數組

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 字符串數組

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字節 指向第三個常量 class-info 又指向 和指向第20個
com/hj/tool/klass/ByteCodeTest

0004  父類索引 2字節 同理指向 java/lang/Object
0000  接口索引 無


0001 field_count u2 1個

field_info[1]
field_info{
u2 access_flags //表示字段的訪問權限、屬性
u2 name_index //對常量池的索引
u2 descriptor_index //對常量池的索引
u2 attributes_count //附加屬性的數量
attribute_info attributes[attributes_count] //每個成員是attribute_info結構
}

0002  private
0005 name_index m
0006 descriptor_index I
0000 attributes_count 0

0002 method_count

method_info{
u2 access_flags //表示方法的訪問權限、屬性
u2 name_index //對常量池的索引
u2 descriptor_index //對常量池的索引
u2 attributes_count//附加屬性的數量
attribute_info attributes[attributes_count] //每個成員是attribute_info結構
}

# 第一個 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 將棧頂兩個相加,結果壓入棧頂
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

 

結語

  文章寫到這裏,感覺非常艱難,一是感覺寫得不知所云,估計只有自己能明白,二是感覺自己的理解還很淺顯。沒動手之前,感覺啥都理解了,真正開始動手吧,又感覺啥都沒理解。這便是從輸入到輸出的真實過程;讀只是輸入,無法形成真正的理解,只有持續輸出才能真正領悟,而這個輸出的過程纔是消化的過程。寫得過程中,又不斷翻閱資料,把原來點點的理解,連接成斷斷續續的線,希望以後可以再深入學習,把這些點點的東西,連成線,匯成面。

     

   成爲一名優秀的程序員!

 

      文章參考了很多《jjava編程思想》,《java虛擬機規範 se 8》,《深入理解java虛擬機》第二版中的內容。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章