class字节码文件格式详解
class字节码文件格式
掌握jvm 字节码,最关键的是学习class文件格式以及字节码指令集等细节,今天我们来学习class字节码文件格式(jdk8版本)。
Java代码经过javac编译器编译成class文件,JVM虚拟机读取class文件执行其中的代码。
通过JVM虚拟机规范,实现了jvm跨平台、跨语言的能力,JVM规范中非常重要的一部分就是class字节码文件格式。 class文件结构
class文件的整体结构如下图所示,其中u1,u2,u4分别表示1个、2个、4个字节长度的无符号数据,无符号byte数据按照具体的场景可以用来表示数字、字符等。 结构中还可以使用复合结构,比如cp_info, cp_info结构也会在规范中进行定义。 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
魔法字符串,固定为0xCAFEBABE minor_version, major_version
分别是class文件的小版本号和大版本号,jvm规范要求运行的jvm版本必须大于等于(更严格说是能支持,不过目前大于等于即可)class文件的major_version才能运行,否则抛出异常。 constant_pool_count
常量池数量,是下面的常量池表的长度加一,因为index=0的常量引用没有使用。 constant_pool[]
常量池表,每个常量池的结构cp_info如下,常量池可以表示字符串常量、类名、接口名、方法等信息,这些常量池会在class文件中其他地方进行引用(比如字段中字段类型、字段名等)。 常量通过index进行引用,常量之间也可以通过index进行引用。
cp_info中的tag字段用来标识当前的常量类型,不同的常量类型有不同的子结构,然后就可以用具体的结构来解析info[]这个byte数组。 常量的结构有, cp_info { u1 tag; u1 info[]; }
Constant type
tag value
CONSTANT_Class
7
CONSTANT_Fieldref
9
CONSTANT_Methodref
10
CONSTANT_InterfaceMethodref
11
CONSTANT_Integer
3
CONSTANT_Float
4
CONSTANT_Long
5
CONSTANT_Double
6
CONSTANT_NameAndType
12
CONSTANT_Utf8
1
CONSTANT_MethodHandle
15
CONSTANT_MethodType
16
CONSTANT_InvokeDynamic
18
我们提前查看一下各个常量类型的结构,给后面介绍Field, Method做铺垫。 CONSTANT_Class_info { u1 tag; u2 name_index; } CONSTANT_Class_info
CONSTANT_Class_info表示类或接口 CONSTANT_Class_info { u1 tag; u2 name_index; }
tag: 是CONSTANT_Class对应的值(7) name_index: name_index是这个类或接口的类名的字符串常量的index CONSTANT_Fieldref_info, CONSTANT_Methodref_info, CONSTANT_InterfaceMethodref_infoCONSTANT_Fieldref_info { u1 tag; u2 class_index; u2 name_and_type_index; } CONSTANT_Methodref_info { u1 tag; u2 class_index; u2 name_and_type_index; } CONSTANT_InterfaceMethodref_info { u1 tag; u2 class_index; u2 name_and_type_index; }
字段引用、方法引用、接口方法引用这三个结构比较类似,都是各自的tag以及class_index和name_and_type_index
class_index: 这个字段、方法所在类的class的常量的index
name_and_type_index: 这个字段的名称和类型结构常量CONSTANT_NameAndType_info的index。name分别是字段名和方法名,类型是字段、方法的descriptor描述符。 CONSTANT_String_info
字符串常量结构
string_index: 指向CONSTANT_Utf8_info的index CONSTANT_String_info { u1 tag; u2 string_index; } CONSTANT_Integer_info, CONSTANT_Float_info
整数和浮点数常量结构,对应的数值占用4个字节。 CONSTANT_Integer_info { u1 tag; u4 bytes; } CONSTANT_Float_info { u1 tag; u4 bytes; } CONSTANT_Long_info, CONSTANT_Double_info
这两个常量结构分别存储long和double类型的数值,大小占用8个字节 high_bytes和low_bytes分别表示高位和低位的数据,以long为例,对应值为((long) high_bytes << 32) + low_bytes CONSTANT_Long_info { u1 tag; u4 high_bytes; u4 low_bytes; } CONSTANT_Double_info { u1 tag; u4 high_bytes; u4 low_bytes; } CONSTANT_NameAndType_info
CONSTANT_NameAndType_info常量用来表示名称和类型,在前面的CONSTANT_Fieldref_info, CONSTANT_Methodref_info, CONSTANT_InterfaceMethodref_info 常量中有使用,结构如下 CONSTANT_NameAndType_info { u1 tag; u2 name_index; u2 descriptor_index; }
name_index: 指向对应名称的utf8常量的CONSTANT_Utf8_info的index descriptor_index: 指向类型描述符的CONSTANT_Utf8_info的index。 Field Descriptor和Method Descriptor
在jvm中,数据分为primitive type(基本类型,比如int, long)和reference type(引用类型),类型的描述符规则如下
类型
描述符
byte
B
char
C
double
D
float
F
int
I
long
J
short
S
boolean
Z
reference,引用类型
LClassName;
数组
[
引用类型的ClassName是/间隔的字符串,比如java.lang.String的描述符为Ljava/lang/String; 数组是在对应的类型前加[,比如int[]描述符为[I, String[]描述符为[Ljava/lang/String;, 多维数组距离 int[][]描述符为[[I
Field Descriptor是对应字段的类型的描述符 Method Descriptor为( {ParameterDescriptor} ) ReturnDescriptor,比如public String test(int a, Long b)的方法描述符为(ILjava/lang/Long)Ljava/lang/String;,如果返回值是void,则使用V CONSTANT_Utf8_info
CONSTANT_Utf8_info常量存储utf8编码的字符串内容,包含一个字符串长度字段和对应长度的byte数组。 CONSTANT_Utf8_info { u1 tag; u2 length; u1 bytes[length]; } class access_flags
access_flags用来表示当前类的一些bit信息(类似bitmap),这样用2个字节的空间就可以表示16个标记信息。
Flag Name
Value
表头
ACC_PUBLIC
0x0001
表示当前类/接口是否是public
ACC_FINAL
0x0010
是否声明了final
ACC_SUPER
0x0020
都是true, 为了兼容旧版本的字节码的标记
ACC_INTERFACE
0x0200
是否是接口
ACC_ABSTRACT
0x0400
是否是抽象类,接口也是抽象类
ACC_SYNTHETIC
0x1000
表示不是代码中生成的类,比如jdk为实现lambda表达式在运行时生成的一些类
ACC_ANNOTATION
0x2000
是否是@interface这样的注解类
ACC_ENUM
0x4000
是否枚举类 Fields
Fields是field_info的数组,每个field_info结构如下。 field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
access_flags: 字段的access_flags,和class的access_flags类似,用来描述字段的public,private,volatile等等标识信息。
Flag Name
Value
描述
ACC_PUBLIC
0x0001
是否是public字段
ACC_PRIVATE
0x0002
是否是private字段
ACC_PROTECTED
0x0004
是否是static字段
ACC_STATIC
0x0008
是否是static字段
ACC_FINAL
0x0010
是否是final字段
ACC_VOLATILE
0x0040
是否是volatile字段
ACC_TRANSIENT
0x0080
是否是transient字段
ACC_SYNTHETIC
0x1000
单元格
ACC_ENUM
0x4000
单元格
name_index: 字段名称的CONSTANT_Utf8_info常量index descriptor_index: 字段类型描述符的CONSTANT_Utf8_info常量index attributes_count: 字段的属性数量 attributes: 字段的属性,结构为attribute_info,比如ConstantValue,描述常量字段的常量值,属性的结构稍后介绍。 Methods
类中所有的方法包括构造函数()、静态初始化方法(),都使用method_info结构,在一个类中,方法名称和方法签名联合起来必须唯一 method_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
access_flags: 方法的标识数据,包括public, private, synchronized等等信息
Flag Name
Value
描述
ACC_PUBLIC
0x0001
public方法
ACC_PRIVATE
0x0002
private方法
ACC_PROTECTED
0x0004
protected方法
ACC_STATIC
0x0008
static方法
ACC_FINAL
0x0010
final方法
ACC_SYNCHRONIZED
0x0020
synchronized方法(方法维度的synchronized声明,不同于synchronized代码块的monitor_enter和monitor_exit)
ACC_BRIDGE
0x0040
是否是transient字段
ACC_VARARGS
0x0080
有可变参数的方法
ACC_NATIVE
0x0100
native方法
ACC_ABSTRACT
0x0400
抽象方法
ACC_STRICT
0x0800
浮点数模式是FT-strict的,这个很少见
ACC_SYNTHETIC
0x1000
是否是合成方法,即不再源代码中的方法
name_index: 指向方法名的CONSTANT_Utf8_info常量 descriptor_index: 指向方法描述符的CONSTANT_Utf8_info常量 attributes_count: 方法的属性数量 attributes[]: 方法的各个属性,其中比较关键的是名字为Code的属性,包含的是方法体的字节码指令。 Attributes属性
Attributes属性在classfile, field_info, method_info中都有使用,结构如下 attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info[attribute_length]; }
attribute_name_index: 指向属性的名称的CONSTANT_Utf8_info常量 attribute_length: 属性信息的字节长度,即info的长度 info[]: 属性的具体信息,每种属性有自己的结构
属性有ConstantValue,Code,StackMapTable,Exceptions,BootstrapMethods等等很多种属性,我们这里重点介绍一下ConstantValue和Code。 ConstantValue属性
常量值属性用来表示常量字段的常量值,数值(int,long,float等)和字符串字段能够声明成常量。 ConstantValue_attribute { u2 attribute_name_index; u4 attribute_length; u2 constantvalue_index; }
attribute_name_index: 指向"ConstantValue"的CONSTANT_Utf8_info attribute_length: 2,因为constantvalue_index是两个byte长度的index constantvalue_index: 指向具体的常量池中的常量,按照类型不同分为CONSTANT_Long,CONSTANT_Float,CONSTANT_Double,CONSTANT_Integer(int, short, char, byte, boolean都用CONSTANT_Integer),CONSTANT_String, Code属性
Code属性用来表示方法体中的代码字节码。 Code_attribute { u2 attribute_name_index; u4 attribute_length; u2 max_stack; u2 max_locals; u4 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attributes_count]; }
attribute_name_index: 指向"Code"的CONSTANT_Utf8_info常量 attribute_length: 后面所有的字段信息的字节数 max_stack: 方法的字节码指令执行过程中需要的操作数栈的最大栈层数,关于方法字节码指令的执行,在字节码指令文章中进行介绍。 max_locals: 方法的字节码指令执行过程中需要的本地变量表的最大长度(注意局部变量表的元素长度是4字节,long和double变量在局部变量表中占两个位置) code_length: 方法体的字节码的长度 code[]: 方法体的字节码 exception_table_length: 异常表的长度 exception_table[]: 异常表数组,每个异常表包含start_pc,end_pc,handler_pc,catch_type。pc是指code[]数组中的索引,也就是从code[]字节码数组start_pc(包含)到end_pc(不包含)中的字节码执行时出现catch_type(指向异常类的CONSTANT_Class_info常量)异常,则转到code[]的handler_pc位置来处理异常。 attributes_count: Code属性的数量 attributes[]: Code属性数组,比如LineNumberTable,LocalVariableTable, LocalVariableTypeTable, StackMapTable
其他的属性可以参考jvm规范 如何解析class文件
假如我们现在有一个class文件,想去查看其中的Java源代码,该如何实现呢?有如下几种方法。 通过javap
javap是jdk里自带的反编译工具,可以打印出更加可读的class字节码信息。 javap -c -cp /Users/liuzhengyang/Code/work/code-test/target/classes/ test.Test Compiled from "Test.java" public class test.Test { public test.Test(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."":()V 4: return public java.lang.String hello(); Code: 0: ldc #2 // String hello world 2: areturn }
javap参数说明
参数
说明
-cp
指定classpath, javap需要到classpath中寻找class文件
-p
默认情况下javap不打印出private的方法、字段,通过-p可以打印全部信息
-c
默认情况下javap不打印出方法的body字节码,通过-c可以打印
-v
打印最全的信息,包括常量池、方法stack size、方法本地变量表等等 通过IDEA反编译
把class文件拖动到IDEA中即可查看到反编译的java代码结果,相比javap更加易读。 通过arthas jad命令
如果要查看运行中的程序中使用到的代码,可以使用arthas的[jad](https://arthas.aliyun.com/doc/jad.html)命令。 更多资料
更详细的资料包括java语言规范、java虚拟机规范可以在[Java Language and Virtual Machine Specifications](https://docs.oracle.com/javase/specs/index.html)中找到 总结
本篇文章介绍了class文件的结构,包括常量池、字段、方法、属性等,详细了解了每个数据的结构,最后了解查看class文件的几种方式。