在家上班重新認識Class又重讀《java編程思想》新感悟


在家上班重新認識Class又重讀《java編程思想》新感悟


學習java有段時間了,想起之前學習java時,看著Class> 這樣的符號就怵,不明白其表示的含義,又重讀《java編程思想》第14章, 趁著這樣的時間好好整理了一下,直面當時的怵。


Class對象

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


<code>public class SuperClassTest {} public class ChildClassTest extends SuperClassTest {}/<code>
<code>SuperClassTest superClassTest = new ChildClassTest();PrintTool.print(superClassTest);/<code>
<code>#打印 com.hj.tool.klass.ChildClassTest@685f4c2e/<code>

 

RTTI的工作原理

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

<code>1)找到操作數棧頂的第一個元素所指向的對象的實際類型,記作C。2)如果在類型C中找到與常量中的描述符和簡單名稱都相符的方法,則進行訪問權限校驗,如果通過則返回這個方法的直接引用,查找過程結束;如果不通過,則返回java.lang.IllegalAccessError異常。3)否則,按照繼承關係從下往上依次對C的各個父類進行第2步的搜索和驗證過程。4)如果始終沒有找到合適的方法,則拋出java.lang.AbstractMethodError異常。由於invokevirtual指令執行的第一步就是在運行期確定接收者的實際類型,所以兩次調用中的invokevirtual指令把常量池中的類方法符號引用解析到了不同的直接引用上,這個過程就是Java語言中方法重寫的本質。我們把這種在運行期根據實際類型確定方法執行版本的分派過程稱為動態分派。/<code> 
在家上班重新認識Class又重讀《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章內容。

  

在家上班重新認識Class又重讀《java編程思想》新感悟

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

<code>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;    }}/<code>

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

  

<code>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/<code>

 

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

