本篇内容主要讲解“JVM中的Class文件结构”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“JVM中的Class文件结构”吧!
为福海等地区用户提供了全套网页设计制作服务,及福海网站建设行业解决方案。主营业务为成都做网站、成都网站设计、成都外贸网站建设、福海网站设计,以传统方式定制建设网站,并提供域名空间备案等一条龙服务,秉承以专业、用心的态度为用户提供真诚的服务。我们深信只要达到每一位用户的要求,就会得到认可,从而选择与我们长期合作。这样,我们也可以走得更远!
本文主要介绍了Class文件的主要组成,包括魔数、版本号、常量池、访问标志等。
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];
}下面会按顺序详细介绍里面的各个字段。
魔数(Magic Number)作为Class的标志,用来告诉JVM这是一个Class文件,魔数是一个4字节的无符号整数,固定为0xCAFEBABE。如果一个Class文件不以0xCAFEBABE开头,那么会抛出如下错误:

Linux下可以直接使用vim打开class文件进行查看,比如需要打开一个Test.class文件,可以输入如下命令:
vim -b Test.class :%!xxd
切换到十六进制后就可以看到魔数了:

魔数后面紧跟着Class的小版本和大版本号,这表示当前Class文件是由哪个版本的编译期产生的。小版本和大版本后都是占用两个字节,比如下图:

0000是小版本号
0037是大版本号,十进制为55,也就是对应JDK 11版本的编译期
在版本号后面,紧跟着就是常量池的数量以及若干个常量池表项:


其中每一个常量池表项都具有标签属性:

对应关系举例如下:
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
LineNumberTableLineNumberTable用于记录字节码偏移量和行号的对应关系,结构如下:
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]; //表数组,每一个元素对应的是一个元组
} 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];
}StackMapTableStackMapTable中含有若干个栈映射帧(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 ,"","()V",null,null);
mw.visitVarInsn(ALOAD,0);
mw.visitMethodInsn(INVOKESPECIAL,"java/lang/Object","","()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});
}
} 
到此,相信大家对“JVM中的Class文件结构”有了更深的了解,不妨来实际操作一番吧!这里是创新互联网站,更多相关内容可以进入相关频道进行查询,关注我们,继续学习!