本文是第九章的一些筆記整理。html
本文主要介紹了Class
文件的主要組成,包括魔數、版本號、常量池、訪問標誌等。java
Class
文件概覽根據JVM
規範,一個Class
文件能夠很是嚴謹地描述爲:web
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]; }
下面會按順序詳細介紹裏面的各個字段。vim
魔數(Magic Number
)做爲Class
的標誌,用來告訴JVM
這是一個Class
文件,魔數是一個4字節的無符號整數,固定爲0xCAFEBABE
。若是一個Class
文件不以0xCAFEBABE
開頭,那麼會拋出以下錯誤:數組
Linux
下能夠直接使用vim
打開class
文件進行查看,好比須要打開一個Test.class
文件,能夠輸入以下命令:性能優化
vim -b Test.class :%!xxd
切換到十六進制後就能夠看到魔數了:bash
魔數後面緊跟着Class
的小版本和大版本號,這表示當前Class
文件是由哪一個版本的編譯期產生的。小版本和大版本後都是佔用兩個字節,好比下圖:oracle
0000
是小版本號0037
是大版本號,十進制爲55
,也就是對應JDK 11
版本的編譯期在版本號後面,緊跟着就是常量池的數量以及若干個常量池表項:app
其中每個常量池表項都具備標籤屬性:jvm
對應關係舉例以下:
tag
爲3:類型爲CONSTANT_Integer
tag
爲4:類型爲CONSTANT_Float
等等,好比CONSTANT_Integer
結構以下:
CONSTANT_Integer_info { u1 tag; u4 bytes; }
一個tag
加上一個四字節的無符號整數。其餘類型大部分相似,篇幅限制,詳細請看JVM規範。
訪問標記使用兩個字節表示,用於代表該類的訪問信息,好比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
,表示是否爲枚舉格式以下:
u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count];
其中this_class
與super_class
都是兩個字節的無符號整數,指向常量池中的一個CONSTANT_Class
,表示當前的類型以及父類。另外,因爲一個類能夠實現多個接口,所以須要以數組形式保存多個接口的索引,若是沒有實現任何接口,則interfaces_count
爲0。
字段的格式以下:
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]; //字節數組表示的信息 }
方法的格式以下:
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]; //屬性,和字段相似,方法也能夠攜帶屬性,一個屬性數量+一個屬性描述數組 }
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
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>元組 }
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]; }
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; }
每一個棧映射幀是爲了說明在一個特定的字節碼偏移位置上,系統的數據類型是什麼,包括局部變量表的類型和操做數棧的類型。
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}); } }