JVM學習筆記(七):Class文件結構

1 來源

  • 來源:《Java虛擬機 JVM故障診斷與性能優化》——葛一鳴
  • 章節:第九章

本文是第九章的一些筆記整理。

2 概述

本文主要介紹了Class文件的主要組成,包括魔數、版本號、常量池、訪問標誌等。

3 Class文件概覽

根據JVM規範,一個Class文件可以非常嚴謹地描述爲:

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];
}

下面會按順序詳細介紹裏面的各個字段。

4 魔數

魔數(Magic Number)作爲Class的標誌,用來告訴JVM這是一個Class文件,魔數是一個4字節的無符號整數,固定爲0xCAFEBABE。如果一個Class文件不以0xCAFEBABE開頭,那麼會拋出如下錯誤:

在這裏插入圖片描述

Linux下可以直接使用vim打開class文件進行查看,比如需要打開一個Test.class文件,可以輸入如下命令:

vim -b Test.class
:%!xxd

切換到十六進制後就可以看到魔數了:

在這裏插入圖片描述

5 版本

魔數後面緊跟着Class的小版本和大版本號,這表示當前Class文件是由哪個版本的編譯期產生的。小版本和大版本後都是佔用兩個字節,比如下圖:

在這裏插入圖片描述

  • 0000是小版本號
  • 0037是大版本號,十進制爲55,也就是對應JDK 11版本的編譯期

6 常量池

在版本號後面,緊跟着就是常量池的數量以及若干個常量池表項:

在這裏插入圖片描述

在這裏插入圖片描述

其中每一個常量池表項都具有標籤屬性:

在這裏插入圖片描述

對應關係舉例如下:

  • tag爲3:類型爲CONSTANT_Integer
  • tag爲4:類型爲CONSTANT_Float

等等,比如CONSTANT_Integer結構如下:

CONSTANT_Integer_info {
    u1 tag;
    u4 bytes;
}

一個tag加上一個四字節的無符號整數。其他類型大部分類似,篇幅限制,詳細請看JVM規範

7 訪問標記

訪問標記使用兩個字節表示,用於表明該類的訪問信息,比如public/abstract等,對應關係如下:

  • ACC_PUBLIC0x0001,表示public
  • ACC_FINAL0x0010,表示是否爲final
  • ACC_SUPER0x0020,表示使用增強的方法調用父類的方法
  • ACC_INTERFACE0x0200,表示是否爲接口
  • ACC_ABSTRACT0x0400,表示是否爲抽象類
  • ACC_SYNTHETIC0x1000,由編譯期產生的類,沒有源碼對應
  • ACC_ANNOTATION0x2000,表示是否是註釋
  • ACC_ENUM0x4000,表示是否爲枚舉

8 當前類、父類和接口

格式如下:

u2             this_class;                                    
u2             super_class;
u2             interfaces_count;
u2             interfaces[interfaces_count];

其中this_classsuper_class都是兩個字節的無符號整數,指向常量池中的一個CONSTANT_Class,表示當前的類型以及父類。另外,由於一個類可以實現多個接口,因此需要以數組形式保存多個接口的索引,如果沒有實現任何接口,則interfaces_count爲0。

9 字段

字段的格式如下:

u2             fields_count;
field_info     fields[fields_count];

fields_count是一個2字節的無符號整數,字段數量之後是具體的字段信息,每個字段都是一個field_info的結構,如下所示:

field_info {
    u2             access_flags;                         //訪問標記,類似於類的訪問標記,可以表示public/private/static等等
    u2             name_index;                           //兩字節整數,指向常量池中的CONSTANT_Utf8
    u2             descriptor_index;                     //也是兩字節整數,用於描述字段類型,也指向常量池中的CONSTANT_Utf8
    u2             attributes_count;                     //屬性數量
    attribute_info attributes[attributes_count];         //屬性,比如存儲初始化值,一些註釋信息等,需要使用attribute_info
}

attribute_info {
    u2 attribute_name_index;                             //屬性名字,指向常量池的索引
    u4 attribute_length;                                 //屬性長度
    u1 info[attribute_length];                           //字節數組表示的信息
}

10 方法

10.1 方法基本結構

方法的格式如下:

u2             methods_count;
method_info    methods[methods_count];

其中每一個method_info結構表示一個方法:

method_info {
    u2             access_flags;                            //訪問標記,標記方法爲public/private等等
    u2             name_index;                              //方法名稱,一個指向常量池的索引
    u2             descriptor_index;                        //方法描述符,也是一個指向常量符的索引
    u2             attributes_count;                        //屬性數量
    attribute_info attributes[attributes_count];            //屬性,和字段類似,方法也可以攜帶屬性,一個屬性數量+一個屬性描述數組
}

10.2 Code屬性

方法的主要內容存放在屬性中,在屬性裏面最重要的一個屬性就是CodeCode存放着方法的字節碼等信息,結構如下:

Code_attribute {
    u2 attribute_name_index;                      //屬性名稱,指向常量池的索引
    u4 attribute_length;                          //屬性長度,不包括前6字節(u2+u4)
    u2 max_stack;                                 //操作數棧最大深度
    u2 max_locals;                                //局部變量表的最大值
    u4 code_length;                               //字節碼長度
    u1 code[code_length];                         //字節碼內容本身
    u2 exception_table_length;                    //異常處理表長度
    {   u2 start_pc;                              //四個字段表示在start_pc到end_pc兩個偏移量之間
        u2 end_pc;                                //如果遇到了catch_type指向的異常
        u2 handler_pc;                            //代碼就跳轉到handler_pc位置執行
        u2 catch_type;                            
    } exception_table[exception_table_length];    //異常表
    u2 attributes_count;
    attribute_info attributes[attributes_count];
}

Code屬性本身也包含其他屬性以進一步存儲一些額外信息,主要包括:

  • LineNumberTable
  • LocalVariableTable
  • StackMapTable

10.2.1 LineNumberTable

LineNumberTable用於記錄字節碼偏移量和行號的對應關係,結構如下:

LineNumberTable_attribute {
    u2 attribute_name_index;                             //指向常量池的索引
    u4 attribute_length;                                 //屬性長度
    u2 line_number_table_length;                         //表項記錄條數
    {   u2 start_pc;                                     //字節碼偏移量
        u2 line_number;	                                 //字節碼偏移量對應的行號
    } line_number_table[line_number_table_length];       //表數組,每一個元素對應的是一個<start_pc,line_number>元組
}

10.2.2 LocalVariableTable

這個屬性也叫局部變量表,記錄了一個方法中所有的局部變量,結構如下:

LocalVariableTable_attribute {
    u2 attribute_name_index;                                     //當前屬性名字,指向常量池的索引
    u4 attribute_length;                                         //屬性長度
    u2 local_variable_table_length;                              //局部變量表的表項條目
    {   u2 start_pc;                                             //當前局部變量開始位置
        u2 length;                                               //當前局部變量長度(可用於計算結束位置)
        u2 name_index;                                           //局部變量名稱,指向常量池的索引
        u2 descriptor_index;                                     //局部變量的類型描述,指向常量池的索引
        u2 index;                                                //局部變量在當前棧幀的局部變量表中的槽位
    } local_variable_table[local_variable_table_length];         
}

10.2.3 StackMapTable

StackMapTable中含有若干個棧映射幀(Stack Map Frame)的數據,不包含運行時所需要的信息,僅用作Class文件的類型校驗,結構如下:

StackMapTable_attribute {
    u2              attribute_name_index;                         //常量池索引,恆爲"StackMapTable"
    u4              attribute_length;                             //屬性長度
    u2              number_of_entries;                            //棧映射幀的數量
    stack_map_frame entries[number_of_entries];                   //具體的棧映射幀
}

union stack_map_frame {                                           //每個棧映射幀被定義爲一個枚舉值,取值如下
    same_frame;                                                   //具體每個取值的意義可以查看JVM規範
    same_locals_1_stack_item_frame;                               //https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.7.4
    same_locals_1_stack_item_frame_extended;
    chop_frame;
    same_frame_extended;
    append_frame;
    full_frame;
}

每個棧映射幀是爲了說明在一個特定的字節碼偏移位置上,系統的數據類型是什麼,包括局部變量表的類型和操作數棧的類型。

11 附錄:ASM簡單使用

ASM是一個Java字節碼操作庫,很多著名的庫都依賴於該庫,比如AspectJCGLIB等等。但是ASM的性能遠遠超過CGLIB等高層字節碼庫,因爲ASM更加接近底層,使用更爲靈活且功能更爲強大。

下面是一個簡單的使用ASM輸出Hello World的例子:

package com.company;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class Main extends ClassLoader implements Opcodes {
    public static void main(String[] args) throws Exception{
    	//創建ClassWriter,指定COMPUTE_MAXS和COMPUTE_FRAMES,分別表示計算最大局部變量表以及最深操作數棧
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES);
        //通過ClassWriter設置類的基本信息,比如public訪問標記,類名爲Example
        cw.visit(V11,ACC_PUBLIC,"Example",null,"java/lang/Object",null);
        //生成Example的構造方法
        MethodVisitor mw = cw.visitMethod(ACC_PUBLIC ,"<init>","()V",null,null);
        mw.visitVarInsn(ALOAD,0);
        mw.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","<init>","()V",false);
        mw.visitInsn(RETURN);
        mw.visitMaxs(0,0);
        mw.visitEnd();

		//生成public static void main(String []args)方法,並生成了main()方法的字節碼
		//要求運行時調用System.out.println(),並輸出"Hello world":
        mw = cw.visitMethod(ACC_PUBLIC+ACC_STATIC,"main","([Ljava/lang/String;)V",null,null);
        mw.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");
        mw.visitLdcInsn("Hello world!");
        mw.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(Ljava/lang/String;)V",false);
        mw.visitInsn(RETURN);
        mw.visitMaxs(0,0);
        mw.visitEnd();

		//獲取二進制表示
        byte[] code = cw.toByteArray();
        Main m = new Main();
        //將class文件載入系統,通過反射調用`main()`方法,輸出結果
        Class<?> mainClass = m.defineClass("Example",code,0,code.length);
        mainClass.getMethods()[0].invoke(null, new Object[]{null});
    }
}

在這裏插入圖片描述

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