<code>#魔數cafe babe #版本 jdk 80000 0034 # 常量池有21 個,第一個,是保留0016 # 第一個常量 CONSTANT_Methodref_info{u1 tag //10u2 class_index //指向CONSTANT_Class_info;表示類u2 name_and_type_index //指向CONSTANT_NameAndType,表示方法名、方法描述符}0a    tag 10 0004  class_index 指向 40012  name_and_type_index 指向 18# 第二個常量 tag=9CONSTANT_Fieldref_info{u1 tag //9u2 class_index //指向CONSTANT_Class_info;既可以表示類、也可以表示接口u2 name_and_type_index //指向CONSTANT_NameAndType,表示字段名、字段描述符}09   tag 90003  class_index  指向 30013  name_and_type_index  指向19# 第三個常量 tag=7CONSTANT_Class_info{u1 tag //tag=7u2 name_index // name_index是索引值,指向CONSTANT_Utf8_info}07 tag 70014 name_index 指向 20 com/hj/tool/klass/ByteCodeTest# 第4個常量 tag=707 0015  name_index 指向 21# 第5個常量 tag=01CONSTANT_Utf8_info{u1 tag //1u2 lengthu1 bytes[length] //長度為length的字符串數組}01 tag0001 length6d asc 109=m# 第6個常量 tag=01010001 length49 asc 73 I 表示int# 第7個常量 tag=010100063c 69 6e 69 74 3e  <init># 第8個常量 tag=01 utf8 字符串數組010003 28 29 56  ()V# 第9個常量 tag=01 utf8 字符串數組01 0004 43 6f 64 65  Code # 第10個常量 tag=01 utf8 字符串數組01000f   length=154c 69 6e 65   Line 4e 75 6d 62 65 72  number54 61 62 6c 65 Table# 第11個常量 tag=01 utf8 字符串數組0100124c 6f 63 LocalVariableTable61 6c 56 61 72 69 61 62 6c 65 54 61 62 6c 65# 第12個常量 tag=01 utf8 字符串數組010004 74 68 69 73  this# 第13個常量 tag=01 utf8 字符串數組0100204c 63 6f 6d 2f 68 6a 2f 74 6f 6f 6c 2f 6b 6c 61 73 73 2f 42 79 74 6543 6f 64 65 54 65 73 74 3bLcom/hj/tool/klass/ByteCodeTest; 3b=;# 第14個常量 tag=01 utf8 字符串數組01 0003 69 6e 63  inc# 第15個常量 tag=01 utf8 字符串數組010003 28 29 49  ()I# 第16個常量 tag=01 utf8 字符串數組01 000a 53 6f 75 72 63 65 46 696c 65  SourceFile# 第17個常量 tag=01 utf8 字符串數組010011  17個42 79 74 65 43 6f 64 65 54 65 7374 2e 6a 61 76 61 ByteCodeTest.java# 第18個常量 tag=12  NameAndTypeCONSTANT_NameAndType{u1 tag //12u2 name_index //指向CONSTANT_Utf8_info,表示名稱u2 descriptor_index //指向CONSTANT_Utf8_info,表示描述符}0c tag 12 nameAndType0007 name_index  指向第7個常量  <init>0008 descriptor_index 指向第8個常量 ()V# 第19個常量 tag=12 NameAndType0c 0005  m0006  I# 第20個常量 tag=01 utf8 字符串數組01001e63 6f 6d 2f 68 6a 2f 74 6f 6f 6c 2f 6b6c 61 73 73 2f 42 79 74 65 43 6f 64 65 54 65 73 74com/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/Objectaccess_flags0021  表示是public ,是1.2以後所以21類索引,父類索引,接口索引0003  類索引 2字節 指向第三個常量 class-info 又指向 和指向第20個com/hj/tool/klass/ByteCodeTest0004  父類索引 2字節 同理指向 java/lang/Object0000  接口索引 無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  private0005 name_index m0006 descriptor_index I0000 attributes_count 00002 method_countmethod_info{u2 access_flags //表示方法的訪問權限、屬性u2 name_index //對常量池的索引u2 descriptor_index //對常量池的索引u2 attributes_count//附加屬性的數量attribute_info attributes[attributes_count] //每個成員是attribute_info結構}# 第一個 method init0001  access_flags public0007  name_index  <init>0008  descriptor_index ()V0001  attributes_count 1attribute_info{u2 attribute_name_index //常量池索引u4 attribute_lengthu1 info[attribute_length]}0009 attribute_name_index Code0000 002f attribute_length 470001 max_stack0001 max_locals0000 0005 code_attribute_length 2ab70001 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# 第二個method0001 access_flags  public000e name_index 14 inc000f descriptor_index 15 ()I0001 attributes_count 1attribute_info0009 attribute_name_index Code0000 0031 attribute_length 4900 02   max_stack00 01   max_locals 一個00 00  00 07  code_length 72a aload_0 將第一個引用類型的本地變量b4 getfield 獲取指定類型的實例字段 m#下面這兩個指令沒弄明白是啥意思,00 nop 不做02 iconst_ml 將-1 推到棧頂04 iconst_1  將1 推到棧頂60 iadd 將棧頂兩個相加,結果壓入棧頂ac ireturn 返回int00 00  exception_table_length00 02   attritutes_count 200 0a LineNumberTable00 00 00 06 length=600 01 00 00 00 0e 00 0b LocalVariableTable 00 00 00 0c length =1200 01 00 00 00 07 00 0c 00 0d 00 00 0001  attributes_count 10010  attribute_name_index 16 SourceFile0000 0002 attribute_length 2 0011 sourcefile_index 17 指向常量池中 ByteCodeTest.jav/<init>/<init>/<init>/<code> 


結語

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

成為一名優秀的程序員!

好了各位,本文到這裡就結束了!如果本文有任何錯誤,請批評指教,不勝感激 !


最後:

為了幫助大家少走彎路,我總結出一個Java程序員的工作2-5年成長路線圖。


在家上班重新認識Class又重讀《java編程思想》新感悟

上面都是自己整理好的!我就把資料貢獻出來給有需要的人!順便求一波關注,哈哈~各位小夥伴關注我後私信【Java】就可以免費領取噠


分享到:


相關文章: