文章目录
前言
在正式进入JVM结构学习之前我们先了解一下
.class
文件的结构。因为在对JVM一无所知的情况下,我们只知道java程序想要运行,一定要编译成class文件,我们先抛开为什么要这要设计,最起码我们找到了入口,对一段程序的解读最重要的不就是入口吗,JVM也是一段程序,我们先从了解其数据来源开始吧!
1. Class文件的总体结构
名称 | 占用字节数 | 数量 | 说明 |
---|---|---|---|
魔数magic | 4字节 | 1 | 无 |
次版本号minor_version | 2字节 | 1 | 无 |
主版本号major_version | 2字节 | 1 | 无 |
常量池大小constant_pool_count | 2字节 | 1 | 常量池入口,容量计数从1开始,如果这里显示9,那么常量的编号为1-8,共8个常量 |
常量池constant_pool | cp_info | constant_pool_count - 1 | 常量池存储了该class文件及其子结构引用的各种常量,包括文字字符串、final声明的变量、类名、方法名。常量池是用一个cp_info结构来描述的,cp_info是一个复合结构,我们会详细说明该符合结构 |
访问方式access_flags | 2字节 | 1 | |
this_class | 2字节 | 1 | |
super_class | 2字节 | 1 | |
interfaces_count | 2字节 | 1 | |
interfaces | 2字节 | interfaces_count | |
fields_count | 2字节 | 1 | |
fields | field_info | fields_count | |
methods_count | 2字节 | 1 | |
methods | method_info | methods_count | |
attributes_count | 2字节 | 1 | |
attributes | attribute_info | attributes_count |
2. 案例
2.2 源代码
public class Test {
private String userName;
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
}
使用javac Test.java
编译,再使用javap -verbose Test.class
反编译
2.3 反编译
Classfile /E:/leetcode/leetcode/editor/cn/Test.class
Last modified 2022-5-30; size 398 bytes
MD5 checksum 9c54ba545f52a6bc3b20ac618cd66d82
Compiled from "Test.java"
public class Test
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#18 // Test.userName:Ljava/lang/String;
#3 = Class #19 // Test
#4 = Class #20 // java/lang/Object
#5 = Utf8 userName
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 getUserName
#12 = Utf8 ()Ljava/lang/String;
#13 = Utf8 setUserName
#14 = Utf8 (Ljava/lang/String;)V
#15 = Utf8 SourceFile
#16 = Utf8 Test.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = NameAndType #5:#6 // userName:Ljava/lang/String;
#19 = Utf8 Test
#20 = Utf8 java/lang/Object
{
public Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public java.lang.String getUserName();
descriptor: ()Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field userName:Ljava/lang/String;
4: areturn
LineNumberTable:
line 5: 0
public void setUserName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field userName:Ljava/lang/String;
5: return
LineNumberTable:
line 9: 0
line 10: 5
}
SourceFile: "Test.java"
2.3.1 major version
主版本号和java版本对应关系
Java 1.2 uses major version 46.
Java 1.3 uses major version 47.
Java 1.4 uses major version 48.
Java 5 uses major version 49.
Java 6 uses major version 50.
Java 7 uses major version 51.
Java 8 uses major version 52.
Java 9 uses major version 53.
2.3.2 minor version
次版本号
2.3.3 flags
access_flags 描述的是**当前类(或者接口)**的访问修饰符, 如public, private等,
此外, 这里面还存在一个标志位, 标志当前的额这个class描述的是类, 还是接口
表示名 | 标志值 | 标志含义 | 针对的对象 |
---|---|---|---|
ACC_PUBLIC | 0x0001 | public类型 | 所有类型 |
ACC_FINAL | 0x0010 | final类型 | 类 |
ACC_SUPER | 0x0020 | 使用新的invokespecial语义 | 类和接口 |
ACC_INTERFACE | 0x0200 | 接口类型 | 接口 |
ACC_ABSTRACT | 0x400 | 抽象类型 | 类和接口 |
ACC_SYNTHETIC | 0x1000 | 该类不由用户代码生成 | 所有类型 |
ACC_ANNOTATION | 0x2000 | 注解类型 | 注解 |
ACC_ENUM | 0x4000 | 枚举类型 | 枚举 |
- ACC_PUBLIC:该class的访问方式为public
- ACC_SUPER:ACC_SUPER 标志用于确定该 Class 文件里面的 invokespecial 指令使用的是哪一种执行语义。
该如何理解使用哪种执行语义呢? 在java1.1之前调用父方法使用的是invokenonvirtual指令,invokenonvirtual不会使用虚函数查找,总是使用静态绑定。
又如何理解总是静态绑定呢? 现有GrandParent、Parent、Child三个类,Parent继承自GrandParent,Child继承自Parent,现GrandParent中有method,Parent中没有重写method,那么Child中调用super.method()一定是指GrandParent的method,这没有任何问题,但是当Parent中重写method后,仅重新编译Parent,Child调用super.method(),仍然调用的GrandParent中的method!因为没有对Child进行编译,此时其使用invokenonvirtual指令调用父方法,已经静态绑定为使用GrandParent的method。
在java1.1之后调用父方法使用invokespecial,会去寻找最近的超类方法。
ACC_SUPER就是用来区分调用父方法使用的是invokenonvirtual还是invokespecial。java 1.0.2版本不会设置ACC_SUPER并且遇到ACC_SUPER时会自动忽略,调用父方法使用invokenonvirtual,使用静态绑定;java1.1版本之后总是会设置为ACC_SUPER,以支持一个正确的调用。
2.3.4 常量池⭐
我们这里说的常量池是指静态常量池,存在于class文件当中。
2.3.4.1 什么是常量?
常量是指在程序的整个运行过程中值保持不变的量。
2.3.4.2 常量池范围?
- 字面量:类的全限定路径、类名、字段名、方法名、常量字段的字面量、方法入参的全限定路径
- 符号引用:Methodref、Fieldref、Class等。
2.3.4.3 常量池中常量种类
- 字面量
1.1 字符串常量:不仅仅包括
1.2 final常量
1.3 整数型常量 - 符号引用
2.1 类和接口的全限定名
2.2 字段的名称和描述符
2.3 方法的名称和描述符
补充:为什么将符号引用当作常量?
因为在编译阶段,JVM不会保存各个方法和字段的最终内存布局信息,因此需要一个常量临时代表内存布局信息,这就是符号引用。符号引用就是用一组符号来表示要引用的目标,符号可以是任何形式的字面量,只要能够无歧义的定位到目标即可。
只有在将类加载到JVM后进行动态链接,将字段、方法的符号引用经过运行期转换转换为直接引用,才能正常使用。
2.3.4.4 看懂常量池
-
字面量
1.1 字符串常量public String s = "abc";
中的"abc"
和s
都为常量,其中"abc"
为字符串常量。1.2 被final修改的变量(静态变量、成员变量、局部变量)。因为该“变量”在编译时值就确定了且不能更改!
public final int i = 257;
中的257
、i
和类型int并记录为I
都为常量,其中257
为字面量。
应当注意,public int i2 = 233;
中的233
并不会被放到常量池,常量池中仅保留其类型int并记录为I
,和变量名i2
,字面量233
并不会保存在常量池。 -
符号引用
2.1 类和接口的全限定名Class类型
#4 = Class #20 // java/lang/Object
2.2 字段的名称和描述符
如果是成员变量则类型为Fieldref,记录变量和类型,如果是局部变量,则常量池中仅仅保存字段名,类型为Utf8
#2 = Fieldref #3.#18 // Test.userName:Ljava/lang/String;
2.3 方法的名称和描述符
#11 = Utf8 getUserName // 方法名
#12 = Utf8 ()Ljava/lang/String; // 参数类型+返回值
可知getUserName方法的参数为空,返回值为String类型。
2.3.4.5 读懂常量池
我们还是以上述的常量池进行讲解。
Constant pool:
#1 = Methodref #4.#17 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#18 // Test.userName:Ljava/lang/String;
#3 = Class #19 // Test
#4 = Class #20 // java/lang/Object
#5 = Utf8 userName
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 getUserName
#12 = Utf8 ()Ljava/lang/String;
#13 = Utf8 setUserName
#14 = Utf8 (Ljava/lang/String;)V
#15 = Utf8 SourceFile
#16 = Utf8 Test.java
#17 = NameAndType #7:#8 // "<init>":()V
#18 = NameAndType #5:#6 // userName:Ljava/lang/String;
#19 = Utf8 Test
#20 = Utf8 java/lang/Object
- methodref:是CONSTANT_Methodref_info类型的简写,表示类的方法引用表类型。该类型的结构如下:
CONSTANT_Methodref_info{
u1 tag = 10; // 当虚拟机访问到常量池中一个数据项的第一个字节为0x0A,那么就代表该数据项是methodref类型
u2 class_index; // 指向此方法的所属类
u2 name_type_index; // 指向此方法的名称和类型
}
如#1
的方法是java.lang.Object
类的名称为<init>
的无参数()
无返回值V
的方法。
- Fieldref:是CONSTANT_Fieldref_info类型的简写,表示字段引用表类型。该类型的结构如下:
CONSTANT_Fieldref_info{
u1 tag = 9; // // 当虚拟机访问到常量池中一个数据项的第一个字节为0x09,那么就代表该数据项是fieldref类型
u2 class_index; // 指向此字段的所属类
u2 name_type_index; // 指向此字段的名称和类型
}
如#2
表示的字段是Test
类下的一个名为userName的类型为String类型的字段。
- Class是CONSTANT_Class_info类型的简写,表示对类和接口的符号引用,该类型的结构如下:
CONSTANT_Class_info{
u1 tag = 7; // // 当虚拟机访问到常量池中一个数据项的第一个字节为0x07,那么就代表该数据项是Class类型
u2 name_index; // 指向此字段的全限定名,一个 CONSTANT_Utf8_info类型的值
}
如#4
表示的类是全限定名为java/lang/Object
的类。
- Utf8是CONSTANT_Utf8_info类型的简写,字符串的字段值、字段名、方法名、方法的参数和返回值类型、全限定类名都是使用该类型表示,该类型的结构如下:
CONSTANT_Utf8_info{
u1 tag = 1; // // 当虚拟机访问到常量池中一个数据项的第一个字节为0x01,那么就代表该数据项是Utf8类型
u2 length; // 表示存储的byte数组长度
u1 bytes[length]; //字节类型存储
}
由于Utf8类型的length是u2类型,占两个字节,所以最多能存储65535个字节。
- 我们看到
#9
和#10
有两个非常奇怪的字符串,分别为Code
和LineNumberTable
,这个与方法区有关,我们稍后再看。
2.3.5 方法区⭐
我们可以看到上述类中有三个方法,我们选取一个方法作为案例讲解:
public void setUserName(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: aload_1
2: putfield #2 // Field userName:Ljava/lang/String;
5: return
LineNumberTable:
line 9: 0
line 10: 5