本文是第九章的一些笔记整理。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}); } }