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_PUBLIC
:0x0001
,表示public
類ACC_FINAL
:0x0010
,表示是否爲final
類ACC_SUPER
:0x0020
,表示使用增強的方法調用父類的方法ACC_INTERFACE
:0x0200
,表示是否爲接口ACC_ABSTRACT
:0x0400
,表示是否爲抽象類ACC_SYNTHETIC
:0x1000
,由編譯期產生的類,沒有源碼對應ACC_ANNOTATION
:0x2000
,表示是否是註釋ACC_ENUM
:0x4000
,表示是否爲枚舉
8 當前類、父類和接口
格式如下:
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
其中this_class
與super_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
屬性
方法的主要內容存放在屬性中,在屬性裏面最重要的一個屬性就是Code
,Code
存放着方法的字節碼等信息,結構如下:
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
字節碼操作庫,很多著名的庫都依賴於該庫,比如AspectJ
、CGLIB
等等。但是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});
}
}