带大家了解一个.class文件是如何运行的
1. 准备工作
首先我们编写一段java代码(带上main方法,因为需要编译)
public class MyTest {
//创建两个静态变量
public static int static_a;
public static double static_b;
//私有变量
private int private_c = 1;
public MyTest(int c){
this.private_c = c;
}
public int add(int a,int b){
int c = 0;
try{
c = a + b;
return c;
}catch (Exception e){
return 0;
}
}
public static void main(String[] args){
MyTest myTest = new MyTest(1);
int return_add = myTest.add(1,4);
System.out.println(return_add);
}
}
运行之后我们找到他的class文件,在cmd窗口下使用 javap -v xxx.class打开文件,结果如下
D:\workspace\TestClass\out\production\TestClass>javap -v MyTest.class
Classfile /D:/workspace/TestClass/out/production/TestClass/MyTest.class
Last modified 2019-6-29; size 937 bytes
MD5 checksum 17e7ebae90a730b8b15f05df32588d11
Compiled from "MyTest.java"
public class MyTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#40 // java/lang/Object."<init>":()V
#2 = Fieldref #4.#41 // MyTest.private_c:I
#3 = Class #42 // java/lang/Exception
#4 = Class #43 // MyTest
#5 = Methodref #4.#44 // MyTest."<init>":(I)V
#6 = Methodref #4.#45 // MyTest.add:(II)I
#7 = Fieldref #46.#47 // java/lang/System.out:Ljava/io/Print
Stream;
#8 = Methodref #48.#49 // java/io/PrintStream.println:(I)V
#9 = Class #50 // java/lang/Object
#10 = Utf8 static_a
#11 = Utf8 I
#12 = Utf8 static_b
#13 = Utf8 D
#14 = Utf8 private_c
#15 = Utf8 <init>
#16 = Utf8 (I)V
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 LocalVariableTable
#20 = Utf8 this
#21 = Utf8 LMyTest;
#22 = Utf8 c
#23 = Utf8 add
#24 = Utf8 (II)I
#25 = Utf8 e
#26 = Utf8 Ljava/lang/Exception;
#27 = Utf8 a
#28 = Utf8 b
#29 = Utf8 StackMapTable
#30 = Class #43 // MyTest
#31 = Class #42 // java/lang/Exception
#32 = Utf8 main
#33 = Utf8 ([Ljava/lang/String;)V
#34 = Utf8 args
#35 = Utf8 [Ljava/lang/String;
#36 = Utf8 myTest
#37 = Utf8 return_add
#38 = Utf8 SourceFile
#39 = Utf8 MyTest.java
#40 = NameAndType #15:#51 // "<init>":()V
#41 = NameAndType #14:#11 // private_c:I
#42 = Utf8 java/lang/Exception
#43 = Utf8 MyTest
#44 = NameAndType #15:#16 // "<init>":(I)V
#45 = NameAndType #23:#24 // add:(II)I
#46 = Class #52 // java/lang/System
#47 = NameAndType #53:#54 // out:Ljava/io/PrintStream;
#48 = Class #55 // java/io/PrintStream
#49 = NameAndType #56:#16 // println:(I)V
#50 = Utf8 java/lang/Object
#51 = Utf8 ()V
#52 = Utf8 java/lang/System
#53 = Utf8 out
#54 = Utf8 Ljava/io/PrintStream;
#55 = Utf8 java/io/PrintStream
#56 = Utf8 println
{
public static int static_a;
descriptor: I
flags: ACC_PUBLIC, ACC_STATIC
public static double static_b;
descriptor: D
flags: ACC_PUBLIC, ACC_STATIC
public MyTest(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>
":()V
4: aload_0
5: iconst_1
6: putfield #2 // Field private_c:I
9: aload_0
10: iload_1
11: putfield #2 // Field private_c:I
14: return
LineNumberTable:
line 8: 0
line 6: 4
line 9: 9
line 10: 14
LocalVariableTable:
Start Length Slot Name Signature
0 15 0 this LMyTest;
0 15 1 c I
public int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC
Code:
stack=2, locals=5, args_size=3
0: iconst_0
1: istore_3
2: iload_1
3: iload_2
4: iadd
5: istore_3
6: iload_3
7: ireturn
8: astore 4
10: iconst_0
11: ireturn
Exception table:
from to target type
2 7 8 Class java/lang/Exception
LineNumberTable:
line 13: 0
line 15: 2
line 16: 6
line 17: 8
line 18: 10
LocalVariableTable:
Start Length Slot Name Signature
10 2 4 e Ljava/lang/Exception;
0 12 0 this LMyTest;
0 12 1 a I
0 12 2 b I
2 10 3 c I
StackMapTable: number_of_entries = 1
frame_type = 255 /* full_frame */
offset_delta = 8
locals = [ class MyTest, int, int, int ]
stack = [ class java/lang/Exception ]
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: new #4 // class MyTest
3: dup
4: iconst_1
5: invokespecial #5 // Method "<init>":(I)V
8: astore_1
9: aload_1
10: iconst_1
11: iconst_4
12: invokevirtual #6 // Method add:(II)I
15: istore_2
16: getstatic #7 // Field java/lang/System.out:Ljav
a/io/PrintStream;
19: iload_2
20: invokevirtual #8 // Method java/io/PrintStream.prin
tln:(I)V
23: return
LineNumberTable:
line 23: 0
line 24: 9
line 25: 16
line 26: 23
LocalVariableTable:
Start Length Slot Name Signature
0 24 0 args [Ljava/lang/String;
9 15 1 myTest LMyTest;
16 8 2 return_add I
}
SourceFile: "MyTest.java"
之后我们将class文件使用notpead++编译器打开,notpead++需要安装HEX-Editor插件,将2进制转为16进制。以下划红线是重点用到的数据
2. Class文件的规定格式
下图中是class文件格式规范,
- 类型- u几就代表几个字节。一个字节为8位。上图中展示数据为16进制,所以每两个数即为一个字节。如16进制的 “ca” 转为2进制为 “11001010”
- 名称- 即规范
- 数量- 如父类名占两个字节,数量只允许有一个。而接口名每个占两个字节,允许有多个。具体多少由接口个数所占的两个字节决定。
3. 翻译字节码的整体流程(所需要的表和方法在下面都可以找到)
名称 | 长度 | 字节码 | 意义 |
---|---|---|---|
魔数 | u4 | ca fe ba be | 验证是否是java文件,会在类加载器加载文件时候进行校验 |
次版本号+主版本号 | u4 | 00 00 00 34 | 5、6字节是次版本号,7、8是主版本号。 解析:34是16进制,转换为10进制为52,计算法则=44+版本号。 即为jdk1.8,同样会在类加载时候进行校验 ,低版本号不能运行高版本号代码就是因为这四个字节 |
常量个数 | u2 | 00 39 | 代表常量池长度。 解析:10进制是57.说明有57-1条常量指令,即为class文件中的Constant pool下所有指令 |
常量池表 | 不定 | 常量池常量表 | 常量池包括字面量和符号引用,这里字面量就是表中第一个字节代表的数字。符号引用则包括下面三类常量: 1.类和接口的全限定名(Fully Qualified Name) 2.字段的名称和描述符(Descriptor) 3.方法的名称和描述符。 常量池中数据指令存储形式是表结构,每个表结构都可能不同,占用字节数也可能不同,具体参照下图常量池表(图1,仅以一条常量池指令为例子) |
u1 | 0a | 解析:根据第一个字节0a到(图1)中查找,找到字面量tag为10,表示类中的一个方法。其后还需要两组两个字节的数据 | |
u2 | 00 09 | 根据(图1)接下来两个字节表示CONSTANT_Class_info的索引项,即为方法的类型描述 。 解析:这两个字节十进制为9,从class文件中的常量池部分找到 #9,#9又指向了#50,#50表示java/lang/Object,所以方法返回值类型就是对象 | |
u2 | 00 28 | 根据(图1)这两个字节表示CONSTANT_NameAndType的索引项,即为类型和名称描述符。 解析:这两个字节十进制为40,从class文件中的常量池部分找到 #15,#51。#15表示< iniit >,#51表示<>V | |
... | ... | (图2)此三步具体含义请见下面常量池表结构代表含义 | |
类访问控制权限 | u2 | 00 21 | 类访问控制权限。 解析:参考(图3),这两个字节是ACC_PUBLIC,ACC_SUPER的并集,即0x0001 + 0x0020 = 0x0021;所以ACC_PUBLIC,ACC_SUPER标志位为真,而ACC_FINAL、ACC_INYERFACE、ACC_ABSTRACT、ACC_ANNOTATION、ACC_ENUM等标志为假。 |
类索引 | u2 | 00 04 | 确定类名 解析:这两个字节十进制为4,在常量池中对应 #43 对应类名称MyTest |
父类索引 | u2 | 00 09 | 确定父类名 解析:这两个字节十进制为9,在常量池中对应 #50 对应父类名称java/lang/Object |
接口个数 | u2 | 00 00 | 表示无实现接口 |
接口名 | 无 | 解析:每个接口名占u2,但是此类无接口,不占字节数 | |
域个数 | u2 | 00 03 | 定义了多少域对象,这里表示有3个 |
字段表集合 | 包含以下几项 | 不定 | 1.字段表(field_info)用于描述类或者接口中声明的变量,换言之是对字段的定义。 2.字段(field)包括类变量以及实例级变量,但是不包括在方法内部声明的局部变量。 3.关于描述字段可以包含的信息有:字段的作用域(public、private、protect修饰符)、是实例变量还是类变量(staic)、可变性(final)、并发可见性(volatile)、可否被序列化(transient)、字段数据类型(基本类型、对象、数组)、字段名称。其中,各个修饰符都是布尔值,而字段名字、数据类型,只能引用常量池中的常量来描述 4.参考(图4),查看每个字段所需的描述字段和字节数 |
access_flags | u2 | 00 09 | 字段的修饰符,占两个字节 解析:参考(图3),查询类访问权限表类型为0009 = 0001+0008 = public static |
name_index | u2 | 00 0a | 代表字段的简单名称(指没有类型和参数修饰的方法或字段的名称) 解析:这两个字节10进制为10,参考常量池#10,表示字段名为static_a |
descriptor_index | u2 | 00 0b | 字段的描述符。 解析:这两个字节10进制为11,在常量池中找到#11表示 I,在(图5)中可找到I的映射关系为int,所以该字段类型为int型 |
attributes_count | u2 | 00 00 | 表示这个字段有几个属性。 |
attributes | 表结构 | 无 | 参考(图7)attributes 可以看成一个数组, 数组中的每一项都是一个attribute_info , 每个attribute_info 表示一个属性, 数组中一共有attributes_count个属性。可以出现在filed_info中的属性有三种, 分别是ConstantValue, Deprecated, 和 Synthetic |
方法个数 | u2 | 00 03 | 定义了3个方法 |
方法表集合 | 包含以下几项 | 不定 | 方法的结构和字段结构相同,所以参考(图4)获取方法所需的描述字段和字节数 |
access_flags | u2 | 00 01 | 方法的修饰符,占两个字节 解析:这两个字节10进制为1,参考(图3),表示字段的修饰符为public |
name_index | u2 | 00 0f | 方法的名称,占两个字节 解析:这两个字节10进制为15,在常量池中#15表示初始方法 |
descriptor_index | u2 | 00 10 | 方法的描述符,作用是描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。 解析:这两个字节10进制为16,查找常量池#16为 (I)V,参考(图5)< I >V 代表参数是int,返回值为void |
attributes_count | u2 | 00 01 | 表示这个方法有1个属性。 |
attributes | 表结构 | 附加的属性内容,格式参照(图8) | |
attribute_name_index | 00 11 | 附加的属性内容名称 解析:这两个字节10进制为17,在常量池中代表Code,参考(图7),查看Code释义。当确定attribute_name_index=Code时,其属性可以使用更为详细的(图9) | |
attribute_length | 00 00 00 4f | 附加的属性内容长度 这两个字节10进制为79,表示长度为79个字节 | |
info的具体解析可参看(图6) |
3.1 常量池表(图1)
3.2 常量池表结构代表含义案例(图2)
第一步:根据二进制文件找到表结构数据,找出第一条常量池指令
根据第一个字节,匹配tag为10,两组index,每组2个字节,找到tag #9 #40
1. #9 CONSTANT_Class_info的索引项
2. #40 CONSTANT_NameAndType的索引项
第二步:找到#9号常量 Class #50
1. #50 代表的是 java/lang/Object
第三步:找到#40对应的常量 NameAndType #15 #51
1. #15 代表 < iniit >
2. #51 代表<>V
最终结果:
1. 拼接后为:V java/lang/Object.< init >()
2. 此条指令 就是返回值为V类型的java/lang/Object(Object父类)的init方法
3. 我们可以看到我们的结果和class文件第一条常量池指令注释是一样的
3.3 类访问控制权限表(图3)
3.4 字段/方法属性描述表(图4)
- 字段表修饰符放在access_flags项中,他与类中的access_flags非常类似,都是一个u2的数据类型。而name_index和descriptor_index,他们都是对常量池的引用,分别代表字段的简单名称以及字段和方法的描述符。
- 全限定名:包名加类名。例:在常量池中:org/fenixsoft/TestClass(org.fenixsoft.TestClass),仅仅是把“.”换成了“/”。
- 简单名称:指没有类型和参数修饰的方法或字段的名称,如Test类中inc()方法和m字段的简单名称分别是“inc”和“m”
- 描述符:作用是描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。
3.5 基本数据类型映射表(图5)
3.6 关于方法表的查找方法(图6)
a) 找到访问控制 access_flag 00 01: public
b) 找到简单名字 name_index 00 17: init
c) 找到描述符 descriptor_index 00 18: (I)v
- 翻译过来就是:public void init(int i)
d) 找到 attribute_count 00 01: 代表有一个属性表
e) 对照属性表 attribute_info(u2,u4,u1*length)
f) 找到域名 attribute_name_index 00 11: Code
g) 找到域长度 attribute_length 00 00 00 4f:79个u1
h) 找到最大栈深 max_stacks 00 02:2 max_stacks
i) 找到最大变量数max_locals, 00 02:2 max_locals
- args_size方法的参数有多少个(默认this,如果方法是static,那么就是0)
j) 找到代码行数 code_length 00 00 00 0f:15
k) 对应的字节码 code
l) 异常表长度 exception_table_length 00 00
- 如果没有异常表,可能是:1.debug断点的问题 2.错误日志没有行号
m) 异常表(图10)exception_table
......
n) 行号表(LineNumberTable)
o) 本地变量表(LocalVariableTable) 00 02 :2
1. Start+length :一个本地变量的作用域
2. Start : 几个槽来存储
3. Name : 简单名字
4. Signature伪泛型,泛型擦除的标志
3.7 JVM 规范中预定义的属性(图7)
属性名称 |
使用位置 |
含义 |
---|---|---|
Code |
方法表中 |
Java代码编译成的字节码指令(即:具体的方法逻辑字节码指令) |
ConstantValue |
字段表中 |
final关键字定义的常量值 |
Deprecated |
类中、方法表中、字段表中 |
被声明为deprecated的方法和字段 |
Exceptions |
方法表中 |
方法声明的异常 |
LocalVariableTable |
Code属性中 |
方法的局部变量描述 |
LocalVariableTypeTable |
类中 |
JDK1.5中新增的属性,它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
InnerClasses |
类中 |
内部类列表 |
EnclosingMethod |
类中 |
仅当一个类为局部类或者匿名类时,才能拥有这个属性,这个属性用于表示这个类所在的外围方法 |
LineNumberTable |
Code属性中 |
Java源码的行号与字节码指令的对应关系 |
StackMapTable |
Code属性中 |
JDK1.6中新增的属性,供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Signature |
类中、方法表中、字段表中 |
JDK1.5新增的属性,这个属性用于支持泛型情况下的方法签名,在Java语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数类型(Parameterized Types),则Signature属性会为它记录泛型签名信息。由于Java的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息 |
SourceFile |
类中 |
记录源文件名称 |
SourceDebugExtension |
类中 |
JDK1.6中新增的属性,SourceDebugExtension用于存储额外的调试信息。如在进行JSP文件调试时,无法通过Java堆栈来定位到JSP文件的行号,JSR-45规范为这些非Java语言编写,却需要编译成字节码运行在Java虚拟机汇中的程序提供了一个进行调试的标准机制,使用SourceDebugExtension就可以存储这些调试信息。 |
Synthetic |
类中、方法表中、字段表中 |
标识方法或字段为编译器自动产生的 |
RuntimeVisibleAnnotations |
类中、方法表中、字段表中 |
JDK1.5中新增的属性,为动态注解提供支持。RuntimeVisibleAnnotations属性,用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的。 |
RuntimeInvisibleAnnotations |
类中、方法表中、字段表中 |
JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations相反用于指明哪些注解是运行时不可见的。 |
RuntimeVisibleParameterAnnotations |
方法表中 |
JDK1.5中新增的属性,作用与RuntimeVisibleAnnotations类似,只不过作用对象为方法的参数。 |
RuntimeInvisibleParameterAnnotations |
方法表中 |
JDK1.5中新增的属性,作用与RuntimeInvisibleAnnotations类似,只不过作用对象为方法的参数。 |
AnnotationDefault |
方法表中 |
JDK1.5中新增的属性,用于记录注解类元素的默认值 |
BootstrapMethods |
类中 |
JDK1.7新增的属性,用于保存invokedynamic指令引用的引导方法限定符 |
3.8 字段/方法表的attributes属性(图8)
3.9 字段/方法表的attributes的详细属性(图9)
当确定attribute_name_index=Code时,此表相当于(图8的补充版)