共计 5852 个字符,预计需要花费 15 分钟才能阅读完成。
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});
}
}