一起学Java虚拟机系列:
- 一起学Java虚拟机(一):内存区域和垃圾收集
前言
了解JVM是对Java程序员的基本要求,但是有多少同学和我有一样醉心解bug堆布局,忘记了内功修炼,对JVM的理解是零碎的。系统地学习一次JVM也许能让我们在这条路走得更好更远。
无关性
平台无关性
- “一次编写,到处运行(Write Once,Run Anywhere)”
- 各种不同平台的Java虚拟机,以及所有平台都统一支持的程序存储格式——字节码(Byte Code)
语言无关性
- Java虚拟机不与包括Java语言在内的任何程序语言绑定,它只与“Class文件”这种特定的二进制文件格式所关联,Class文件中包含了Java虚拟机指令集、符号表以及若干其他辅助信息。
- Java的规范拆分成了《Java语言规范》(The Java Language Specification)及《Java虚拟机规范(The Java Virtual Machine Specification)两部分
- 作为一个通用的、与机器无关的执行平台,任何其他语言的实现者都可以将Java虚拟机作为他们语言的运行基础,以Class文件作为他们产品的交付媒介。
Class类文件的结构
Class文件是一组以8个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文
件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数
据,没有空隙存在。当遇到需要占用8个字节以上空间的数据项时,则会按照高位在前(大端表示法(big-endian))的方式分割成若干个8个字节进行存储。
“无符号数”和"表"
根据《Java虚拟机规范》的规定,Class文件格式采用一种类似于C语言结构体的伪结构来存储数
据,这种伪结构中只有两种数据类型:“无符号数”和“表”。
- 无符号数属于基本的数据类型,以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成字符串值。
- 表是由多个无符号数或者其他表作为数据项构成的复合数据类型,为了便于区分,所有表的命名都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个Class文件本质上也可以视作是一张表。
使用010Editor查看class文件结构
通过010Editor
查看HelloWorld.class
public class HelloWorld {
private static final String HELLO_WORLD = "Hello World!";
public static void main(String args[]) {
System.out.println(HELLO_WORLD);
}
}
下载地址:
https://www.sweetscape.com/download/010editor/
下载完成后打开class文件会自动提示安装CLASSAdv.bt插件
魔数
- magic
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为
一个能被虚拟机接受的Class文件,Class文件的魔数取得很有“浪漫气息”,值为0xCAFEBABE
版本号
- minorersion 和 major_version
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version)
,第7和第8个字节是主版本号(Major Version)
每个版本的 JDK 都有自己特定的版本号。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能运行高版本的 Class 文件,即使文件格式没有发生任何变化,虚拟机也拒绝执行高于其版本号的 Class 文件
常量池
- u2 constant_pool_count
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常
量池容量计数值(constant_pool_count)
需要注意的是,常量池的下标是从
1
开始的,也就代表该 Class 文件具有 36
个常量。那么,为什么下标要从 1 开始呢?目的是为了表示在特定情况下 不引用任何一个常量池项,这时候下标就用 0
表示。
- u2 constant_pool
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References),常量池中每一项常量都是一个表。
常量池的数据类型有十几种,各自都有自己的数据结构,但是他们都有一个共有属性 tag
。tag 是标志位,标记是哪一种数据结构。
常见常量池类型:
类 型 | 标 志 | 描 述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
COSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标识方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
CONSTANT_Module_ino | 19 | 表示一个模块 |
CONSTANT_Package_ino | 20 | 表示一个模块开或者导出的包 |
我们再来粗略解析一下常量池的第一项常量
- tag :10 代表类型CONSTANT_Methodref_info,类中方法的符号引用
- classIndex : 6 这个是一个常量池索引, 代表第5个数据项(CONSTANT_Methodref_info 的 class_index 指向的数据项永远是 CONSTANT_Class_info)
- name_and_type_index :23 同样是一个常量池索引
先来看:classIndex:
- tag :7代表CONSTANT_Class_info
- name_index :30 又是一个常量池索引
- 数据项29代表了一个CONSTANT_Utf8_info类型,bytes[16]里是字符串的内容,从 010Editor 解析内容可以看到这个字符串是 java/lang/System,表示类的权限定名
回头在来看第一个常量的 name_and_type_index
- tag : 12 代表CONSTANT_NameAndType_info
- name_index 表示字段或者方法的非限定名,这里的值是
- descriptor_index表示字段描述符或者方法描述符,这里的值是 ()V。
这样,常量池的第一个数据项就分析完了,后面的每一个数据项都可以按照这样分析
javap工具
剩余部分全部手动分析太雷,我们偷个懒。在JDK的bin目录中,Oracle公司已经为我们
准备好一个专门用于分析Class文件字节码的工具:javap
HP-ProDesk-680-G6-PCI-Microtower-PC:~/DEBUG$ javap -verbose HelloWorld.class
Classfile /home/mi/DEBUG/HelloWorld.class
Last modified May 12, 2021; size 641 bytes
MD5 checksum 1910a4531e5743c190636067d43d4bc4
Compiled from "HelloWorld.java"
public class com.wang.javavmdemo.HelloWorld
minor version: 0
major version: 52
flags: (0x0021) ACC_PUBLIC, ACC_SUPER
this_class: #3 // com/wang/javavmdemo/HelloWorld
super_class: #6 // java/lang/Object
interfaces: 0, fields: 1, methods: 2, attributes: 1
Constant pool:
#1 = Methodref #6.#23 // java/lang/Object."":()V
#2 = Fieldref #24.#25 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Class #26 // com/wang/javavmdemo/HelloWorld
#4 = String #27 // Hello World!
#5 = Methodref #28.#29 // java/io/PrintStream.println:(Ljava/lang/String;)V
#6 = Class #30 // java/lang/Object
#7 = Utf8 HELLO_WORLD
#8 = Utf8 Ljava/lang/String;
#9 = Utf8 ConstantValue
#10 = Utf8
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/wang/javavmdemo/HelloWorld;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 SourceFile
#22 = Utf8 HelloWorld.java
#23 = NameAndType #10:#11 // "":()V
#24 = Class #31 // java/lang/System
#25 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#26 = Utf8 com/wang/javavmdemo/HelloWorld
#27 = Utf8 Hello World!
#28 = Class #34 // java/io/PrintStream
#29 = NameAndType #35:#36 // println:(Ljava/lang/String;)V
#30 = Utf8 java/lang/Object
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Ljava/lang/String;)V
{
public com.wang.javavmdemo.HelloWorld();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/wang/javavmdemo/HelloWorld;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #4 // String Hello World!
5: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LineNumberTable:
line 7: 0
line 8: 8
LocalVariableTable:
Start Length Slot Name Signature我
0 9 0 args [Ljava/lang/String;
}
SourceFile: "HelloWorld.java"
类文件的剩余部分,先预览一下:
访问标志
- access_flags
在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或
者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract
类型;如果是类的话,是否被声明为final;等等
标志位及含义表:
标志名称 | 标 志 值 | 含 义 |
---|---|---|
ACC_PUBIC | 0x0001 | 是否为 public 类型 |
ACC_FINAL | 0x0010 | 是否声明为 final |
ACC_SUPER | 0x0020 | JDK1.0.2 之后编译出来的类这个标志都必须为真 |
ACC_INTERFACE | 0x0200 | 是否为接口 |
ACC_ABSTRACT | 0x0400 | 是否为 abstract 类型 |
ACC_SYNTHETIC | 0x1000 | 标记这个类并非由用户代码产生 |
ACC_ANNOTATION | 0x2000 | 是否为注解 |
ACC_ENUM | 0x4000 | 是否为枚举类型 |
ACC_MODULE | 0x8000 | 是否为模块 |
HelloWorld是一个普通类,不是接口、注解、枚举类型或者莫模块,并且被public关键字修饰
因此他的access_flag应该为ACC_PUBIC|ACC_SUPER,转换为10进制就是33
类索引、父类索引与接口索引集合
Class文件中由这三项数据来确定该类型的继承关系。
- this_class类索引用于确定这个类的全限定名。value指向常量池中的索引。
- super_class父类索引用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。
- interfaces_count,表示的是该类实现的接口数量。HelloWorld未实现任何接口所以是0。
- 如果实现了若干接口,这些接口信息将存储在之后的 interfaces[] 之中。接口索引集合就用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。
字段表长度和字段表集合
Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。
- fileds_count 表示该类中声明的变量个数
- filed_info 表示该类中声明的变量信息
private static final String HELLOWORLD = "HelloWorld";
可以看出来,Java中描述一个字段首先是访问范围,是公有的还是私有的,或者受保护的,这个信息决定了字段是否堆特定范围的类可见。
其次是一些关键字修饰的描述信息,是实例变量还是类变量,是否可变,并发可见性,是否可被序列化等,这些关键字包括static、final 、volatile、transient等。
在后面便是字段的数据类型(基本数据类型、数组、对象)和名称。
上述的这些修饰符都是用布尔值来描述的,而数据类型和名称都是不确定的,通常引用常量池的常量来描述。
方法表长度和方法表集合
Class文件存储
格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依
次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表
集合(attributes)几项
方法的定义可以通过访问标志、名称和描述符索引来表述清楚,那么方法中的代码又在哪里呢?
方法里的java代码经过编译器编译成字节码指令后,存放在方法的属性表集合里一个名为“Code”的属性里。
attribute_name_index 对应一个类型为CONSTANT_Utf8_info的常量索引,常量值固定为“Code”
属性表长度和属性表集合
Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息
与class文件其它数据项目严格要求顺序长度不同,属性表集合限制相对比较宽松,不要求各个属性表具有严格顺序,只要不与已有属性名重复,任何人实现的编译器均可向属性表写入自己的属性,jvm运行时会自动忽略掉不认识的属性。
java7中定义的属性如下表:
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量池 |
Deprecated | 类,方法,字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
EnclosingMethod | 类文件 | 仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法 |
InnerClass | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部便狼描述 |
StackMapTable | Code属性 | JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配 |
Signature | 类,方法表,字段表 | 用于支持泛型情况下的方法签名 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
Synthetic | 类,方法表,字段表 | 标志方法或字段为编译器自动生成的 |
LocalVariableTypeTable | 类 | 使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加 |
RuntimeVisibleAnnotations | 类,方法表,字段表 | 为动态注解提供支持 |
RuntimeInvisibleAnnotations | 表,方法表,字段表 | 用于指明哪些注解是运行时不可见的 |
RuntimeVisibleParameterAnnotation | 方法表 | 作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法 |
RuntimeInvisibleParameterAnnotation | 方法表 | 作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数 |
AnnotationDefault | 方法表 | 用于记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokeddynamic指令引用的引导方式限定符 |
关于字段表集合、属性表集合、方法表集合的结构 以及acess_flag的列表
可以像查字典一样查阅《深入理解JAVA虚拟机》6.3小节的内容。我们只需要理解原理。