JVM_字節(jié)碼文件(ClassFile)詳解

我們知道javac 命令可以將 .java 文件編譯成 .class 文件乱豆,而這個Class 文件 中包含了Java虛擬機指令集奖恰、符號表以及若干其他輔助信息;最終將在Java虛擬機運行宛裕。

  • Java虛擬機最終運行的是 Class 文件瑟啃,它其實不管你是不是由 .java 文件生成的,如果有別的程序語言能將自己程序邏輯翻譯成一個Class 文件揩尸,那么它也可以運行在 Java虛擬機 上的岩榆。
  • 也就是說Java虛擬機其實并不和 java 語言強綁定勇边,它只與Class 這個二進制文件強關(guān)聯(lián)识颊,Class 文件包含程序運行的所有信息。

本文是以 JVM8 為例的奕坟。

一. class 文件格式

每一個 Class文件都有如下的 ClassFile 文件結(jié)構(gòu):

ClassFile {
   u4 magic;
   u2 minor_version;
   u2 major_version;
   u2 constant_pool_count;
   cp_info constant_pool[constant_pool_count-1];
   u2 access_flags;
   u2 this_class;
   u2 super_class;
   u2 interfaces_count;
   u2 interfaces[interfaces_count];
   u2 fields_count;
   field_info fields[fields_count];
   u2 methods_count;
   method_info methods[methods_count];
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}
  • 每一個Class文件都對應(yīng)著唯一一個類或接口的定義信息曙砂,但是反過來說退个,類或接口并不一定都得定義在文件里(比如類或接口也可以通過類加載器直接生成)砌滞。
  • Class文件 并不一定是一個文件歇父,它指的是是一組以8個字節(jié)為基礎(chǔ)單位的二進制流,各個數(shù)據(jù)項目嚴(yán)格按照順序緊湊地排列在文件之中沙合,中間沒有添加任何分隔符奠伪,這使得整個Class文件中存儲的內(nèi)容幾乎全部是程序運行的必要數(shù)據(jù),沒有空隙存在首懈。多字節(jié)數(shù)據(jù)項總是按照 big-endian(大端在前)的順序存儲绊率。
  • Class文件 有兩種數(shù)據(jù)格式:
    • 一種是固定字節(jié)數(shù)的數(shù)據(jù)結(jié)構(gòu),稱之為(Items)究履,其實就是無符號數(shù)。例如上圖 u2 表示兩個字節(jié)無符號數(shù)(范圍 0 -> 65536)炊甲。
    • 一種是可變長度字節(jié)數(shù)的數(shù)據(jù)結(jié)構(gòu)菱父,稱之為(Tables),用來記錄復(fù)合數(shù)據(jù)結(jié)構(gòu),都是以 _info 結(jié)尾鳖轰。例如上圖中的 cp_info,field_info,method_infoattribute_info

先簡單介紹一下 ClassFile 文件結(jié)構(gòu)各部分含義:

  • magic : Class文件 的魔數(shù),代表這個二進制文件是 Class文件,它的固定是 0xCAFEBABE,不會改變智亮。
  • minor_versionmajor_version : 表示這個 Class文件 的副版本號和主版本號状原。
  • constant_pool_count : 表示常量池的數(shù)量。

    它的類型是 u2,也就是說一個 Class文件最多有65536 -1 個常量磨澡。

  • constant_pool[constant_pool_count-1] : 表示常量池厦酬,記錄這個Class文件所有常量信息国夜。
    • 常量池包含Class文件及其子結(jié)構(gòu)中所有的常量信息窄驹。
    • 常量池中的每一項都有一個特征,第一個字節(jié)表示這個常量的類型,在 JVM8 中一共有 14 中類型。
    • 常量池的索引范圍是 1 -> constant_pool_count -1花吟,也就是constant_pool_count -1 個常量谬墙,索引 0 表示不是任何常量。
  • access_flags : 表示這個 Class文件 訪問標(biāo)志。
    • 我們知道在 java 語言中,類,方法,成員字段都可以設(shè)置不同的修飾符,這里的訪問標(biāo)志就表示修飾符相關(guān)信息镐躲。
    • 它的類型是 u2,有16 位二進制數(shù)端礼,也就是說它最多可以表示 16 個不同的訪問標(biāo)志凡桥。
  • this_class : 表示當(dāng)前類或者接口的類常量索引啊掏。

    它的類型是 u2娜睛,就是表示常量池中某項的有效索引值,而且它指向的那個常量必須是 CONSTANT_Class_info 類型。

  • super_class : 表示當(dāng)前類或者接口的父類常量索引。
    • 它的類型也是 u2,也表示常量池中某項的有效索引值。但是根據(jù)當(dāng)前Class文件是類或者接口,分為兩種情況:
      • 如果當(dāng)前是類的話阿迈,當(dāng) super_class值是 0待逞,那就表示這個類是 Object 類牺荠;其他的情況,super_class值都是常量池中一個CONSTANT_Class_info 類型常量的索引值驰凛。
      • 如果當(dāng)前是接口的話胚宦,那么super_class值只會是常量池中一個 CONSTANT_Class_info 類型常量的索引值,而且這個CONSTANT_Class_info 類型表示 Object 類轴捎,因為接口的父類就只能是 Object
  • interfaces_count : 表示當(dāng)前類實現(xiàn)的接口數(shù)量汇竭,或者當(dāng)前接口繼承的接口數(shù)量皂甘。

    它的類型是 u2 ,表示最多有 65536 個接口;如果是 0 表示沒有任何接口。

  • interfaces[interfaces_count] : 表示接口對應(yīng)的常量池索引值表,長度就是 interfaces_count
  • fields_count : 表示當(dāng)前類或者接口的字段數(shù)量,包括靜態(tài)字段和成員字段。
  • fields[fields_count] : 表示當(dāng)前類或者接口的字段詳細(xì)信息的表,長度是 fields_count

    它的類型是 field_info,記錄著這個字段的所有信息乾戏。

  • methods_count : 表示當(dāng)前類或者接口的方法數(shù)量念搬,包括類方法和實例方法悯搔。
  • methods[methods_count] : 表示當(dāng)前類或者接口的方法詳細(xì)信息的表灌曙,長度是 fields_count蚣驼。

    它的類型是 method_info,記錄著這個方法的所有信息相艇。

  • attributes_count : 表示當(dāng)前類或者接口的屬性數(shù)量颖杏。
  • attributes[attributes_count] : 表示當(dāng)前類或者接口的屬性詳細(xì)信息的表。

    它的類型是 attribute_info坛芽,屬性的種類非常多,不僅在類中有茵宪,在字段詳情field_info 和方法詳情method_info 中也有相關(guān)屬性表黍聂。這個我們后面會慢慢說明。

