目录
class
文件一组以8位
字节为基础单位的二进制流,每一个字节用2个
十六进制表示。在解读这些十六进制数据时,JVM文档定义了一套自己的数据类型来表示class
文件数据:使用u1
, u2
, u4
和u8
分别来代表1个字节
,2个字节
、4个字节
和8个字节
。下图使用的是010Editor
打开的一个class
文件:
如图所示,u2 minor_version中的u2
表示占用2个字节,而上面会显示占用连续的4个十六进制位。在class文件中,字节的存储顺序、长度等都是有严格限定的,不允许改变。
在计算机中,1个字节 = 8位(8个二进制位),1个十六进制 = 4个二进制位 (如:F = 1111),1个字节 = 2个十六进制,所以2个字节用4个十六进制表示。
分析class文件常用的工具:
- javap (JDK自带的class文件解析工具)
- 010Editor (号称世界上最好的十六进制编辑器)
- Java Decompiler (用于反编译class文件)
上图就是使用的010Editor
打开的class文件。
Class类文件的结构
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
u4 magic
表示magic占用了4个字节存储,而文件中前四个字节是CAFEBABE
,表明CAFEBABE
属于magic
。
magic
的作用类似于文件的扩展名,我们通过文件扩展名可以很容易看出这个文件是什么类型的文件,但是由于文件扩展名容易被修改,所以在文件中通过magic
来标记这个属于什么文件。如:JVM通过读取前4个字节就知道该文件是不是一个class文件。
minor_version 和 major_version
minor_version
和major_version
表示的是class文件被编译时使用的JDK版本号,minor_version
表示次版本号,major_version
表示主版本号。
常量项个数(constant_pool_count)
The value of the constant_pool_count item is equal to the number of entries in the constant_pool table plus one.
constant_pool_count
显示常量池中共有多少个数据项,但实际的数量比constant_pool_count
少一个,如constant_pool_count
= 72,则constant_pool
为[0] ~ [70]。
常量池(constant_pool[constant_pool_count - 1])
The constant_pool table is indexed from 1 to constant_pool_count - 1.
Each item in the constant_pool table must begin with a 1-byte tag indicating the kind of cp_info entry.
constant_pool
是一个结构表,它表示在ClassFile结构及其子结构中引用的各种字符串常量,类和接口名称,字段名称以及其他常量。每个常量池结构中的第一个字节必须是tag
标签,tag
标签表示当前这个常量属于哪种常量类型。
常量池索引是从1到constant_pool_count - 1,而常量池数组是从0开始的,所以索引指向的时候要减1。
例如:
name_index = 0x0023 = 35
,实际上name索引值指向的是constant_pool[34]
;
constant_pool_count = 0x0025 = 37,实际常量项个数是36,这样做的目的是满足后面其他结构中需要表明不引用任何一个常量项的含义,这个时候就将索引值置为0。
常量池的结构如下:
cp_info {
u1 tag;
u1 info[];
}
tag
标签类型有如下14种:
类型 | 标志 | 描述 |
---|---|---|
CONSTANT_Class | 7 | 类或接口的符号引用 |
CONSTANT_Fieldref | 9 | 字段的符号引用 |
CONSTANT_Methodref | 10 | 类中方法 |
CONSTANT_InterfaceMethodref | 11 | 接口中方法的符号引用 |
CONSTANT_String | 8 | 字符串型字面量 |
CONSTANT_Integer | 3 | 整形字面量 |
CONSTANT_Float | 4 | 浮点型字面量 |
CONSTANT_Long | 5 | 长整型字面量 |
CONSTANT_Double | 6 | 双精度浮点型字面量 |
CONSTANT_NameAndType | 12 | 字段或方法的部分符号引用 |
CONSTANT_Utf8 | 1 | UTF-8编码的字符串 |
CONSTANT_MethodHandle | 15 | 表示方法句柄 |
CONSTANT_MethodType | 16 | 表示方法类型 |
CONSTANT_InvokeDynamic | 18 | 表示一个动态方法调用点 |
CONSTANT_Class_info 型常量的结构
CONSTANT_Class_info
型常量代表一个类或接口,其结构如下:
CONSTANT_Class_info {
u1 tag; // 用于区分常量类型
u2 name_index; // 索引值,指向常量池中一个`CONSTANT_Utf8_info`类型常量,此常量代表了这个类(或者接口)的全限定名
}
除了Object
类以外,其他类一般至少有两个CONSTANT_Class_info
类型的常量,如下图所示:
在图中u2 name_index
的索引值是68
,表示的是name_index
对应着常量池中constant_pool[67]
,查看该值:
由上图可以看出,类名为com/fhmou/base/IntTest
。
CONSTANT_Utf8_info 型常量的结构
CONSTANT_Utf8_info
型常量用来表示常量字符串值,其结构如下:
CONSTANT_Utf8_info {
u1 tag; // 用于区分常量类型
u2 length; // length表示字符串长度是多少字节(不是字符串长度)
u1 bytes[length]; // 字符串字节数组
}
length
表示字符串中字节的大小,而u2
则表示length
最大的取值范围,u2
指的是存储2个字节即16位,也就是说,length
最大的取值是65535B
。 65535 B ≈ 64 K B 65535B \approx 64KB 65535B≈64KB,也就是说Java程序中如果定义了超过64KB英文字符的变量或方法名,将会无法编译。
字段类型存储在CONSTANT_Utf8_info
型常量结构中,每种类型的字符串只会在常量池中存储一份,如:
private int a;
private int b;
private int c;
在常量池中存储类型字符串的时候,只会存储一次int
,且int
在常量池中会改写为I
,其他类型标识字符如下所示:
标识字符 | 含义 |
---|---|
B | 基本类型byte |
C | 基本类型char |
D | 基本类型double |
F | 基本类型float |
I | 基本类型int |
J | 基本类型long |
L ClassName; | 对象类型 |
S | 基本类型short |
Z | 基本类型boolean |
[ | 数组对象 |
CONSTANT_Fieldref\Methodref\InterfaceMethodref_info 型常量的结构
Fields, methods, and interface methods are represented by similar structures:
CONSTANT_***ref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
class
文件是类
类型的话,常量池中会有一个CONSTANT_Methodref_info
类型的常量,如下所示,CONSTANT_Methodref_info
的值为java/lang/Object."<init>":()V
,<init>
表示一个实例的初始化方法,放回值为空,这时的CONSTANT_Fieldref_info
用于描述字段信息。
#1 = Methodref #23.#57 // java/lang/Object."<init>":()V
tag
类型为CONSTANT_Methodref_info
时,class_index
必须指向一个类,不能是接口;tag
类型为CONSTANT_InterfaceMethodref_info
时,class_index
必须指向一个接口,不能是类;tag
类型为CONSTANT_Fieldref_info
时,class_index
可能指向一个类或接口;name_and_type_index
必须是字段描述否则就是方法的描述。
类
中定义了几个字段,就有几个CONSTANT_Fieldref_info
常量,字段
在常量池中引用关系如下:
CONSTANT_NameAndType_info型常量的结构
CONSTANT_NameAndType_info
型常量结构常用来表示字段
或方法
,但是不包含该字段或方法属于那个类或接口,其结构如下:
CONSTANT_NameAndType_info {
u1 tag; // 类型标记
u2 name_index; // 指向该字段或方法名称的常量项索引
u2 descriptor_index; // 指向字段类型或方法返回值类型的常量项索引
}
如在类
中定义了一个字段private double dNum = 20.0;
,在常量池中可以看到定义的该字段,如下图所示,在CONSTANT_NameAndType_info
型常量结构中只显示了name_index
和descriptor_index
索引,而不是直接显示值,这是为了减少重复定义。
访问标志(access_flags)
在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用来识别这个Class文件是一个类
还是接口
文件,文件的修饰类型:public
, final
, abstract
等,具体的标志含义如下:
Flag | 十六进制 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 定义public类型 |
ACC_FINAL | 0x0010 | 定义final类型,不允许有子类 |
ACC_SUPER | 0x0020 | 用来表示如何调用父类的方法,JDK 1.0.2以后编译出来的类的这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 标识这是一个接口,不是类 |
ACC_ABSTRACT | 0x0400 | 定义abstract类型,不允许实例化 |
ACC_SYNTHETIC | 0x1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标识这是一个注释 |
ACC_ENUM | 0x4000 | 标识这是一个枚举类型 |
Class文件可能有多个访问标志信息,多个访问标志的值加起来的值就是Class文件中标识位的值,如下查看的是一个普通类文件:
如图所示,该类是一个普通的Java类,不是接口、枚举或者注解,被public关键字修饰但没有被申明为final和abstract,并且使用JDK 1.8 编译器编译,所以 access_flags = 0x0001 + 0x0020 = 0x0021。而图中的access_flags标志确为0x0021。
如果定义的是一个接口类,那么访问标志为:ACC_ANNOTATION
和 ACC_INTERFACE
。
类、父类(this_class、super_class)
this_class
和super_class
的值都是一个索引值,指向的是constant_pool
数组项。
接口
interfaces_count
表示该类文件实现了接口的数量;interfaces[]
数组中的每个值都必须是constant_pool
表中的有效索引。
字段
fields_count
该类或者接口所拥有的字段数。
field_info
结构用于表示字段的详细信息,其结构如下:
field_info {
u2 access_flags; // 字段访问标志
u2 name_index; // 字段名称
u2 descriptor_index; // 字段类型
u2 attributes_count; // 属性数量
attribute_info attributes[attributes_count]; // 属性项
}
access_flags
:用于表示字段的访问属性,有如下几种类型:
标志名称 | 含义 |
---|---|
ACC_PUBLIC | 字段是否 public |
ACC_PRIVATE | 字段是否 private |
ACC_PROTECTED | 字段是否 protected |
ACC_STATIC | 字段是否 static |
ACC_FINAL | 字段是否 final |
ACC_VOLATILE | 字段是否 volatile |
ACC_TRANSIENT | 字段是否 transient |
ACC_SYSTHETIC | 字段是否由编译器自动生成 |
ACC_ENUM | 字段是否 enum |
name_index
:CONSTANT_Utf8类型常量项的索引,里面存储了字段名称。
descriptor_index
:CONSTANT_Utf8类型常量项的索引,里面存储了字段描述符。
描述符标识字符含义:
标识字符 | 含义 | 标识字符 | 含义 |
---|---|---|---|
B | byte | J | long |
C | char | S | short |
D | double | Z | boolean |
F | float | V | void |
I | int | L | 对象类型 |
注:字段集合中不会列出从超类或者父接口中继承而来的字段。
方法
methods_count
: 该类或接口拥有的方法数。
methods
:列表每一下为method_info数据,method_info结构如下:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
access_flags
:用于表示方法的访问属性。
name_index
:CONSTANT_Utf8类型常量项的索引,里面存储了方法名称。
descriptor_index
:CONSTANT_Utf8类型常量项的索引,里面存储了方法描述符。
attributes_count
:当前方法拥有的attribute数量。
attributes
:列表每一项为attribute_info结构数据,下面会详细描述。
方法中的Java代码,经过编译器编译成字节码指令后,存放在方法属性集合中一个名为Code
的属性里面。
属性
在Class文件
、字段
、方法
中都可以携带自己的属性集合,以用于描述某些场景专有信息。
例如:
- Class文件:
SourceFile
用于记录类文件名称; - 字段:
ConstantValue
用于表示final
关键字定义的常量值; - 方法:使用
Code
属性存放Java代码编译成的字节码指令。
Code属性
每个方法在执行的同时都会创建一个栈帧(Stack Frame)
用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息,如下图所示:
Java程序方法体中的代码经过Javac
编译器处理后,最终变为字节码指令存储在Code
属性内。并非所有的方法都必须存在这个属性,譬如接口或者抽象类中的方法就不存在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
。
attribute_length
: 属性长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性长度减去6个字节。
max_stack
: 操作数栈深度的最大值。
max_locals
: 代表了局部变量表所需的存储空间。max_locals
的单位是Slot
,Slot
是虚拟机为局部变量分配内存所使用的最小单位。
code_length
和code
用来存储Java源程序编译后生成的字节码指令
。code_length
代表字节码长度,code
是用来存储字节码指令的一系列字节流。每一个字节码指令
就是一个u1类型的字节,而字节码指令的长度不能超过4个字节,所以只要不编写超长的方法,一般字节码指令
的个数是不会超过4个字节的。字节码指令
如下图所示:
下面列出了几个常用的虚拟机字节码指令表
字节码 | 助记符 | 指令含义 |
---|---|---|
0x2A | aload_0 | 将第一个引用类型本地变量推送至栈顶 |
0xB7 | invokespecial | 调用超类构造方法,实例初始化方法,私有方法 |
0x11 | sipush | 将一个短整型常量值(-32768 ~ 32767)推送至栈顶 |
0xB5 | putfield | 为指定的类的实例域赋值 |
0x12 | ldc | 将int, float 或 String型常量值从常量池中推送至栈顶 |
0x13 | ldc_w | 将int, float 或 String型常量值从常量池中推送至栈顶(宽索引) |
0x14 | ldc2_w | 将long 或 double型常量值从常量池中推送至栈顶(宽索引) |
0xB1 | return | 从当前方法返回void |
0xB2 | getstatic | 获取指定类的静态域,并将其值压入栈顶 |
OxBB | new | 创建一个对象,并将其引用值压入栈顶 |
exception
: 作用是列举出方法中可能抛出的异常,也就是方法描述时在throws
关键字后面列举的异常。
attributes
: 方法的属性,如下图所示:
LineNumberTable
属性用于描述Java源码行号与字节码行号之间的对应关系。
LocalVariableTable
属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。
Javap
在JDK 的bin
目录中,Orcale公司为我们提供了一个专门用于分析Class文件字节码的工具:javap
,使用javap
工具的-verbose
参数输出Class文件字节码内容。
源码如下:
package com.fhmou.base;
public class IntTest {
private int num1 = 1000;
public final static int num2 = 2000;
private long lNum1 = 99999;
private long lNum2 = 99999;
private double dNum = 20.0;
private String name1 = "aaaa";
private String name2 = "aaaa";
private String text = "bbbb";
public String getNameTest() {
return "this is a test";
}
public static void main(String[] args) {
int a = 3;
int b = 4;
int c = 4;
long lNum2 = 200000;
double dNum2 = 20000.0d;
String text1 = "aaaa";
String text2 = "dddd";
}
}
使用javap
反编译:
javap -verbose IntTest
Warning: Binary file IntTest contains com.fhmou.base.IntTest
Classfile /Users/luyanliang/program/me/learnProgram/javaSE/out/production/javaSE/com/fhmou/base/IntTest.class
Last modified Jul 10, 2019; size 1064 bytes
MD5 checksum f79a67dd40288c19574373d32c82346a
Compiled from "IntTest.java"
public class com.fhmou.base.IntTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #22.#58 // java/lang/Object."<init>":()V
#2 = Fieldref #21.#59 // com/fhmou/base/IntTest.num1:I
#3 = Long 99999l
#5 = Fieldref #21.#60 // com/fhmou/base/IntTest.lNum1:J
#6 = Fieldref #21.#61 // com/fhmou/base/IntTest.lNum2:J
#7 = Double 20.0d
#9 = Fieldref #21.#62 // com/fhmou/base/IntTest.dNum:D
#10 = String #63 // aaaa
#11 = Fieldref #21.#64 // com/fhmou/base/IntTest.name1:Ljava/lang/String;
#12 = Fieldref #21.#65 // com/fhmou/base/IntTest.name2:Ljava/lang/String;
#13 = String #66 // bbbb
#14 = Fieldref #21.#67 // com/fhmou/base/IntTest.text:Ljava/lang/String;
#15 = String #68 // this is a test
#16 = Long 200000l
#18 = Double 20000.0d
#20 = String #69 // dddd
#21 = Class #70 // com/fhmou/base/IntTest
#22 = Class #71 // java/lang/Object
#23 = Utf8 num1
#24 = Utf8 I
#25 = Utf8 num2
#26 = Utf8 ConstantValue
#27 = Integer 2000
#28 = Utf8 lNum1
#29 = Utf8 J
#30 = Utf8 lNum2
#31 = Utf8 dNum
#32 = Utf8 D
#33 = Utf8 name1
#34 = Utf8 Ljava/lang/String;
#35 = Utf8 name2
#36 = Utf8 text
#37 = Utf8 <init>
#38 = Utf8 ()V
#39 = Utf8 Code
#40 = Utf8 LineNumberTable
#41 = Utf8 LocalVariableTable
#42 = Utf8 this
#43 = Utf8 Lcom/fhmou/base/IntTest;
#44 = Utf8 getNameTest
#45 = Utf8 ()Ljava/lang/String;
#46 = Utf8 main
#47 = Utf8 ([Ljava/lang/String;)V
#48 = Utf8 args
#49 = Utf8 [Ljava/lang/String;
#50 = Utf8 a
#51 = Utf8 b
#52 = Utf8 c
#53 = Utf8 dNum2
#54 = Utf8 text1
#55 = Utf8 text2
#56 = Utf8 SourceFile
#57 = Utf8 IntTest.java
#58 = NameAndType #37:#38 // "<init>":()V
#59 = NameAndType #23:#24 // num1:I
#60 = NameAndType #28:#29 // lNum1:J
#61 = NameAndType #30:#29 // lNum2:J
#62 = NameAndType #31:#32 // dNum:D
#63 = Utf8 aaaa
#64 = NameAndType #33:#34 // name1:Ljava/lang/String;
#65 = NameAndType #35:#34 // name2:Ljava/lang/String;
#66 = Utf8 bbbb
#67 = NameAndType #36:#34 // text:Ljava/lang/String;
#68 = Utf8 this is a test
#69 = Utf8 dddd
#70 = Utf8 com/fhmou/base/IntTest
#71 = Utf8 java/lang/Object
{
public static final int num2;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
ConstantValue: int 2000
public com.fhmou.base.IntTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: sipush 1000
8: putfield #2 // Field num1:I
11: aload_0
12: ldc2_w #3 // long 99999l
15: putfield #5 // Field lNum1:J
18: aload_0
19: ldc2_w #3 // long 99999l
22: putfield #6 // Field lNum2:J
25: aload_0
26: ldc2_w #7 // double 20.0d
29: putfield #9 // Field dNum:D
32: aload_0
33: ldc #10 // String aaaa
35: putfield #11 // Field name1:Ljava/lang/String;
38: aload_0
39: ldc #10 // String aaaa
41: putfield #12 // Field name2:Ljava/lang/String;
44: aload_0
45: ldc #13 // String bbbb
47: putfield #14 // Field text:Ljava/lang/String;
50: return
LineNumberTable:
line 7: 0
line 9: 4
line 11: 11
line 12: 18
line 13: 25
line 14: 32
line 15: 38
line 16: 44
LocalVariableTable:
Start Length Slot Name Signature
0 51 0 this Lcom/fhmou/base/IntTest;
public java.lang.String getNameTest();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: ldc #15 // String this is a test
2: areturn
LineNumberTable:
line 19: 0
LocalVariableTable:
Start Length Slot Name Signature
0 3 0 this Lcom/fhmou/base/IntTest;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=10, args_size=1
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: iconst_4
5: istore_3
6: ldc2_w #16 // long 200000l
9: lstore 4
11: ldc2_w #18 // double 20000.0d
14: dstore 6
16: ldc #10 // String aaaa
18: astore 8
20: ldc #20 // String dddd
22: astore 9
24: return
LineNumberTable:
line 23: 0
line 24: 2
line 25: 4
line 26: 6
line 27: 11
line 28: 16
line 29: 20
line 30: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
2 23 1 a I
4 21 2 b I
6 19 3 c I
11 14 4 lNum2 J
16 9 6 dNum2 D
20 5 8 text1 Ljava/lang/String;
24 1 9 text2 Ljava/lang/String;
}
SourceFile: "IntTest.java"
如上所示,在程序中定义了两个方法:getNameTest()
和main(String[] args)
,而不管是使用010Editor
还是使用javap
查看Class文件都显示三个方法,这是因为默认隐藏了类的构造方法。
在上述三个方法中的args_size
都显示1
,但是却没有传入参数,这个因为在任何实例方法中,都可以通过this
关键字访问到此方法所属的对象。
关于基本数据类型在常量池的存放
int
型的值在-32768 ~ 32767
之间的话,不会把值放入到常量池中;
long
型(包括double, float)的值不是0
或1
的话,就会把值放入常量池中。
参考网站
- 《深入理解Java虚拟机》 – 周志明
- Oracle JVM规范中的 The ClassFile Structure