深入理解Java Class文件格式
首先, 讓我們回顧一下關于class文件格式的之前兩篇博客的主要內容。 在?深入理解Java Class文件格式(一)?中, 講解了class文件在整個java體系結構中的位置和作用, 講解了class文件中的魔數和版本號相關的信息, 并且對常量池進行了概述。 在?深入理解Java Class文件格式(二)?中, 主要講解了class文件中的特殊字符串, 包括類的全限定名, 字段描述符和方法描述符, 這些特殊字符串大量出現在class文件的常量池中, 是理解常量池的基礎。 本文會詳細講解常量池中的各個數據項。
如果你還沒有讀過前兩篇文章, 建議先去讀一下, 這樣才能保持知識的連貫性。 前兩篇文章的鏈接已經在上面給出。 下面開始講解常量池。
常量池中各數據項類型詳解
關于常量池的大概內容, 已經在?深入理解Java Class文件格式(一)?中講解過了, 這篇文章中還介紹了常量池中的11種數據類型。 本文的任務是詳細講解這11種數據類型, 深度剖析源文件中的各種信息是以什么方式存放在常量池中的。?
我們知道, 常量池中的數據項是通過索引來引用的, 常量池中的各個數據項之間也會相互引用。在這11中常量池數據項類型中, 有兩種比較基礎, 之所以說它們基礎, 是因為這兩種類型的數據項會被其他類型的數據項引用。 這兩種數據類型就是CONSTANT_Utf8 和 CONSTANT_NameAndType , 其中CONSTANT_NameAndType類型的數據項(CONSTANT_NameAndType_info)也會引用CONSTANT_Utf8類型的數據項(CONSTANT_Utf8_info) 。 與其他介紹常量池的書籍或其他資料不同, 本著循序漸進和先后分明的原則, 我們首先對這兩種比較基本的類型的數據項進行介紹, 然后再依次介紹其他9中數據項。?
(1)?CONSTANT_Utf8_info
一個CONSTANT_Utf8_info是一個CONSTANT_Utf8類型的常量池數據項, 它存儲的是一個常量字符串。 常量池中的所有字面量幾乎都是通過CONSTANT_Utf8_info描述的。下面我們首先講解CONSTANT_Utf8_info數據項的存儲格式。在前面的文章中, 我們提到, 常量池中數據項的類型由一個整型的標志值(tag)決定, 所以所有常量池類型的info中都必須有一個tag信息, 并且這個tag值位于數據項的第一個字節上。 一個11中常量池數據類型, 所以就有11個tag值表示這11中類型。而CONSTANT_Utf8_info的tag值為1, 也就是說如果虛擬機要解析一個常量池數據項, 首先去讀這個數據項的第一個字節的tag值, 如果這個tag值為1, 那么就說明這個數據項是一個CONSTANT_Utf8類型的數據項。 緊挨著tag值的兩個字節是存儲的字符串的長度length, 剩下的字節就存儲著字符串。 所以, 它的格式是這樣的:
其中tag占一個字節, length占2個字節, bytes代表存儲的字符串, 占length字節。所以, 如果這個CONSTANT_Utf8_info存儲的是字符串"Hello", 那么他的存儲形式是這樣的:
現在我們知道了CONSTANT_Utf8_info數據項的存儲形式, 那么CONSTANT_Utf8_info數據項都存儲了什么字符串呢??CONSTANT_Utf8_info可包括的字符串主要以下這些:
程序中的字符串常量
常量池所在當前類(包括接口和枚舉)的全限定名
常量池所在當前類的直接父類的全限定名
常量池所在當前類型所實現或繼承的所有接口的全限定名
常量池所在當前類型中所定義的字段的名稱和描述符
常量池所在當前類型中所定義的方法的名稱和描述符
由當前類所引用的類型的全限定名
由當前類所引用的其他類中的字段的名稱和描述符
由當前類所引用的其他類中的方法的名稱和描述符
與當前class文件中的屬性相關的字符串, 如屬性名等
總結一下, 其中有這么五類: 程序中的字符串常量, 類型的全限定名, 方法和字段的名稱, 方法和字段的描述符, 屬性相關字符串。 程序中的字符串常量不用多說了, 我們經常使用它們創建字符串對象, 屬性相關的字符串, 等到講到class中的屬性信息(attibute)時自會提及。 方法和字段的名稱也不用多說了 。 剩下的就是類型的全限定名,方法和字段的描述符, 這就是上篇文章中提及的"特殊字符串", 不熟悉的同學可以先讀一下上篇文章?深入理解Java Class文件格式(二)?。 還有一點需要說明, 類型的全限定名, 方法和字段的名稱, 方法和字段的描述符, 可以是本類型中定義的, 也可能是本類中引用的其他類的。?
下面我們通過一個例子來進行說明。 示例源碼:
package com.jg.zhang;
?
public class Programer extends Person {
?
?? ?static String company = "CompanyA";
?? ?
?? ?static{
?? ??? ?System.out.println("staitc init");
?? ?}
?? ?
?? ?
?? ?String position;
?? ?Computer computer;
?
?? ?public Programer() {
?? ??? ?this.position = "engineer";
?? ??? ?this.computer = new Computer();
?? ?}
?? ?
?? ?public void working(){
?? ??? ?System.out.println("coding...");
?? ??? ?computer.working();
?? ?}
}
別看這個類簡單, 但是反編譯后, 它的常量池有53項之多。 在這53項常量池數據項中, 各種類型的數據項都有, 當然也包括不少的CONSTANT_Utf8_info 。 下面只列出反編譯后常量池中的CONSTANT_Utf8_info 數據項:
#2 = Utf8 ? ? ? ? ? ? ? com/jg/zhang/Programer ? ? ? ? ?//當前類的全限定名
#4 = Utf8 ? ? ? ? ? ? ? com/jg/zhang/Person ? ? ? ? ? ? //父類的全限定名
#5 = Utf8 ? ? ? ? ? ? ? company ? ? ? ? ? ? ? ? ? ? ? ? //company字段的名稱
#6 = Utf8 ? ? ? ? ? ? ? Ljava/lang/String; ? ? ? ? ? ? ?//company和position字段的描述符
#7 = Utf8 ? ? ? ? ? ? ? position ? ? ? ? ? ? ? ? ? ? ? ?//position字段的名稱
#8 = Utf8 ? ? ? ? ? ? ? computer ? ? ? ? ? ? ? ? ? ? ? ?//computer字段的名稱
#9 = Utf8 ? ? ? ? ? ? ? Lcom/jg/zhang/Computer; ? ? ? ? //computer字段的描述符
#10 = Utf8 ? ? ? ? ? ? ?<clinit> ? ? ? ? ? ? ? ? ? ? ? ?//類初始化方法(即靜態初始化塊)的方法名
#11 = Utf8 ? ? ? ? ? ? ?()V ? ? ? ? ? ? ? ? ? ? ? ? ? ? //working方法的描述符
#12 = Utf8 ? ? ? ? ? ? ?Code ? ? ? ? ? ? ? ? ? ? ? ? ? ?//Code屬性的屬性名
#14 = Utf8 ? ? ? ? ? ? ?CompanyA ? ? ? ? ? ? ? ? ? ? ? ?//程序中的常量字符串
#19 = Utf8 ? ? ? ? ? ? ?java/lang/System ? ? ? ? ? ? ? ?//所引用的System類的全限定名
#21 = Utf8 ? ? ? ? ? ? ?out ? ? ? ? ? ? ? ? ? ? ? ? ? ? //所引用的out字段的字段名
#22 = Utf8 ? ? ? ? ? ? ?Ljava/io/PrintStream; ? ? ? ? ? //所引用的out字段的描述符
#24 = Utf8 ? ? ? ? ? ? ?staitc init ? ? ? ? ? ? ? ? ? ? //程序中的常量字符串
#27 = Utf8 ? ? ? ? ? ? ?java/io/PrintStream ? ? ? ? ? ? //所引用的PrintStream類的全限定名
#29 = Utf8 ? ? ? ? ? ? ?println ? ? ? ? ? ? ? ? ? ? ? ? //所引用的println方法的方法名
#30 = Utf8 ? ? ? ? ? ? ?(Ljava/lang/String;)V ? ? ? ? ? //所引用的println方法的描述符
#31 = Utf8 ? ? ? ? ? ? ?LineNumberTable ? ? ? ? ? ? ? ? //LineNumberTable屬性的屬性名
#32 = Utf8 ? ? ? ? ? ? ?LocalVariableTable ? ? ? ? ? ? ?//LocalVariableTable屬性的屬性名
#33 = Utf8 ? ? ? ? ? ? ?<init> ? ? ? ? ? ? ? ? ? ? ? ? ?//當前類的構造方法的方法名
#41 = Utf8 ? ? ? ? ? ? ?com/jg/zhang/Computer ? ? ? ? ? //所引用的Computer類的全限定名
#45 = Utf8 ? ? ? ? ? ? ?this ? ? ? ? ? ? ? ? ? ? ? ? ? ?//局部變量this的變量名
#46 = Utf8 ? ? ? ? ? ? ?Lcom/jg/zhang/Programer; ? ? ? ?//局部變量this的描述符
#47 = Utf8 ? ? ? ? ? ? ?working ? ? ? ? ? ? ? ? ? ? ? ? //woking方法的方法名
#49 = Utf8 ? ? ? ? ? ? ?coding... ? ? ? ? ? ? ? ? ? ? ? //程序中的字符串常量
#52 = Utf8 ? ? ? ? ? ? ?SourceFile ? ? ? ? ? ? ? ? ? ? ?//SourceFile屬性的屬性名
#53 = Utf8 ? ? ? ? ? ? ?Programer.java ? ? ? ? ? ? ? ? ?//當前類所在的源文件的文件名
上面只列出了反編譯結果中常量池中的CONSTANT_Utf8_info數據項。 其中第三列不是javap反編譯的輸出結果, 而是我加上的注釋。 讀者可以對比上面的程序源碼來看一下, 這樣的話, 就可以清楚的看出, 源文件中的各種字符串, 是如何和存放到CONSTANT_Utf8_info中的。
這里要強調一下, 源文件中的幾乎所有可見的字符串都存放在CONSTANT_Utf8_info中, 其他類型的常量池項只不過是對CONSTANT_Utf8_info的引用。 其他常量池項, 把引用的CONSTANT_Utf8_info組合起來, 進而可以描述更多的信息。 下面將要介紹的CONSTANT_NameAndType_info就可以驗證這個結論。
(2)?CONSTANT_NameAndType類型的數據項
常量池中的一個CONSTANT_NameAndType_info數據項, 可以看做CONSTANT_NameAndType類型的一個實例 。 從這個數據項的名稱可以看出, 它描述了兩種信息,第一種信息是名稱(Name), 第二種信息是類型(Type) 。 這里的名稱是指方法的名稱或者字段的名稱, 而Type是廣義上的類型, 它其實描述的是字段的描述符或方法的描述符。 也就是說, 如果Name部分是一個字段名稱, 那么Type部分就是相應字段的描述符; 如果Name部分描述的是一個方法的名稱, 那么Type部分就是對應的方法的描述符。 也就是說, 一個CONSTANT_NameAndType_info就表示了一個方法或一個字段。?
下面先看一下CONSTANT_NameAndType_info數據項的存儲格式。 既然是常量池中的一種數據項類型, 那么它的第一個字節也是tag, 它的tag值是12, 也就是說, 當虛擬機讀到一個tag為12的常量池數據項, 就可以確定這個數據項是一個CONSTANT_NameAndType_info 。 tag值一下的兩個字節叫做name_index, 它指向常量池中的一個CONSTANT_Utf8_info, 這個CONSTANT_Utf8_info中存儲的就是方法或字段的名稱。 name_index以后的兩個字節叫做descriptor_index, 它指向常量池中的一個CONSTANT_Utf8_info, 這個CONSTANT_Utf8_info中存儲的就是方法或字段的描述符。 下圖表示它的存儲布局:
下面舉一個實例進行說明, 實例的源碼為:
package com.jg.zhang;
?
public class Person {
?
?? ?int age;
?
?? ?int getAge(){
?? ??? ?return age;
?? ?}
}
這個Person類很簡單, 只有一個字段age, 和一個方法getAge 。 將這段代碼使用javap工具反編譯之后, 常量池信息如下:
? ?#1 = Class ? ? ? ? ? ? ?#2 ? ? ? ? ? ? // ?com/jg/zhang/Person
? ?#2 = Utf8 ? ? ? ? ? ? ? com/jg/zhang/Person
? ?#3 = Class ? ? ? ? ? ? ?#4 ? ? ? ? ? ? // ?java/lang/Object
? ?#4 = Utf8 ? ? ? ? ? ? ? java/lang/Object
? ?#5 = Utf8 ? ? ? ? ? ? ? age
? ?#6 = Utf8 ? ? ? ? ? ? ? I
? ?#7 = Utf8 ? ? ? ? ? ? ? <init>
? ?#8 = Utf8 ? ? ? ? ? ? ? ()V
? ?#9 = Utf8 ? ? ? ? ? ? ? Code
? #10 = Methodref ? ? ? ? ?#3.#11 ? ? ? ? // ?java/lang/Object."<init>":()V
? #11 = NameAndType ? ? ? ?#7:#8 ? ? ? ? ?// ?"<init>":()V
? #12 = Utf8 ? ? ? ? ? ? ? LineNumberTable
? #13 = Utf8 ? ? ? ? ? ? ? LocalVariableTable
? #14 = Utf8 ? ? ? ? ? ? ? this
? #15 = Utf8 ? ? ? ? ? ? ? Lcom/jg/zhang/Person;
? #16 = Utf8 ? ? ? ? ? ? ? getAge
? #17 = Utf8 ? ? ? ? ? ? ? ()I
? #18 = Fieldref ? ? ? ? ? #1.#19 ? ? ? ? // ?com/jg/zhang/Person.age:I
? #19 = NameAndType ? ? ? ?#5:#6 ? ? ? ? ?// ?age:I
? #20 = Utf8 ? ? ? ? ? ? ? SourceFile
? #21 = Utf8 ? ? ? ? ? ? ? Person.java
常量池一共有21項, 我們可以看到, 一共有兩個CONSTANT_NameAndType_info 數據項, 分別是第#11項和第#19項, 其中第#11項的CONSTANT_NameAndType_info又引用了常量池中的第#7項和第#8項, 被引用的這兩項都是CONSTANT_Utf8_info , 它們中存儲的字符串常量值分別是 <init> 和 ()V。 其實他們加起來表示的就是父類Object的構造方法。 那么這里為什么會是父類Object的構造方法而不是本類的構造方法呢? 這是因為類中定義的方法如果不被引用(也就是說在當前類中不被調用), 那么常量池中是不會有相應的?CONSTANT_NameAndType_info 與之對應的, 只有引用了一個方法, 才有相應的CONSTANT_NameAndType_info 與之對應。 這也是為什么說CONSTANT_NameAndType_info 是方法的符號引用的一部分的原因。 (這里提到一個新的概念, 叫做方法的符號引用, 這個概念會在后面的博客中進行講解)?可以看到, 在源碼存在兩個方法, 分別是編譯器默認添加的構造方法和我們自己定義的getAge方法, 因為并沒有在源碼中顯示的調用這兩個方法,所以在常量池中并不存在和這兩個方法相對應的CONSTANT_NameAndType_info 。??之所以會存在父類Object的構造方法對應的CONSTANT_NameAndType_info , 是因為子類構造方法中會默認調用父類的無參數構造方法。 我們將常量中的其他信息去掉, 可以看得更直觀:
下面講解常量池第#19項的CONSTANT_NameAndType_info , 它引用了常量池第#5項和第#6項, 這兩項也是CONSTANT_Utf8_info 項, 其中存儲的字符串分別是age和I, 其中age是源碼中字段age的字段名, I是age字段的描述符。 所以這個CONSTANT_NameAndType_info 就表示對本類中的字段age的引用。 除去常量池中的其他信息, 可以看得更直觀:
和方法相同, 只定義一個字段而不引用它(在源碼中表現為不訪問這個變量), 那么在常量池中也不會存在和該字段相對應的CONSTANT_NameAndType_info 項。這也是為什么說CONSTANT_NameAndType_info作為字段符號引用的一部分的原因。 (這里提到一個新的概念, 叫做字段的符號引用, 這個概念會在后面的博客中進行講解)?在本例中之所以會出現這個CONSTANT_NameAndType_info , 是因為在源碼的getAge方法中訪問了這個字段:
?? ?int getAge(){
?? ??? ?return age;
?? ?}
下面給出這兩個CONSTANT_NameAndType_info真實的內存布局圖:
和Object構造方法相關的CONSTANT_NameAndType_info的示意圖:
和age字段相關的CONSTANT_NameAndType_info示意圖:
這兩張圖能夠很好的反映出CONSTANT_NameAndType_info和CONSTANT_Utf8_info 這兩種常量池數據項的數據存儲方式, 也能夠真實的反應CONSTANT_NameAndType_info和CONSTANT_Utf8_info 的引用關系。?
總結
本篇博客就到此為止, 在本文中我們主要介紹了常量池中的兩種數據項:?CONSTANT_NameAndType_info 和?CONSTANT_Utf8_info ?。 其中CONSTANT_Utf8_info存儲的是源文件中的各種字符串, 而CONSTANT_NameAndType_info表述的是源文件中對一個字段或方法的符號引用的一部分(即 方法名加方法描述符, 或者是 字段名加字段描述符)。在下一篇博客中, 繼續講解常量池中的其他類型的數據項 。
---------------------?
作者:張紀剛?
來源:CSDN?
原文:https://blog.csdn.net/zhangjg_blog/article/details/21557357?
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!
總結
以上是生活随笔為你收集整理的深入理解Java Class文件格式的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java class类文件结构
- 下一篇: Java Class 文件格式及其简单