二. 各類名稱在 Class 文件 中的表示形式

  • 全限定名

    類和接口的名稱都是全限定形式,被稱為二進制名稱瑟枫,不過和java 語言中不同室埋,它使用(/) 而不是 (.) 作為分隔符的,例如 java.lang.Object 就變成了 java/lang/Object

  • 非限定名
    • 方法名溯祸,字段名,局部變量名和形式參數(shù)名都是非限定形式。
    • 非限定名至少有一個 Unicode 字符,但是不能是 ASCII 字符 (. ; [ /) 這四個字符。
    • 除了實例初始化方法 <init> 和類初始化方法 <clinit> 以外捐腿,其他非限定名也不能出現(xiàn) (< >) 這兩個字符丹拯。

三. 描述符

描述符是表示字段或方法類型的字符串。

3.1 字段描述符

字段描述符表示類必孤、實例或局部變量的類型蔚龙。

字段描述符語法:

1. FieldDescriptor -> FieldType
2. FieldType -> BaseType | ObjectType | ArrayType
3. BaseType -> B | C | D | F | I | J | S | Z
4. ObjectType -> LClassName;
5. ArrayType -> [ComponentType
6. ComponentType -> FieldType

這個語法中眠寿,B C D F I J S Z L ; [ 是終結(jié)符,其他的都是非終結(jié)符焦蘑。這個方面不清楚的請看我的 編譯原理-文法定義 相關(guān)文章說明盯拱。

從上面文法可以看出,字段描述符中一共有三個類型:

  • BaseType 表示基礎(chǔ)類型。
    • 一共分為八種狡逢,分別是 B C D F I J S Z宁舰。
    • 其中 Z 表示 boolean 類型,因為 B 已經(jīng)表示byte 類型了奢浑;J 表示 long 類型蛮艰,因為 L 這個字符被引用類型用了。
  • ObjectType 表示引用類型雀彼。

    它的格式是 LClassName;壤蚜,即以L開頭,;結(jié)尾徊哑,中間是類的全限定名袜刷。例如Object 類就是 Ljava/lang/Object;

  • ArrayType 表示數(shù)組類型莺丑。

    它的格式是 [ComponentType著蟹,即以[開頭,ComponentType 表示三個類型中任意類型窒盐。例如 int[] 就是 [I; int[][] 就是 [[I; String[] 就是 [Ljava/lang/String;草则。

3.2 方法描述符

方法描述符包含0個或者多個參數(shù)描述符以及一個返回值描述符。

方法描述符語法:

1. MethodDescriptor -> ({ParameterDescriptor}) ReturnDescriptor
2. ParameterDescriptor-> FieldType
3. ReturnDescriptor-> FieldType | VoidDescriptor
4. VoidDescriptor-> V
  • 這個語法中 ( ) V 這個三個字符是終結(jié)符蟹漓。
  • {ParameterDescriptor} 中的 { } 表示這個非終結(jié)符 ParameterDescriptor 能夠出現(xiàn)0次或者多次。
  • V 表示沒有任何返回值源内,就是 void 關(guān)鍵字葡粒。
  • 例如方法 String test(int i, long l, Integer i1, Long l1, Object[] objs) {..} 對應(yīng)的描述符 (IJLjava/lang/Integer;Ljava/lang/Long;[Ljava/lang/Object;)Ljava/lang/String;

看了描述符,可能大家有點疑惑膜钓,泛型信息怎么表示啊?

描述符的確不能記錄泛型相關(guān)信息嗽交,泛型信息記錄在 Signatures 屬性中。

四. 常量池

常量池的通用格式如下:

cp_info {
   u1 tag;
   u1 info[];
}

一個字節(jié)無符號數(shù) tag 表示常量類型颂斜;info 表示不同常量類型的數(shù)據(jù)夫壁。

目前 JVM8 中一共用14 種常量類型,分別如下:

Constant Type Value 描述
CONSTANT_Utf8 1 表示 Utf8 編碼的字符串
CONSTANT_Integer 3 表示整形字面量
CONSTANT_Float 4 表示單精度浮點型字面量
CONSTANT_Long 5 表示長整形字面量
CONSTANT_Double 6 表示雙精度浮點型字面量
CONSTANT_Class 7 表示類或者接口的符號引用
CONSTANT_String 8 表示字符串類型字面量
CONSTANT_Fieldref 9 表示字段的符號引用
CONSTANT_Methodref 10 表示類中方法的符號引用
CONSTANT_InterfaceMethodref 11 表示接口中方法的符號引用
CONSTANT_NameAndType 12 表示字段或者方法的名稱和描述符
CONSTANT_MethodHandle 15 表示方法的句柄
CONSTANT_MethodType 16 表示方法的類型
CONSTANT_InvokeDynamic 18 表示動態(tài)計算常量
CONSTANT_Utf8_info {
   u1 tag;
   u2 length;
   u1 bytes[length];
}
CONSTANT_Integer_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Float_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Long_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
CONSTANT_Double_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
CONSTANT_Class_info {
   u1 tag;
   u2 name_index;
}
CONSTANT_String_info {
   u1 tag;
   u2 string_index;
}
CONSTANT_Fieldref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_Methodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_NameAndType_info {
   u1 tag;
   u2 name_index;
   u2 descriptor_index;
}
CONSTANT_MethodHandle_info {
   u1 tag;
   u1 reference_kind;
   u2 reference_index;
}
CONSTANT_MethodType_info {
   u1 tag;
   u2 descriptor_index;
}
CONSTANT_InvokeDynamic_info {
   u1 tag;
   u2 bootstrap_method_attr_index;
   u2 name_and_type_index;
}

4.1 CONSTANT_Utf8_info

CONSTANT_Utf8_info {
   u1 tag;
   u2 length;
   u1 bytes[length];
}
  • tag : 表示常量類型沃疮,值就是1(CONSTANT_Utf8)
  • length : 表示 Utf8 字符串的總字節(jié)數(shù)盒让。

    它的類型是 u2,也就是 Utf8 字符串最多只能有 65536 個字節(jié)司蔬。

  • bytes[length] : 表示Utf8 字符串的字節(jié)數(shù)組數(shù)據(jù)邑茄。

4.2 CONSTANT_Integer_infoCONSTANT_Float_info

CONSTANT_Integer_info {
   u1 tag;
   u4 bytes;
}
CONSTANT_Float_info {
   u1 tag;
   u4 bytes;
}
  • tag : 表示常量類型。
    • CONSTANT_Integer_info 的值就是3(CONSTANT_Integer)俊啼。
    • CONSTANT_Float_info的值就是 4(CONSTANT_Float)肺缕。
  • bytes : 是一個四個字節(jié)數(shù),用來儲存 intfloat 類型字面量。

4.3 CONSTANT_Long_infoCONSTANT_Double_info

CONSTANT_Long_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
CONSTANT_Double_info {
   u1 tag;
   u4 high_bytes;
   u4 low_bytes;
}
  • tag : 表示常量類型同木。
    • CONSTANT_Long_info 的值就是5(CONSTANT_Long)浮梢。
    • CONSTANT_Double_info的值就是 6(CONSTANT_Double)。
  • high_byteslow_bytes : 組成一個8個字節(jié)數(shù)彤路,來儲存 longdouble 類型字面量黔寇。
  • 特別注意,這兩種類型會在常量表中占據(jù)兩個索引斩萌,也就是如果索引 3 處的CONSTANT_Long_info類型缝裤,那么下一個有效索引是 5

4.4 CONSTANT_Class_info

CONSTANT_Class_info {
   u1 tag;
   u2 name_index;
}
  • tag : 表示常量類型颊郎,CONSTANT_Class_info 的值就是7(CONSTANT_Class)憋飞。
  • name_index : 表示類符號引用的索引。

    值必須是當(dāng)前常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引姆吭。

4.5 CONSTANT_String_info

CONSTANT_String_info {
   u1 tag;
   u2 string_index;
}
  • tag : 表示常量類型榛做,CONSTANT_String_info 的值就是8(CONSTANT_String)。
  • string_index : 表示字符串值的索引内狸。

    值必須是當(dāng)前常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引检眯。

4.6 CONSTANT_Fieldref_info , CONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info

CONSTANT_Fieldref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_Methodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
CONSTANT_InterfaceMethodref_info {
   u1 tag;
   u2 class_index;
   u2 name_and_type_index;
}
  • tag : 表示常量類型。
    • CONSTANT_Fieldref_info 的值就是9(CONSTANT_Fieldref);
    • CONSTANT_Methodref_info 的值就是10(CONSTANT_Methodref);
    • CONSTANT_InterfaceMethodref_info 的值就是11(CONSTANT_InterfaceMethodref)昆淡。
  • class_index : 表示這個字段或者方法所屬類符號引用的索引锰瘸。
    • 值必須是當(dāng)前常量池中一個 CONSTANT_Class_info 類型常量的有效索引。
  • name_and_type_index : 表示這個字段或者方法的名稱和描述符的索引昂灵。
    • 值必須是當(dāng)前常量池中一個 CONSTANT_NameAndType_info 類型常量的有效索引避凝。
    • 知道字段的名稱和描述符,就知道了字段的名字和類型眨补。
    • 知道方法的名稱和描述符管削,就知道了方法的名字和方法參數(shù)類型以及方法返回值類型。

我們知道要使用一個字段或者調(diào)用一個方法撑螺,就必須知道字段或者方法所屬類符號引用含思,和字段的名字和類型,方法的名字和方法參數(shù)類型以及方法返回值類型甘晤。
但是我們知道類是能繼承的含潘,那么子類調(diào)用父類的方法或者字段,這里的所屬類符號引用安皱,到底是子類本身還是父類的呢调鬓?

請大家思考一下,后面的例子中酌伊,我們將會講解腾窝。

4.7 CONSTANT_NameAndType_info

CONSTANT_NameAndType_info {
   u1 tag;
   u2 name_index;
   u2 descriptor_index;
}
  • tag : 表示常量類型缀踪,CONSTANT_NameAndType_info 的值就是12(CONSTANT_NameAndType)。
  • name_index : 表示字段或者方法名稱虹脯。

    值必須是當(dāng)前常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引驴娃。

  • descriptor_index : 表示字段或者方法描述符。

    值必須是當(dāng)前常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引循集。

4.8 CONSTANT_MethodHandle_info

CONSTANT_MethodHandle_info {
   u1 tag;
   u1 reference_kind;
   u2 reference_index;
}
  • tag : 表示常量類型唇敞,CONSTANT_MethodHandle_info 的值就是15(CONSTANT_MethodHandle)。
  • reference_kind :表示方法句柄的類型咒彤。

    值范圍是 0 ~ 9疆柔,不同的值決定了句柄字節(jié)碼的行為。

  • reference_index :就是常量池中的一個有效索引镶柱,因為reference_kind 值的不同旷档,分為幾種情況:
    • 如果reference_kind 的值是 1 (REF_getField), 2
      (REF_getStatic), 3 (REF_putField), 或者 4 (REF_putStatic);都是和字段相關(guān)歇拆,那么reference_index 必須是常量池中CONSTANT_Fieldref_info 類型常量的有效索引鞋屈。
    • 如果reference_kind 的值是5 (REF_invokeVirtual) 或者 8
      (REF_newInvokeSpecial),那么reference_index 必須是常量池中CONSTANT_Methodref_info 類型常量的有效索引故觅,表示一個類中的實例方法或者構(gòu)造函數(shù)厂庇。
    • 如果reference_kind 的值是6 (REF_invokeStatic)
      或者 7 (REF_invokeSpecial),且class 版本小于 52 (即 JVM8)输吏,那么reference_index必須是CONSTANT_Methodref_info 類型索引权旷;如果 版本大于或者等于 52,那么reference_index必須是CONSTANT_Methodref_info 類型索引或者 CONSTANT_InterfaceMethodref_info 類型索引评也,因為JVM8 以上接口有默認(rèn)方法炼杖。
    • 如果reference_kind 的值是9 (REF_invokeInterface),那么reference_index必須是CONSTANT_InterfaceMethodref_info 類型索引盗迟。
    • 如果reference_kind 的值是5 (REF_invokeVirtual), 6
      (REF_invokeStatic), 7 (REF_invokeSpecial), or 9(REF_invokeInterface),那么reference_index必須是CONSTANT_Methodref_info 類型索引或者 CONSTANT_InterfaceMethodref_info 類型索引熙含,但是不能是實例初始化方法<init> 和 類初始化方法<clinit>罚缕。
    • 如果reference_kind 的值是 8 (REF_newInvokeSpecial),那么reference_index必須是CONSTANT_Methodref_info 類型索引怎静,而且它就是實例初始化方法<init>邮弹。

4.9 CONSTANT_MethodType_info

CONSTANT_MethodType_info {
   u1 tag;
   u2 descriptor_index;
}
  • tag : 表示常量類型,CONSTANT_MethodType_info 的值就是16(CONSTANT_MethodType)蚓聘。
  • descriptor_index : 表示方法的描述符腌乡。

    值必須是當(dāng)前常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引。

4.10 CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info {
   u1 tag;
   u2 bootstrap_method_attr_index;
   u2 name_and_type_index;
}
  • tag : 表示常量類型夜牡,CONSTANT_InvokeDynamic_info 的值就是18(CONSTANT_InvokeDynamic)与纽。
  • bootstrap_method_attr_index : 表示引導(dǎo)方法 bootstrap_methods 中的有效索引。

    引導(dǎo)方法 bootstrap_methods 記錄在 Class 文件BootstrapMethods 屬性中。

  • name_and_type_index : 表示方法的名稱和描述符的索引急迂。

    值必須是當(dāng)前常量池中一個 CONSTANT_NameAndType_info 類型常量的有效索引影所。

五. 訪問標(biāo)志(access_flags)

我們知道類,方法,字段都有不同的訪問標(biāo)志,在Class 文件 中使用一個u2 類型數(shù)據(jù)項來存儲僚碎,也就是最多可以有 16 個不同標(biāo)志位猴娩。
在類,方法,字段中有相同的標(biāo)志,也有不同的標(biāo)志勺阐,總體規(guī)劃卷中,我們可以借助 Modifier 類的源碼來了解:

    public static final int PUBLIC           = 0x00000001;
    public static final int PRIVATE          = 0x00000002;
    public static final int PROTECTED        = 0x00000004;
    public static final int STATIC           = 0x00000008;
    public static final int FINAL            = 0x00000010;
    public static final int SYNCHRONIZED     = 0x00000020;
    public static final int VOLATILE         = 0x00000040;
    public static final int TRANSIENT        = 0x00000080;
    public static final int NATIVE           = 0x00000100;
    public static final int INTERFACE        = 0x00000200;
    public static final int ABSTRACT         = 0x00000400;
    public static final int STRICT           = 0x00000800;
    static final int BRIDGE      = 0x00000040;
    static final int VARARGS     = 0x00000080;
    static final int SYNTHETIC   = 0x00001000;
    static final int ANNOTATION  = 0x00002000;
    static final int ENUM        = 0x00004000;
    static final int MANDATED    = 0x00008000;
  • 因為 access_flags 是兩個字節(jié)數(shù),這里使用 int 類型渊抽,也就說前面4 個永遠是 0(0x0000----)蟆豫。
  • 這里 0x000000400x00000080 重復(fù)使用了,但是沒關(guān)系腰吟,因為表示不同的訪問標(biāo)志无埃。

5.1 類的訪問標(biāo)志

Modifier 類中,類的訪問標(biāo)志:

    private static final int CLASS_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.FINAL   |
        Modifier.STRICT;

    private static final int INTERFACE_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.STRICT;

我們知道在 java 中類可以用的修飾符有: public,protected,private,abstract,static,final,strictfp毛雇。

  • 其中 protected,privatestatic 都是只能用在內(nèi)部類里面嫉称。
  • strictfp 這個關(guān)鍵字表示這個類精確進行浮點運算。不過這么多年也沒有看誰用過灵疮。
  • final 關(guān)鍵字不能用在接口上织阅。

但是我們再看 Class 文件 中類的訪問標(biāo)志:

標(biāo)志名 描述
ACC_PUBLIC 0x0001 聲明為 public,可以從包外訪問
ACC_FINAL 0x0010 聲明為 final震捣,不允許被繼承
ACC_SUPER 0x0020 為了兼容之前編譯器編譯代碼而設(shè)置的荔棉,目前編譯器編譯的代碼,這個標(biāo)志位都是1蒿赢。
ACC_INTERFACE 0x0200 表示是類還是接口
ACC_ABSTRACT 0x0400 聲明為 abstract润樱,不能被實例化
ACC_SYNTHETIC 0x1000 聲明為 synthetic,表示這個 Class 文件 不在源代碼中
ACC_ANNOTATION 0x2000 表示為注解類型
ACC_ENUM 0x4000 表示為枚舉類型

仔細(xì)看羡棵,你會發(fā)現(xiàn)有些不同點:

  • 內(nèi)部類中的三個修飾符信息壹若,沒有出現(xiàn)在Class 文件 中類的訪問標(biāo)志里。

    的確是這樣皂冰,三個修飾符只會控制 javac 編譯行為的不同店展,編譯完的內(nèi)部類 class 文件中,是沒有這三個修飾符信息秃流。例如赂蕴,非 static 修飾的內(nèi)部類, javac 編譯的時候舶胀,會在 class 文件中概说,添加一個字段碧注,類型就是外部類。

  • ACC_INTERFACE,ACC_ANNOTATIONACC_ENUM 分別表示 java 目前的三種類型 interface,@interfaceenum席怪。

5.2 字段的訪問標(biāo)志

Modifier 類中应闯,字段的訪問標(biāo)志:

    private static final int FIELD_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.STATIC         | Modifier.FINAL        | Modifier.TRANSIENT |
        Modifier.VOLATILE;

我們知道在 java 中字段可以用的修飾符有: public,protected,private,static,final,transientvolatile

  • 其中 transient 表示這個字段是瞬時挂捻,進行java 序列化的時候碉纺,不會序列化被transient修飾的字段。
  • volatile 表示這個字段是可見的刻撒。volatile關(guān)鍵字的詳細(xì)介紹請看 Java多線程詳細(xì)介紹骨田。

但是我們再看 Class 文件 中字段的訪問標(biāo)志:

標(biāo)志名 描述
ACC_PUBLIC 0x0001 聲明為 public,可以從包外訪問
ACC_PRIVATE 0x0002 聲明為 private声怔,只能在定義該字段的類中訪問
ACC_PROTECTED 0x0004 聲明為 protected态贤,子類可以訪問
ACC_STATIC 0x0008 聲明為 static
ACC_FINAL 0x0010 聲明為 final,表示對象構(gòu)造完成后醋火,不能直接修改該字段了
ACC_VOLATILE 0x0040 聲明為 volatile
ACC_TRANSIENT 0x0080 聲明為 transient
ACC_SYNTHETIC 0x1000 表示該字段不是在源碼中悠汽,由編譯器生成
ACC_ENUM 0x4000 表示該字段是枚舉(enum)類型

Class 文件 中字段的訪問標(biāo)志和java 中字段的修飾符差不多,只是多了 ACC_SYNTHETICACC_ENUM 兩個標(biāo)志芥驳。

5.3 方法的訪問標(biāo)志

Modifier 類中柿冲,方法的訪問標(biāo)志:

    private static final int CONSTRUCTOR_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE;

    private static final int METHOD_MODIFIERS =
        Modifier.PUBLIC         | Modifier.PROTECTED    | Modifier.PRIVATE |
        Modifier.ABSTRACT       | Modifier.STATIC       | Modifier.FINAL   |
        Modifier.SYNCHRONIZED   | Modifier.NATIVE       | Modifier.STRICT;

我們知道在 java 中方法可以用的修飾符有:
public,protected,private,abstract,static,final,synchronized, synchronizedstrictfp

但是我們再看 Class 文件 中方法的訪問標(biāo)志:

標(biāo)志名 描述
ACC_PUBLIC 0x0001 聲明為 public兆旬,可以從包外訪問
ACC_PRIVATE 0x0002 聲明為 private假抄,只能在定義該字段的類中訪問
ACC_PROTECTED 0x0004 聲明為 protected,子類可以訪問
ACC_STATIC 0x0008 聲明為 static
ACC_FINAL 0x0010 聲明為 final, 表示方法不能被覆蓋
ACC_SYNCHRONIZED 0x0020 聲明為 synchronized, 表示對方法的調(diào)用丽猬,會包裝在同步鎖里
ACC_BRIDGE 0x0040 聲明為 bridge宿饱,由編譯器產(chǎn)生
ACC_VARARGS 0x0080 表示方法帶有變長參數(shù)
ACC_NATIVE 0x0100 聲明為 native, 不是由 java 語言實現(xiàn)
ACC_ABSTRACT 0x0400 聲明為 abstract ,該方法沒有實現(xiàn)方法
ACC_STRICT 0x0800 聲明為 strictfp脚祟,使用精確浮點模式
ACC_SYNTHETIC 0x1000 該方法由編譯器合成的谬以,不是由源碼編譯出來的

六. 字段和方法

6.1 字段

字段詳情 field_info 的格式如下:

field_info {
   u2 access_flags;
   u2 name_index;
   u2 descriptor_index;
   u2 attributes_count;
   attribute_info attributes[attributes_count];
}
  • access_flags : 表示字段訪問標(biāo)志,在上面已經(jīng)介紹了由桌。
  • name_index : 表示字段的名稱蛉签。

    值必須是常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引。

  • descriptor_index : 表示字段的描述符。

    值必須是常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引。

  • attributes_count : 表示當(dāng)前字段的屬性數(shù)量璧微。
  • attributes[attributes_count] : 表示當(dāng)前字段的屬性詳細(xì)信息的表嗦玖。

6.2 方法

方法詳情 method_info 的格式如下:

method_info {
 u2 access_flags;
 u2 name_index;
 u2 descriptor_index;
 u2 attributes_count;
 attribute_info attributes[attributes_count];
}
  • access_flags : 表示方法訪問標(biāo)志,在上面已經(jīng)介紹了妈经。
  • name_index : 表示方法的名稱淮野。

    值必須是常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引捧书。

  • descriptor_index : 表示方法的描述符。

    值必須是常量池中一個 CONSTANT_Utf8_info 類型常量的有效索引骤星。

  • attributes_count : 表示當(dāng)前方法的屬性數(shù)量经瓷。
  • attributes[attributes_count] : 表示當(dāng)前方法的屬性詳細(xì)信息的表。

關(guān)于 Class 文件 中屬性相關(guān)信息洞难,我們再后面章節(jié)介紹舆吮。

七. 例子

我們可以通過 javap 的命令來閱讀 Class 文件 中相關(guān)信息。

7.1 最簡單的例子

package com.zhang.jvm.reflect.example;

public class T {
}

這個是最簡單的一個類队贱,沒有任何字段和方法色冀,只繼承Object 類,我們來看看它編譯后的字節(jié)碼信息柱嫌,通過javap -p -v T.class 的命令:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 288 bytes
  MD5 checksum 2771c8258a6734d72812fca914966e07
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#13         // java/lang/Object."<init>":()V
   #2 = Class              #14            // com/zhang/jvm/reflect/example/T
   #3 = Class              #15            // java/lang/Object
   #4 = Utf8               <init>
   #5 = Utf8               ()V
   #6 = Utf8               Code
   #7 = Utf8               LineNumberTable
   #8 = Utf8               LocalVariableTable
   #9 = Utf8               this
  #10 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #11 = Utf8               SourceFile
  #12 = Utf8               T.java
  #13 = NameAndType        #4:#5          // "<init>":()V
  #14 = Utf8               com/zhang/jvm/reflect/example/T
  #15 = Utf8               java/lang/Object
{
  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
  • 前面的minor version: 0 之前的都是.class 文件本身的信息锋恬,這里魔數(shù)magic 信息被省略了。
  • 然后依次就是常量池信息 Constant pool编丘,字段相關(guān)信息(因為T.class 沒有字段与学,所以這里沒有),方法相關(guān)信息({} 里面的嘉抓,就是T.class默認(rèn)的構(gòu)造器方法<init>)索守,最后類屬性相關(guān)信息(就是這里的SourceFile: "T.java")。
  • this_class,super_class,interfaces 相關(guān)信息就在 public class com.zhang.jvm.reflect.example.T 中掌眠,Object 父類就隱藏蕾盯。

我們重點關(guān)注常量池相關(guān)信息,會發(fā)現(xiàn)雖然T.class 很干凈蓝丙,但是也有15 個常量级遭,來我們依次分析:

  • #1 : 這是一個 CONSTANT_Methodref_info 類型,值是java/lang/Object."<init>":()V渺尘。

    很明顯它就是 Object 類構(gòu)造器方法的符號引用挫鸽,通過這個引用來調(diào)用Object 類構(gòu)造器方法。

  • #2 : 這是一個 CONSTANT_Class_info 類型鸥跟,值是 com/zhang/jvm/reflect/example/T丢郊。

    就是本類的全限定名稱。

  • #3 : 這是一個 CONSTANT_Class_info 類型医咨,值是 java/lang/Object枫匾。
  • #4 : 這是一個 CONSTANT_Utf8_info 類型,值是 <init>拟淮。

    默認(rèn)構(gòu)造器方法的名稱干茉。

  • #5 : 這是一個 CONSTANT_Utf8_info 類型,值是 ()V很泊。

    默認(rèn)構(gòu)造器方法的描述符角虫。

  • #6 : 這是一個 CONSTANT_Utf8_info 類型沾谓,值是 Code,是一個屬性名稱戳鹅。
  • #7 : 這是一個 CONSTANT_Utf8_info 類型均驶,值是 LineNumberTable,是一個屬性名稱枫虏。
  • #8 : 這是一個 CONSTANT_Utf8_info 類型妇穴,值是 LocalVariableTable,是一個屬性名稱模软。
  • #9 : 這是一個 CONSTANT_Utf8_info 類型伟骨,值是 this
  • #10 : 這是一個 CONSTANT_Utf8_info 類型燃异,值是 Lcom/zhang/jvm/reflect/example/T;携狭。

    它是T 類型的描述符。

  • #11 : 這是一個 CONSTANT_Utf8_info 類型回俐,值是 SourceFile逛腿,是一個屬性名稱。
  • #12 : 這是一個 CONSTANT_Utf8_info 類型仅颇,值是 T.java单默。
  • #13 : 這是一個 CONSTANT_Utf8_info 類型,值是 "<init>":()V忘瓦。

    它是構(gòu)造器方法的描述符搁廓。

  • #14 : 這是一個 CONSTANT_Utf8_info 類型,值是 com/zhang/jvm/reflect/example/T耕皮。

    就是本類的全限定名稱境蜕。

  • #14 : 這是一個 CONSTANT_Utf8_info 類型,值是 java/lang/Object凌停。

7.2 有字段和方法的例子

package com.zhang.jvm.reflect.example;
public class T {
    private String name;
    public void test() {}
}

與之前的例子相比較粱年,多了一個字段和方法,那么得到的字節(jié)碼信息如下:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 388 bytes
  MD5 checksum f97a6c1995036e9605c0121916c3d815
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #3.#16         // java/lang/Object."<init>":()V
   #2 = Class              #17            // com/zhang/jvm/reflect/example/T
   #3 = Class              #18            // java/lang/Object
   #4 = Utf8               name
   #5 = Utf8               Ljava/lang/String;
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #13 = Utf8               test
  #14 = Utf8               SourceFile
  #15 = Utf8               T.java
  #16 = NameAndType        #6:#7          // "<init>":()V
  #17 = Utf8               com/zhang/jvm/reflect/example/T
  #18 = Utf8               java/lang/Object
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"

與之前的相比較罚拟,發(fā)現(xiàn)有三個變化:

  • 常量池中多了三個常量台诗,都是 CONSTANT_Utf8_info 類型的,分別是 name,Ljava/lang/String;test赐俗。
  • 多了一個字段name相關(guān)信息拉队。
  • 多了一個方法test相關(guān)信息。

但是你會發(fā)現(xiàn)常量池中怎么沒有這個字段nameCONSTANT_Fieldref_info 類型的常量呢阻逮?
那是因為我們沒有使用這個字段氏仗。

package com.zhang.jvm.reflect.example;
public class T {
    private String name;
    public void test() {}
    public void test1() {
        name = "12";
        test(); 
    }
}

多寫了一個方法test1 來調(diào)用name 字段和 test 方法,那么得到的字節(jié)碼信息如下:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 499 bytes
  MD5 checksum 2ddae70db9ea9f755f9312eb2b2f2d07
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = String             #21            // 12
   #3 = Fieldref           #5.#22         // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
   #4 = Methodref          #5.#23         // com/zhang/jvm/reflect/example/T.test:()V
   #5 = Class              #24            // com/zhang/jvm/reflect/example/T
   #6 = Class              #25            // java/lang/Object
   #7 = Utf8               name
   #8 = Utf8               Ljava/lang/String;
   #9 = Utf8               <init>
  #10 = Utf8               ()V
  #11 = Utf8               Code
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #16 = Utf8               test
  #17 = Utf8               test1
  #18 = Utf8               SourceFile
  #19 = Utf8               T.java
  #20 = NameAndType        #9:#10         // "<init>":()V
  #21 = Utf8               12
  #22 = NameAndType        #7:#8          // name:Ljava/lang/String;
  #23 = NameAndType        #16:#10        // test:()V
  #24 = Utf8               com/zhang/jvm/reflect/example/T
  #25 = Utf8               java/lang/Object
{
  private java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PRIVATE

  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #2                  // String 12
         3: putfield      #3                  // Field name:Ljava/lang/String;
         6: aload_0
         7: invokevirtual #4                  // Method test:()V
        10: return
      LineNumberTable:
        line 10: 0
        line 11: 6
        line 12: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"

與之前的相比較,發(fā)現(xiàn)如下變化:

  • 常量池中多了七個常量皆尔,分別是字符串 "12" 對應(yīng)的兩個常量#2#21; 字段name 對應(yīng)的兩個常量#3#22;方法test 對應(yīng)的兩個常量#4#23; 以及方法test1 的名稱常量#17
  • 多了一個方法test1相關(guān)信息币励。

7.3 繼承的例子

package com.zhang.jvm.reflect.example;
public class TParent {
    public String name;
    public void say(){}
}

package com.zhang.jvm.reflect.example;
public class T extends TParent {
    public void test() {
        name = "T";
        say();
    }
}

這里定義一個父類TParent慷蠕,有一個公共字段name和方法say。子類T 繼承TParent類食呻,并有一個方法test 調(diào)用父類的字段和方法流炕,來看T 的字節(jié)碼信息:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 452 bytes
  MD5 checksum aeea52a9b2b166d588e1336dd0a4dcc1
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         // com/zhang/jvm/reflect/example/TParent."<init>":()V
   #2 = String             #18            // T
   #3 = Fieldref           #5.#19         // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
   #4 = Methodref          #5.#20         // com/zhang/jvm/reflect/example/T.say:()V
   #5 = Class              #21            // com/zhang/jvm/reflect/example/T
   #6 = Class              #22            // com/zhang/jvm/reflect/example/TParent
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #14 = Utf8               test
  #15 = Utf8               SourceFile
  #16 = Utf8               T.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Utf8               T
  #19 = NameAndType        #23:#24        // name:Ljava/lang/String;
  #20 = NameAndType        #25:#8         // say:()V
  #21 = Utf8               com/zhang/jvm/reflect/example/T
  #22 = Utf8               com/zhang/jvm/reflect/example/TParent
  #23 = Utf8               name
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               say
{
  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #2                  // String T
         3: putfield      #3                  // Field name:Ljava/lang/String;
         6: aload_0
         7: invokevirtual #4                  // Method say:()V
        10: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
  • 你會發(fā)現(xiàn)字段 name 在常量池中#3(Fieldref)和方法 say 在常量池中#4(Methodref),它們所屬類都是 T,而不是 TParent仅胞。
  • 但是你又發(fā)現(xiàn)在T的字節(jié)碼文件中每辟,就沒有 name 字段相關(guān)信息和 say 方法相關(guān)信息。
  • 這個是沒有關(guān)系的干旧,因為只要父類中相關(guān)字段和方法訪問權(quán)限是可以的渠欺,那么子類找不到也會到父類去找的。

但是如果你就想調(diào)用父類的該怎么辦呢椎眯?

這個很好辦挠将,我們知道java 中有 super 關(guān)鍵字。

package com.zhang.jvm.reflect.example;
public class T extends TParent {
    public void test() {
        super.name = "T";
        super.say();
    }
}

再來看T的字節(jié)碼信息:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 452 bytes
  MD5 checksum 7d0901b392b0bfd74300cb87482ba183
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#17         // com/zhang/jvm/reflect/example/TParent."<init>":()V
   #2 = String             #18            // T
   #3 = Fieldref           #6.#19         // com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
   #4 = Methodref          #6.#20         // com/zhang/jvm/reflect/example/TParent.say:()V
   #5 = Class              #21            // com/zhang/jvm/reflect/example/T
   #6 = Class              #22            // com/zhang/jvm/reflect/example/TParent
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #14 = Utf8               test
  #15 = Utf8               SourceFile
  #16 = Utf8               T.java
  #17 = NameAndType        #7:#8          // "<init>":()V
  #18 = Utf8               T
  #19 = NameAndType        #23:#24        // name:Ljava/lang/String;
  #20 = NameAndType        #25:#8         // say:()V
  #21 = Utf8               com/zhang/jvm/reflect/example/T
  #22 = Utf8               com/zhang/jvm/reflect/example/TParent
  #23 = Utf8               name
  #24 = Utf8               Ljava/lang/String;
  #25 = Utf8               say
{
  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
         4: return
      LineNumberTable:
        line 6: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: ldc           #2                  // String T
         3: putfield      #3                  // Field com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
         6: aload_0
         7: invokespecial #4                  // Method com/zhang/jvm/reflect/example/TParent.say:()V
        10: return
      LineNumberTable:
        line 8: 0
        line 9: 6
        line 10: 10
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"
  • 我們發(fā)現(xiàn)字段 name 在常量池中#3(Fieldref)和方法 say 在常量池中#4(Methodref)编整,它們所屬類都變成了 TParent舔稀。
  • 還有一點需要特別注意的,就是在 test 方法的指令集中掌测,調(diào)用 say 方法的指令内贮,從 invokevirtual 指令變成 invokespecial 指令。

子類可以直接使用父類允許訪問權(quán)限的字段和方法汞斧,即使子類中沒有相關(guān)字段和方法夜郁,這個是繼承的功效。
但是我們知道面向?qū)ο笳Z言断箫,除了繼承的特性拂酣,還有一個多態(tài)特性。

  • 多態(tài)就是根據(jù)運行時仲义,對象實際類型來調(diào)用對應(yīng)方法婶熬,而不是編譯時寫死的類型去調(diào)用,所謂的寫死類型就是常量池中 Methodref 類型常量中所屬的類型埃撵。
  • 我們知道方法是具有多態(tài)特性的赵颅,那么字段也有多態(tài)么。
package com.zhang.jvm.reflect.example;
public class TParent {
    public String name = "TParent";
    public void say(){
        System.out.println("I am TParent");
    }
}

package com.zhang.jvm.reflect.example;
public class T extends TParent {
    public String name = "T";
    public void say(){
        System.out.println("I am T");
    }
    public static void main(String[] agrs) {
        TParent tParent = new T();
        T t = (T) tParent;

        System.out.println("tParent.name: "+ tParent.name+"\nt.name: "+t.name);

        tParent.say();
        t.say();
    }
}

運行結(jié)果:

tParent.name: TParent
t.name: T
I am T
I am T

你會發(fā)現(xiàn)即使運行期是同一個對象暂刘,但是字段name 得到結(jié)果是不一樣的饺谬,即字段是不能多態(tài)的;但是方法的確是按照運行期實際對象類型調(diào)用。下面看它的字節(jié)碼信息:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/T.class
  Last modified 2021-12-6; size 1070 bytes
  MD5 checksum 3490fbda3bf98c7fbd556ef4c0f5f3f4
  Compiled from "T.java"
public class com.zhang.jvm.reflect.example.T extends com.zhang.jvm.reflect.example.TParent
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #18.#38        // com/zhang/jvm/reflect/example/TParent."<init>":()V
   #2 = String             #39            // T
   #3 = Fieldref           #7.#40         // com/zhang/jvm/reflect/example/T.name:Ljava/lang/String;
   #4 = Fieldref           #41.#42        // java/lang/System.out:Ljava/io/PrintStream;
   #5 = String             #43            // I am T
   #6 = Methodref          #44.#45        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #7 = Class              #46            // com/zhang/jvm/reflect/example/T
   #8 = Methodref          #7.#38         // com/zhang/jvm/reflect/example/T."<init>":()V
   #9 = Class              #47            // java/lang/StringBuilder
  #10 = Methodref          #9.#38         // java/lang/StringBuilder."<init>":()V
  #11 = String             #48            // tParent.name:
  #12 = Methodref          #9.#49         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #13 = Fieldref           #18.#40        // com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
  #14 = String             #50            // \nt.name:
  #15 = Methodref          #9.#51         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #16 = Methodref          #18.#52        // com/zhang/jvm/reflect/example/TParent.say:()V
  #17 = Methodref          #7.#52         // com/zhang/jvm/reflect/example/T.say:()V
  #18 = Class              #53            // com/zhang/jvm/reflect/example/TParent
  #19 = Utf8               name
  #20 = Utf8               Ljava/lang/String;
  #21 = Utf8               <init>
  #22 = Utf8               ()V
  #23 = Utf8               Code
  #24 = Utf8               LineNumberTable
  #25 = Utf8               LocalVariableTable
  #26 = Utf8               this
  #27 = Utf8               Lcom/zhang/jvm/reflect/example/T;
  #28 = Utf8               say
  #29 = Utf8               main
  #30 = Utf8               ([Ljava/lang/String;)V
  #31 = Utf8               agrs
  #32 = Utf8               [Ljava/lang/String;
  #33 = Utf8               tParent
  #34 = Utf8               Lcom/zhang/jvm/reflect/example/TParent;
  #35 = Utf8               t
  #36 = Utf8               SourceFile
  #37 = Utf8               T.java
  #38 = NameAndType        #21:#22        // "<init>":()V
  #39 = Utf8               T
  #40 = NameAndType        #19:#20        // name:Ljava/lang/String;
  #41 = Class              #54            // java/lang/System
  #42 = NameAndType        #55:#56        // out:Ljava/io/PrintStream;
  #43 = Utf8               I am T
  #44 = Class              #57            // java/io/PrintStream
  #45 = NameAndType        #58:#59        // println:(Ljava/lang/String;)V
  #46 = Utf8               com/zhang/jvm/reflect/example/T
  #47 = Utf8               java/lang/StringBuilder
  #48 = Utf8               tParent.name:
  #49 = NameAndType        #60:#61        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #50 = Utf8               \nt.name:
  #51 = NameAndType        #62:#63        // toString:()Ljava/lang/String;
  #52 = NameAndType        #28:#22        // say:()V
  #53 = Utf8               com/zhang/jvm/reflect/example/TParent
  #54 = Utf8               java/lang/System
  #55 = Utf8               out
  #56 = Utf8               Ljava/io/PrintStream;
  #57 = Utf8               java/io/PrintStream
  #58 = Utf8               println
  #59 = Utf8               (Ljava/lang/String;)V
  #60 = Utf8               append
  #61 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #62 = Utf8               toString
  #63 = Utf8               ()Ljava/lang/String;
{
  public java.lang.String name;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC

  public com.zhang.jvm.reflect.example.T();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method com/zhang/jvm/reflect/example/TParent."<init>":()V
         4: aload_0
         5: ldc           #2                  // String T
         7: putfield      #3                  // Field name:Ljava/lang/String;
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  this   Lcom/zhang/jvm/reflect/example/T;

  public void say();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #5                  // String I am T
         5: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 9: 0
        line 10: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  this   Lcom/zhang/jvm/reflect/example/T;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #7                  // class com/zhang/jvm/reflect/example/T
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: checkcast     #7                  // class com/zhang/jvm/reflect/example/T
        12: astore_2
        13: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
        16: new           #9                  // class java/lang/StringBuilder
        19: dup
        20: invokespecial #10                 // Method java/lang/StringBuilder."<init>":()V
        23: ldc           #11                 // String tParent.name:
        25: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: aload_1
        29: getfield      #13                 // Field com/zhang/jvm/reflect/example/TParent.name:Ljava/lang/String;
        32: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        35: ldc           #14                 // String \nt.name:
        37: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        40: aload_2
        41: getfield      #3                  // Field name:Ljava/lang/String;
        44: invokevirtual #12                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        47: invokevirtual #15                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        50: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        53: aload_1
        54: invokevirtual #16                 // Method com/zhang/jvm/reflect/example/TParent.say:()V
        57: aload_2
        58: invokevirtual #17                 // Method say:()V
        61: return
      LineNumberTable:
        line 12: 0
        line 13: 8
        line 15: 13
        line 17: 53
        line 18: 57
        line 19: 61
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      62     0  agrs   [Ljava/lang/String;
            8      54     1 tParent   Lcom/zhang/jvm/reflect/example/TParent;
           13      49     2     t   Lcom/zhang/jvm/reflect/example/T;
}
SourceFile: "T.java"

這個字節(jié)碼信息比較多募寨,我們主要觀察 main 方法的指令集信息:

  • tParent.name 對應(yīng)第 29 行指令族展,使用的常量池索引是 #13,是一個Fieldref 類型拔鹰,所屬的類索引就是 TParent仪缸。
  • tParent.name 對應(yīng)第 41 行指令,使用的常量池索引是 #3列肢,是一個Fieldref 類型恰画,所屬的類索引就是 T
  • tParent.say() 對應(yīng)第 54 行指令瓷马,使用的常量池索引是 #16拴还,是一個Methodref 類型,所屬的類索引就是 TParent欧聘。
  • tParent.say() 對應(yīng)第 58 行指令片林,使用的常量池索引是 #17,是一個Methodref 類型树瞭,所屬的類索引就是 T拇厢。

主要的區(qū)別就是在于 invokevirtual 指令,就是它實現(xiàn)了方法多態(tài)的功能晒喷,它會根據(jù)運行期對象實際類型去匹配對應(yīng)的方法孝偎,而不是根據(jù)這里 Methodref 常量中規(guī)定所屬類去匹配。

7.4 內(nèi)部類

java 語言中內(nèi)部類其實是一個非常特殊的存在凉敲,里面有很多javac 編譯器幫我們做的事情衣盾,如下是一個簡單的內(nèi)部類:

public class TS {

    TInner inner = null;

    public void test() {
        inner.name = null;
    }

    class TInner {
        private String name;
    }
}

先看一下TS 的字節(jié)碼:

Classfile /Users/zhangxinhao/work/java/test/example/jvm/build/classes/java/main/com/zhang/jvm/reflect/example/TS.class
  Last modified 2021-12-6; size 637 bytes
  MD5 checksum 5ebdb2d72b0b3d5a224c59860e8b386a
  Compiled from "TS.java"
public class com.zhang.jvm.reflect.example.TS
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #4.#22         // com/zhang/jvm/reflect/example/TS.inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
   #3 = Methodref          #6.#23         // com/zhang/jvm/reflect/example/TS$TInner.access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
   #4 = Class              #24            // com/zhang/jvm/reflect/example/TS
   #5 = Class              #25            // java/lang/Object
   #6 = Class              #26            // com/zhang/jvm/reflect/example/TS$TInner
   #7 = Utf8               TInner
   #8 = Utf8               InnerClasses
   #9 = Utf8               inner
  #10 = Utf8               Lcom/zhang/jvm/reflect/example/TS$TInner;
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               LocalVariableTable
  #16 = Utf8               this
  #17 = Utf8               Lcom/zhang/jvm/reflect/example/TS;
  #18 = Utf8               test
  #19 = Utf8               SourceFile
  #20 = Utf8               TS.java
  #21 = NameAndType        #11:#12        // "<init>":()V
  #22 = NameAndType        #9:#10         // inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
  #23 = NameAndType        #27:#28        // access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
  #24 = Utf8               com/zhang/jvm/reflect/example/TS
  #25 = Utf8               java/lang/Object
  #26 = Utf8               com/zhang/jvm/reflect/example/TS$TInner
  #27 = Utf8               access$002
  #28 = Utf8               (Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
{
  com.zhang.jvm.reflect.example.TS$TInner inner;
    descriptor: Lcom/zhang/jvm/reflect/example/TS$TInner;
    flags:

  public com.zhang.jvm.reflect.example.TS();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: aconst_null
         6: putfield      #2                  // Field inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
         9: return
      LineNumberTable:
        line 6: 0
        line 8: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/zhang/jvm/reflect/example/TS;

  public void test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field inner:Lcom/zhang/jvm/reflect/example/TS$TInner;
         4: aconst_null
         5: invokestatic  #3                  // Method com/zhang/jvm/reflect/example/TS$TInner.access$002:(Lcom/zhang/jvm/reflect/example/TS$TInner;Ljava/lang/String;)Ljava/lang/String;
         8: pop
         9: return
      LineNumberTable:
        line 11: 0
        line 12: 9
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      10     0  this   Lcom/zhang/jvm/reflect/example/TS;
}
SourceFile: "TS.java"
InnerClasses:
     #7= #6 of #4; //TInner=class com/zhang/jvm/reflect/example/TS$TInner of class com/zhang/jvm/reflect/example/TS

在這個類的字節(jié)碼文件中,你會發(fā)現(xiàn)一個很有意思的事情爷抓,那就是在 test 方法指令集中势决,語句 inner.name = null 居然沒有對應(yīng) putfield 指令,而是 invokestatic 指令蓝撇,調(diào)用了TInner 中一個名為access$002 的類方法果复。

為什么會這樣呢?

  • 那是因為字段都是有訪問權(quán)限的渤昌,而 TInnername 字段的訪問權(quán)限是private虽抄,那么只有這個 TInner 類里面才可以訪問它,也就是這個name 字段只能在TInner 類的常量池中生成對應(yīng)的CONSTANT_field_info 類型常量独柑。
  • 但是java 語言規(guī)范里面迈窟,外部類又可以調(diào)用內(nèi)部類的私有字段,所以javac 編譯器幫我們做了處理忌栅,在 TInner 類中生成了一個名為access$002靜態(tài)方法來給私有字段賦值车酣。

TInner 的字節(jié)碼不能使用javap 命令看到,我就簡單地說一下,有如下重點:

  • TInner 字節(jié)碼中有兩個字段湖员,namethis$0贫悄。

    this$0 的類型就是 Lcom/zhang/jvm/reflect/example/TS; ,也就是說內(nèi)部類都持有一個外部類類型的字段破衔。

  • TInner 字節(jié)碼的構(gòu)造器方法
    0 aload_0
    1 aload_1
    2 putfield #2 <com/zhang/jvm/reflect/example/TS$TInner.this$0>
    5 aload_0
    6 invokespecial #3 <java/lang/Object.<init>>
    9 return
    

    也就是說內(nèi)部類的構(gòu)造函數(shù)肯定會將外部類的引用傳遞進來的清女,然后賦值給this$0字段。

  • 如果外部類給內(nèi)部類的私有變量賦值了晰筛,那么就會生成一個靜態(tài)方法,來進行內(nèi)部類私有變量的賦值拴袭。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末读第,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子拥刻,更是在濱河造成了極大的恐慌怜瞒,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件般哼,死亡現(xiàn)場離奇詭異吴汪,居然都是意外死亡,警方通過查閱死者的電腦和手機蒸眠,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進店門漾橙,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人楞卡,你說我怎么就攤上這事霜运。” “怎么了蒋腮?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵淘捡,是天一觀的道長。 經(jīng)常有香客問我池摧,道長焦除,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任作彤,我火速辦了婚禮膘魄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宦棺。我一直安慰自己瓣距,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布代咸。 她就那樣靜靜地躺著蹈丸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逻杖,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天奋岁,我揣著相機與錄音,去河邊找鬼荸百。 笑死闻伶,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的够话。 我是一名探鬼主播蓝翰,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼女嘲!你這毒婦竟也來了畜份?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤欣尼,失蹤者是張志新(化名)和其女友劉穎爆雹,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愕鼓,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡钙态,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了菇晃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片册倒。...
    茶點故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖谋旦,靈堂內(nèi)的尸體忽然破棺而出剩失,到底是詐尸還是另有隱情,我是刑警寧澤册着,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布拴孤,位于F島的核電站,受9級特大地震影響甲捏,放射性物質(zhì)發(fā)生泄漏演熟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一司顿、第九天 我趴在偏房一處隱蔽的房頂上張望芒粹。 院中可真熱鬧,春花似錦大溜、人聲如沸化漆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽座云。三九已至疙赠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間朦拖,已是汗流浹背圃阳。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留璧帝,地道東北人捍岳。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓,卻偏偏與公主長得像睬隶,于是被迫代替她去往敵國和親锣夹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,697評論 2 351

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