[動態代理三部曲:中] - 從動態代理,看Class檔案結構定義

一支彩筆發表於2018-08-13

前言

這篇內容是上一篇[動態代理三部曲:上] - 動態代理是如何"坑掉了"我4500塊錢的補充,進一步分析篇。 建議二者結合食用,醇香綿軟,入口即化。

好了,不扯淡了,開始...

正文

2、Class 檔案的格式

這裡為啥是2開頭呢?因為上篇文章是1

這部分內容不知道各位小夥伴是怎麼感覺的。最開始學習的時候,我是一頭霧水,不知道如何下手。當一步步結合反射、JVM記憶體模型,類載入機制後。再回過頭來就會發現一起豁然開朗。

此篇內容的開始,讓我們根據我們demo中所用的類:RentHouseProcessorHandler來分析這個問題。
如果我們用十六進位制編輯器(比如:Sublime)開啟這個RentHouseProcessorHandler.class檔案:

十六進位制的Class檔案

說實話這一行行的文字,最開始我是拒絕的。哦,上帝,為什麼要讓我看這些鬼東西...其實如果我們靜下心來,想當年高中時代學習數學,物理公式那樣去認真的對待它。就會發現它不過就是:一堆人為賦予了特別含義的符號而已。

在我們準備讀懂這些十六進位制文字時,先讓我們看一幅《Java虛擬機器規範(Java SE 7)》對class檔案的定義:

《Java虛擬機器規範(Java SE 7)》


2.1、Class檔案的規範結構

2.1.1、標準結構

上圖的內容,其實非常的通俗易懂,不要因為是不常見的英文就牴觸它們。讓我們嘗試著去翻譯它們: 1、魔數;2、次版本號;3、主版本號;4、常量池數量;5、常量池;6、許可權標識;7、此類;8、父類;9、介面數目;10、介面;11、變數數目;12、變數;13、方法數目;14、方法....

Class結構

其實是不是發現了什麼,這不是就一個類應該存在的東西麼?沒錯啊,Class檔案的結構就是固定了我們編寫的Class類所存放的規則而已。最開始的我,以為是深奧,沒敢去了解他們。當我躊躇滿志,鼓足勇氣去準備好好大幹一場的時候,才發現它太簡單了...就是一些規則,僅此而已。

2.1.2、特別注意的結構:表

雖然只是一些規則,但規則之中,總會有一些特別需要我們去注意的地方:比如cp_info這個型別。在《深入理解Java虛擬機器》中,作者把以_info結尾的型別稱之為“表”。這裡讓我們也沿用這種表達方式。說白了,它就是擁有多級關係的型別。

cp_info 表示常量池(常量池:首先它和方法區中執行時常量池不是同一個內容。這裡的常量池存放了字面量和符號引用)。


符號引用:

符號引用:

  • 類和介面的全侷限定名
  • 欄位的名稱和描述符
  • 方法的名稱和描述符

這裡符號引用的作用,我們想先一個問題。CPU執行程式的時候,實際上是去尋找對應指令的記憶體地址。但是我們的Class檔案是先被編譯出來的,但是此時還沒有被JVM載入到記憶體,所以肯定是不可能存在記憶體地址這一說的。因此我們的Class檔案需要一些標識,讓JVM載入內容的時候從常量池中獲取到對應的符號引用,然後在對映到具體的記憶體地址上。


放到常量池的中資料項在《Java虛擬機器規範(Java SE 7)》中一共有14個常量,每一種常量都是一個“表”,並且每種常量都用一個公共的tag來表示是哪種型別的常量。具體內容如下圖:

表型別.png

這裡讓我們先解讀一下這個常量池:讓我們跳過u4的魔數、u2的此版本、u2的主版本。直接來看constant_pool_count。跳過對應的內容,那麼我們的constant_pool_count就對應十六進位制的20,對應十進位制的32,也就是說常量池中有32個內容?實際不是的,因為設計者將第0個位置空出來另做打算。所以我們的常量池只有31個內容。

constant_pool_count

我們可以通過javap命令證實這個問題。

javap

接下來的一個位元組:0a,翻譯成十進位制就是10,對應我們表中的CONSTANT_Methodref_info,而接下來的四個位元組。分別代表索引3,20。這裡的索引代表什麼意思呢?注意理解下圖中標紅的地方:

常量池只有31個內容

接下來就不逐個解讀這些內容了,因為它就是一個對應的過程。如果小夥伴們有興趣可以自行去嘗試解讀一番哦。推薦一個工具JavaClassViewer,可以比較方便的檢視這些內容:

JavaClassViewer-左檢視

JavaClassViewer-右檢視


常量池結束之後,便是我們正常的變數,方法的資訊。而這裡我們需要了解一個全新的概念:描述符。 對於我們來說一個變數、方法在java原始碼裡是什麼樣子我們很清楚。但是它們在class檔案裡是什麼樣子的呢?這個樣子其實就被稱之為:描述符。

上文談動態代理的時候,我們瞭解到了ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);方法中,通過:

dout.writeInt(0xCAFEBABE);
MethodInfo minfo = new MethodInfo("<init>", "(Ljava/lang/reflect/InvocationHandler;)V",ACC_PUBLIC);    
複製程式碼

等方法,構建了我們的$Proxy0所需要的class結構,是不是和我們javap出來的內容很類似?接下來讓我們走進描述符。

2.2、變數、方法的描述符

經過上述內容的鋪墊,0xCAFEBABE是什麼意思,應該無需多言了。而<init>是構造方法的意思。再加上(Ljava/lang/reflect/InvocationHandler;)V以及ACC_PUBLIC就可以表示為InvocationHandler的public的構造方法,其中V表示無返回值。

  • <init>:物件構造器方法。
  • <clinit>:類構造器方法。

這裡的內容就被稱之為方法的描述符,讓我們簡單的看一些圖,加深這方面內容。

型別描述符

基本型別和void在描述符中都有一個大寫字元和他們對應; 那麼引用型別的描述符,又是什麼樣子的呢?

“L” + 型別的全限定名 + “;”

例如下圖中的:Ljava/lang/Object;就是表示這是一個Object型別。

方法對應的描述符

而方法描述符的規則也很簡單,上圖中,總結出來就是一句話:

(引數型別1引數型別2引數型別3 ...)返回值型別

例如上圖中的:int[] m(int i,String s)轉換為描述符:(ILjava/lang/String;)[I

不知道截了這麼多圖,大家對描述符有沒有比較明確的認識。說白了我們我們ProxyGenerator.generateProxyClass(proxyName,interfaces, accessFlags);中所write的內容就是具體方法的描述符。

然後通過DataOutputStream轉成byte陣列,那麼就是我們Class檔案所固定的內容了。因此,此時我們的Class檔案就已經構建完畢,接下來所需要的就是將其載入到記憶體中,供我們使用。

2.3、收尾

到這準備結束class檔案結構的內容。不知道小夥伴們是否有收穫。因為篇幅是在有限,有些內容又不是一句話倆句話可以描述清楚的。所以有些內容一帶而過,實在抱歉。具體細節內容,大家可以參考《深入理解Java虛擬機器》。

希望大家可以諒解。

我是一個應屆生,最近和朋友們維護了一個公眾號,內容是我們在從應屆生過渡到開發這一路所踩過的坑,已經我們一步步學習的記錄,如果感興趣的朋友可以關注一下,一同加油~

個人公眾號:IT面試填坑小分隊

相關文章