用Java实现JVM目录
第零章 用Java实现JVM之随便说点什么
第一章 用Java实现JVM之JVM的准备知识
第二章 用Java实现JVM之解析Class文件
第三章 用Java实现JVM之运行时数据区
第四章 用Java实现JVM之指令集和解释器
第五章 用Java实现JVM之类和对象
第六章 用Java实现JVM之方法调用
第七章 用Java实现JVM之数组和字符串
第八章 用Java实现JVM之本地方法调用
第九章 用Java实现JVM之异常处理
第十章 用Java实现JVM之结束
文章目录
- 用Java实现JVM目录
- 前言
- 一、Class文件结构
- 1.魔数
- 2.版本信息
- 3.常量池
- 4.访问标识、类索引、父类索引、接口
- 5.字段
- 6.方法
- 7.属性
- ConstantValue
- Code
- StackMapTable
- Exceptions
- InnerClasses
- EnclosingMethod
- Synthetic
- Signature
- SourceFile
- SourceDebugExtension
- LineNumberTable
- LocalVariableTable
- LocalVariableTypeTable
- Deprecated
- RuntimeVisibleAnnotations
- RuntimeInVisibleAnnotations
- RuntimeVisibleParameterAnnotations
- RuntimeInVisibleAnnotationsAnnotations
- RuntimeVisibleTypeAnnotations
- RuntimeInvisibleTypeAnnotations
- AnnotationDefault
- BootstrapMethods
- MethodParameters
- 二、类加载
- 三、内存管理
- 四、指令集和执行引擎
- 总结
前言
要想实现一个JVM,我们必须要对它有具体的了解,下列内容主要是来自《Java虚拟机规范.Java SE 8版》、《深入理解Java虚拟机》
接下来我们一起看下JVM的具体细节,用一个小栗子做实验,(接下来的实验都是用HelloWorld编译后的Class文件),代码很简单,如下:
package com.hqd.test;
import java.io.Serializable;
public class HelloWorld implements Cloneable, Serializable {
private static int a = 0;
private int b;
public static void main(String[] args) {
System.out.println("hello world");
}
}
一、Class文件结构
JVM想要正确的解释运行Class文件,则Class文件必须是按规范来的,不然每个人都来个不同规范的Class文件,JVM岂不是要懵逼了,到底要怎么执行。所以先有规范,再有实现
Class文件是一种8位字节的二进制流文件, 各个数据项按顺序紧密的从前向后排列, 相邻的项之间没有间隙, 这样可以使得class文件非常紧凑, 体积轻巧, 可以被JVM快速的加载至内存, 并且占据较少的内存空间。 我们的Java源文件, 在被编译之后, 每个类(或者接口)都单独占据一个class文件, 并且类中的所有信息都会在class文件中有相应的描述, 由于class文件很灵活, 它甚至比Java源文件有着更强的描述能力
(这段是抄的,实在想不出怎么描述,汗),我们先来看下Class文件结构是什么样子的,如下:
再来具体看下Class文件的结构:
注:*_info则表示有层次关系的复合结构的数据,u1代表1个字节,u2代表2个字节,以此类推。。。
类型 | 名称 | 数量 |
---|---|---|
u4 | magic(魔数) | 1 |
u2 | minor_version(次版本号) | 1 |
u2 | major_version(主版本号) | 1 |
u2 | constant_pool_count(常量池长度) | 1 |
cp_info | constant_pool(常量池) | constant_pool_count-1 |
u2 | access_flags(类的访问标识) | 1 |
u2 | this_class(类信息) | 1 |
u2 | super_class(父类信息) | 1 |
u2 | interfaces_count(接口数量) | 1 |
u2 | interfaces(接口信息) | interfaces_count |
u2 | fields_count(字段数量) | 1 |
field_info | fields(字段信息) | fields_count |
u2 | methods_count(方法数量) | 1 |
method_info | methods(方法信息) | methods_count |
u2 | attributes_count(属性数量) | 1 |
attribute_info | attributes(属性信息) | attributes_count |
1.魔数
Class文件的前四个字节放的是魔术,也就是0XCAFEBABE ,是一个固定格式,代表它是一个Class标准文件,如果不是以此为开头的话,JVM会拒绝接受。也就是说魔数实际上是用来标识文件类型的
接下来试验一下
将HelloWorld编译一下,这是个正常的Class文件
随便改一下
2.版本信息
魔数之后,接下来就是版本信息了,也就是minor_version 和 major_version。minor_version 代表次版本,major_version代表主版本。前面两个字节是次版本,后边两个字节是主版本。JDK1.1的版本号为45.0-45.65535(10进制),之后每个大版本发布主版本号加一,如:JDK1.2:46.0~46.65535。下边是JDK主版本号对应表,如下:
JDK版本 | 十进制值 | 十六进制值 |
---|---|---|
JDK 1.1 | 45 | 0x2D |
JDK 1.2 | 46 | 0x2E |
JDK 1.3 | 47 | 0x2F |
JDK 1.4 | 48 | 0x30 |
JDK 1.5 | 49 | 0x31 |
JDK 1.6 | 50 | 0x32 |
JDK 1.7 | 51 | 0x33 |
JDK 1.8 | 52 | 0x34 |
JDK 1.9 | 53 | 0x35 |
JDK 10 | 54 | 0x36 |
JDK 11 | 55 | 0x37 |
JDK 12 | 56 | 0x38 |
JDK 13 | 57 | 0x39 |
JDK 14 | 58 | 0x3A |
JDK 15 | 59 | 0x3B |
JDK 16 | 60 | 0x3C |
JDK 17 | 61 | 0x3D |
注:表格来自维基百科
带次版本号对应表,如下:
JDK版本 | -target参数 | 十进制值 | 十六进制值 |
---|---|---|---|
JDK 1.1.8 | 不能带target参数 | 00 03 00 2D | 45.3 |
JDK 1.2.2 | 不带(默认-target 1.1) | 00 03 00 2D | 45.3 |
JDK 1.2.2 | -target 1.2 | 00 00 00 2E | 46.0 |
JDK 1.3.1_19 | 不带(默认-target 1.1) | 00 03 00 2D | 45.3 |
JDK 1.3.1_19 | -target 1.3 | 00 00 00 2F | 47.0 |
JDK 1.4.2_10 | 不带(默认-target 1.2) | 00 00 00 2E | 46.0 |
JDK 1.4.2_10 | -target 1.4 | 00 00 00 30 | 48.0 |
JDK 1.5.0_11 | 不带(默认-target 1.5) | 00 00 00 31 | 49.0 |
JDK 1.5.0_11 | -target 1.4 -source 1.4 | 00 00 00 30 | 48.0 |
JDK 1.6.0_01 | 不带(默认-target 1.6) | 00 00 00 32 | 50.0 |
JDK 1.6.0_01 | -target 1.5 | 00 00 00 31 | 49.0 |
JDK 1.6.0_01 | -target 1.4 -source 1.4 | 00 00 00 30 | 48.0 |
JDK 1.7.0 | 不带(默认-target 1.7) | 00 00 00 33 | 51.0 |
JDK 1.7.0 | -target 1.6 | 00 00 00 32 | 50.0 |
JDK 1.7.0 | -target 1.4 -source 1.4 | 00 00 00 30 | 48.0 |
看完是不是觉得有点懵,-target和-source 是啥玩意,我们用javac看一下,如下:
这下就清楚了,-target就是生成特定 VM 版本的类文件,-source是提供与指定发行版的源兼容性
注:表格来自《深入理解Java虚拟机》
接下来我们拿我们的HelloWorld程序验证一下,打开HelloWorld.class文件,如下:
这边主版本是十六进制34,也就是JDK8了。高版本的JDK是能向下兼容的,但是低版本的JDK不能运行高版本JDK编译出来的Class文件
接下来试验一下
将HelloWorld.class文件的版本号34改成33,也就是JDK7,发现是可以正常运行的
接下来,将33改成35,也就是JDK1.9。改完之后再运行一下程序,JVM抛给我们一个java.lang.UnsupportedClassVersionError错误
3.常量池
版本信息之后,接下来出场的就是常量池信息。常量池有些麻烦,我们一一来看下吧
常量池其实可以看做缓存一类的东西了,为了避免重复使用导致的不必要的空间浪费,于是做成常量池
常量池中主要存放两大类常量:字面量(Literal)和符号引用(Synbolic Reference).
字面量:比较接近于Java语言层面的常量概念,如文本字符串,声明为final的常量值.
符号引用:包括如下三类常量:
- 类和接口的全限定名(Fully Qualified Name)
- 字段的名称和描述符(Descriptor)
- 方法的名称和描述符
( ̄▽ ̄),这个表述看的有点难理解,我的理解是:字面量应该就是基础类型加上字符串了,而类和方法以及属性的名称和表述符都可以用字符串来表示,所以就做成了符号引用来节省空间。个人理解,如果有错欢迎大家指出
接下来我们来看下字节码文件,前2个字节代表的就是常量池的长度了,我们拿起我们熟悉的的HelloWorld来看下,如下:
可以看这边常量池的长度是十六进制0x00 2c,也就是十进制的44了。这里需要注意一下,常量池第0项是空出来的,代表不引用任何常量池。这个比较特殊,我们拿Object.class文件看一下。如下:
我们都是知道Object是没有父类的,所以这边super_class的常量池引用就是0了。我们再看下HelloWorld.class文件,如下:
HelloWorld.class就是正常的一个常量池引用了
注:这里为了方便所以是用工具看的,为了加深理解才用notepad++
看完常量池长度之后,接下来就是常量内容了,常量池开始都有一个字节的标志位(tag),用来区分类型的。常量池的格式如下:
info的内容是由tag决定,我们再来看下常量池有哪些的类型,如下:
tag标志 | 类型 | 描述 |
---|---|---|
1 | CONSTANT_utf8_info | UTF-8编码的字符串 |
3 | CONSTANT_Integer_info | 整形字面量 |
4 | CONSTANT_Float_info | 浮点型字面量 |
5 | CONSTANT_Long_info | 长整型字面量 |
6 | CONSTANT_Double_info | 双精度浮点型字面量 |
7 | CONSTANT_Class_info | 类或接口的符号引用 |
8 | CONSTANT_String_info | 字符串类型字面量 |
9 | CONSTANT_Fieldref_info | 字段的符号引用 |
10 | CONSTANT_Methodref_info | 类中方法的符号引用 |
11 | CONSTANT_InterfaceMethodref_info | 接口中方法的符号引用 |
12 | CONSTANT_NameAndType_info | 字段或方法的符号引用 |
15 | CONSTANT_MethodHandle_info | 方法句柄 |
16 | CONSTANT_MothodType_info | 标志方法类型 |
18 | CONSTANT_InvokeDynamic_info | 动态方法调用点 |
继续看下每个细项,如下:
常量池类型 | 属性 | 类型 | 描述 |
---|---|---|---|
CONSTANT_utf8_info | tag | u1 | 值为1 |
length | u2 | UTF-8编码字符串占用字节数 | |
bytes | u1 | 长度为length的UTF-8编码的字符串 | |
CONSTANT_Integer_info | tag | u1 | 值为3 |
bytes | u4 | 按照高位在前存储int值 | |
CONSTANT_Float_info | tag | u1 | 值为4 |
bytes | u4 | 按照高位在前存储float值 | |
CONSTANT_Long_info | tag | u1 | 值为5 |
bytes | u8 | 按照高位在前存储long值 | |
CONSTANT_Double_info | tag | u1 | 值为6 |
bytes | u8 | 按照高位在前存储double值 | |
CONSTANT_Class_info | tag | u1 | 值为7 |
name_index | u2 | 指向全限定名常量池索引 | |
CONSTANT_String_info | tag | u1 | 值为8 |
string_index | u2 | 指向字符串字面量索引 | |
CONSTANT_Fieldref_info | tag | u1 | 值为9 |
class_index | u2 | 指向字段所属类或接口描述符CONSTANT_Class_info索引 | |
name_and_type_index | u2 | 指向字段名和类型描述符CONSTANT_NameAndType_info索引 | |
CONSTANT_Methodref_info | tag | u1 | 值为10 |
class_index | u2 | 指向方法所属类或接口描述符CONSTANT_Class_info索引 | |
name_and_type_index | u2 | 指向方法名和类型描述符CONSTANT_NameAndType_info索引 | |
CONSTANT_InterfaceMethodref_info | tag | u1 | 值为11 |
class_index | u2 | 指向接口方法所属类或接口描述符CONSTANT_Class_info索引 | |
name_and_type_index | u2 | 指向接口方法名和类型描述符CONSTANT_NameAndType_info索引 | |
CONSTANT_NameAndType_info | tag | u1 | 值为12 |
name_index | u2 | 指向字段或方法名称索引 | |
descriptor_index | u2 | 指向字段或方法描述符索引 | |
CONSTANT_MethodHandle_info | tag | u1 | 值为15 |
reference_kind | u1 | 方法句柄类型(值范围必须在1-9之内) | |
reference_index | u2 | 根据reference_kind类型变化的索引值(这里有些麻烦,具体参考《Java虚拟机规范.Java SE 8版》) | |
CONSTANT_MothodType_info | tag | u1 | 值为16 |
descriptor_index | u2 | 指向方法描述符索引 | |
CONSTANT_InvokeDynamic_info | tag | u1 | 值为18 |
bootstrap_method_attr_index | u2 | 指向当前ckass文件中引导方法表 | |
name_and_type_index | u2 | 指向接口方法名和类型描述符CONSTANT_NameAndType_info索引 |
有了这些知识,我们再来用我们的HelloWorld.class文件验证一下,如下:
我们在常量池数量之后继续往下看,也就是第10个字节开始,读一个字节,发现是0x0a,也就是十进制的10了。查一下表,就是CONSTANT_Methodref_info,CONSTANT_Methodref_info除了tag还有两个属性class_index、name_and_type_index,继续往下读取,class_index占两个字节,所以是0x00 07,也就是十进制的7。name_and_type_index也占两个字节,所以是0x00 1b,也就是十进制的27。这样我们就读取一个常量了
注:这里就不一个个看下去了,篇幅太长了。但是想要实现JVM的话,常量池是必须得看得懂的
HelloWorld.class文件的常量池读完之后,就在图中标注的位置了
4.访问标识、类索引、父类索引、接口
常量池读取完之后是类的标识符,类的访问标识符也有一个表(大家先别崩溃,这个比常量池简单多了(`・ω・´),很快就能看完),如下:
标志 | 标志值 | 说明 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否是public |
ACC_FINAL | 0x0010 | 是否是final |
ACC_SUPER | 0x0020 | 是否允许invokespecial字节码指令 |
ACC_INTERFACE | 0x0200 | 标志是接口 |
ACC_ABSTRACT | 0x0400 | 标志是抽象类 |
ACC_SYNTHETIC | 0x1000 | 这个类不是由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 标志是一个注解 |
ACC_ENUM | 0x4000 | 标志是枚举 |
注:这个表是类的访问标识,后面还有方法和字段的。表来自《深入理解Java虚拟机》
接下来老样子,拿出我们的HelloWorld.class来看。如下:
我们查看下Class文件结构的表,发现类访问标识符是占2个字节,也就是0x00 21了。我们再查下类访问标识符的表,类的标识就是ACC_SUPER和ACC_PUBLIC了
看完类的访问标识符,我们继续往下看。接下来就是类的全限定名称了。如下:
类名也是占2个字节,所以这里就是十六进制的0x00 06,十进制也是6。注意这边存在名称是常量池索引,也就是在常量池中下标为6的地方了。我们借用classpy工具来验证下,如下:
我们看到常量池下标6的位置是个CONSTANT_Class_info类型,查一下之前的常量池类型的表,发现这里存的也是一个引用,我们继续往下找,如下:
我们看一个CONSTANT_utf8_info类型的字面量,这就是真正的类名了
全限定类名之后就是类的父类了,存的类型和上边的一致。如下:
这里是十六进制0x00 07,十进制也是7。我们再看下常量池,如下:
继续往下看,如下:
这里和上边一致,就不多赘诉了。可以看到,HelloWorld的父类就是Object
看完父类,接下来就是接口了。我们都知道Java可以实现多个接口,所以这边接口是一个数组。首先出现的就是实现接口的数量,如下:
可以看到,这里的接口数量是2,就是我们程序实现的Cloneable和Serializable。这边接口说到底也是个类,所以读取方式还是和上边一致。接下来验证一下,首先是接口的索引,如下:
接口的全限定类名也是占2个字节,这里可以看到就是在常量池8和9的位置,继续往下看,如下:
继续看下常量池36和37的位置,如下:
这里可以看到是Cloneable和Serializable全限定类名,证明我们的想法是对的
5.字段
解析完接口,接下来就是类的属性了。我们都知道Java可以有多个属性,所以先出现的依旧是属性的数量,占2个字节。我们看下HelloWorld.class文件,如下:
可以看到,这里的数量是2,也就是我们HelloWorld程序中的a和b了,验证之前,我们先来看下字段的结构,如下:
类型 | 名称 | 数量 |
---|---|---|
u2 | access_flags(访问标志) | 1 |
u2 | name_index(字段名索引) | 1 |
u2 | descriptor_index(描述符索引) | 1 |
u2 | attributes_count(属性数量) | 1 |
attribute_info | attributes(属性信息) | attributes_count |
字段结构包括访问标志、字段常量池索引,描述符索引以及属性信息。其中属性信息必须是attribute_info 结构的(详情见第7节)。接下来先看下字段的访问标志有哪些,如下:
标志 | 标志值 | 说明 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否是public |
ACC_PRIVATE | 0x0002 | 是否是private |
ACC_PROTECTED | 0x0004 | 是否是protecte |
ACC_STATIC | 0x0008 | 声明是静态 |
ACC_FINAL | 0x0010 | 声明是final |
ACC_VOLATILE | 0x0040 | 声明是volatile,无法缓存 |
ACC_TRANSIENT | 0x0080 | 声明是transient,无法持久化 |
ACC_SYNTHETIC | 0x1000 | 声明该字段由编译器产生 |
ACC_ENUM | 0x4000 | 标志是枚举类型 |
看完标志符,我们再来看下字段描述符表。如下:
FieldType字符 | 类型 | 说明 |
---|---|---|
B | byte | 有符号的字节整数 |
C | char | UTF-16编码 |
D | double | 双精度浮点数 |
F | float | 单精度浮点数 |
I | int | 有符号整数 |
J | long | 有符号长整数 |
L ClassName; | reference | ClassName类实例 |
S | short | 有符号短整数 |
Z | boolean | 布尔值 |
[ | reference | 一个一纬数组 |
有了以上的知识,我们再来看下我们的HelloWorld.class文件,如下:
首先读取标志符(u2)这里十六进制是0x000a,查一下字段标志符表,发现是0x0002,0x0008的和了,也就是标志符是private,static。接下来出现的就是字段名索引和描述符索引,都是占两个字节,十六进制分别是0x000a(字段名索引)、0x000b(描述符索引),这两个指向的是常量池索引,我们借用工具查看一下,如下:
这个字段名是a,描述符是I(int)
接下来一个字段就去展开了,大家自己动手看下(* ̄︶ ̄)
还有个attribute_info,这个是字段的附加属性,具体看7.属性
6.方法
属性解析完,接下来就是方法了。和属性一样,java可以有多个方法,所以首先出现的是方法个数,占2个字节。如下:
这里我们惊奇的发现,竟然有3个方法,明明我们的HelloWorld只有一个main方法来着。其实多出来的两个是编译器帮我们自动生成,也就是<init>和<clinit>方法,借用工具来验证一下,如下:
接下来就是方法数组,每个数组元素如下:
和字段类似,先是访问标识(access_flags),再是方法名称引用(name_index),再是返回值引用(descriptor_index),再是参数个数(attributes_count),最后是附加信息数组
我们先来看下方法访问标志有哪些,如下:
标志 | 标志值 | 说明 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否是public |
ACC_PRIVATE | 0x0002 | 是否是private |
ACC_PROTECTED | 0x0004 | 是否是protecte |
ACC_STATIC | 0x0008 | 声明是静态 |
ACC_FINAL | 0x0010 | 声明是final |
ACC_SYNCHRONIZED | 0x0020 | 声明是同步方法 |
ACC_BRIDGE | 0x0040 | 声明是桥接方法 |
ACC_VARARGS | 0x0080 | 声明方法带变长参数 |
ACC_NATIVE | 0x0100 | 声明方法本地方法 |
ACC_ABSTRACT | 0x0400 | 声明方法是抽象方法 |
ACC_STRICT | 0x0800 | 声明方法是strictfp方法,使用FP-strict浮点模式 |
ACC_SYNCTHETIC | 0x1000 | 声明方法没有出现在源代码中,而是由编译器生成 |
我们再来看下HelloWorld.class文件,这里以mian方法为例。如下:
这里是十六进制00 09,也就是9,0x0001+0x0008的结果,即public、static
name_index和descriptor_index与字段解析方法一致,都是常量池索引,这里就不再赘述了,如下:
对应常量池20和21的位置,如下:
可以看到,就是main方法的描述信息了,再来看下attributes_count,这个表示方法的附加属性的个数,如下:
是十六进制00 01,只有1个
attribute_info这个是方法的附加属性,具体看7.属性
7.属性
接下来又是一块难啃的骨头,不过也是最后一个了,这个说完Class文件结构就完成了,首先来看下有哪些属性类型,如下:
属性 | 使用位置 | 说明 |
---|---|---|
ConstantValue | 字段表 | 虽然jvm规范是static修饰的字段,但是使用idea、javac编译,只有final修饰会生成此属性 |
Code | 方法表 | 存的是方法体的指令(abstract、native方法除外) |
StackMapTable | Code属性 | 提供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
EnclosingMethod | 类文件 | 局部类或者匿名类才有的属性 |
Synthetic | 类、方法、字段 | 标识是否由编译器生成的 |
Signature | 类、方法、字段 | 记录泛型信息 |
SourceFile | 类文件 | 记录源文件名称 |
SourceDebugExtension | 类文件 | 用于存储额外的调试信息 |
LineNumberTable | Code属性 | 源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
LocalVariableTypeTable | Code属性 | 方法的局部变量描述 |
Deprecated | 类、方法、字段 | 被声明为@Deprecated的方法、字段、类 |
RuntimeVisibleAnnotations | 类、方法、字段 | 标注在字段、方法、类上的运行时注解 |
RuntimeInVisibleAnnotations | 类、方法、字段 | 标注在字段、方法、类上的非运行时注解 |
RuntimeVisibleParameterAnnotations | 方法表 | 运行时方法参数注解 |
RuntimeInVisibleAnnotationsAnnotations | 方法表 | 非运行时方法参数注解 |
RuntimeVisibleTypeAnnotations | 类、方法、字段、Code属性 | 记录了标注在对应类声明、字段声明或方法声明所使用的类型上面的运行时可见注解 |
RuntimeInvisibleTypeAnnotations | 类、方法、字段、Code属性 | 记录了标注在对应类声明、字段声明或方法声明所使用的类型上面的非运行时可见注解 |
AnnotationDefault | 方法表 | 记录注解类元素的默认值 |
BootstrapMethods | 类文件 | 用于保存invokedynamic指令引用的引导方法限定符 |
MethodParameters | 方法表 | 用于记录形参有关的信息 |
好了,有关属性类型就上述那些属性了,接下来具体来看看,每个属性的结构吧
ConstantValue
ConstantValue属于定长属性,虽然jvm规范是static修饰的字段,但是使用idea、javac编译,只有final修饰会生成此属性,拿出我们的HelloWorld,改成如下:
import java.io.Serializable;
@Deprecated
public class HelloWorld implements Cloneable, Serializable {
private static int a = 0;
private static final int b = 1;
private final int c = 2;
public static void main(String[] args) {
System.out.println("hello world");
}
}
再来看下class文件,如下:
可以看到,a属性并没有生成ConstantValue,而b、c都生成了
ConstantValue结构,如下:
attribute_name_index 这个属性算是我们的老朋友了,之前经常看到过,即常量池的有效索引,表示字符串ConstantValue
attribute_length 的值固定是2
constantvalue_index 也是常量池的有效索引。该索引指向的常量池位置给出该属性的常量值
Code
Code属于变长属性,存的是方法体生成的指令(abstract、native方法除外)
Code结构,如下:
attribute_name_index 这个属性不多赘述了,即常量池的有效索引,指向字符串Code
attribute_length 给出当前属性的长度,不包括初始的6个字节
max_stack 操作数栈的最大深度,虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度
max_locals 给出局部变量的个数,需要注意的是double和long用两个slot,其他用一个slot
code_length 存储了字节码指令的长度
code[code_length] 存储了具体的字节码指令
exception_table_length 显示异常(受检查的异常)中的个数
exception_table[exception_table_length] 就是每个异常的具体信息了,包含以下几个属性:
- start_pc 表示异常处理器在 code[] 的起始位置
- end_pc 表示异常处理器在 code[] 的结束位置
- handler_pc 表示异常处理器的起点
- catch_type 如果catch_type值不为0,则必须是常量池的有效索引,常量池在该索引处类型,必须是CONSTANT_Class_info结构,表示捕捉的异常类型
文字还是太苍白无力了,接下来验证一下吧,先拿出我们心爱的HelloWorld,改成如下:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.Serializable;
@Deprecated
public class HelloWorld implements Cloneable, Serializable {
private final int a = 0;
private int b;
public static void main(String[] args) {
try {
FileInputStream fis = new FileInputStream("");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
System.out.println("hello world");
}
}
这里只是随便加了个检查时异常,try…catch一下,再来看下字节码,如下:
这下就很清楚了,start_pc和end_pc分别标注try的开始和结束位置(含头不含尾,实际0-9),handler_pc 表示catch起始位置,catch_type 则是异常类型了
attributes_count 返回Code属性的个数
attributes[attributes_count] 属性表,每个元素都是attribute_info类型
StackMapTable
StackMapTable属于变长属性,这个属性用于jvm类型检查验证阶段。提供新的类型检查验证器(Type Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
StackMapTable结构,如下:
attribute_name_index 这个属性就不再做过分的叙述了,指向常量池位置,表示字符串StackMapTable
attribute_length 表示当前属性的长度,不包括之前6个字节
number_of_entries 表示entries表中的成员数量, entries表中每个成员都是stack_map_frame结构
entries[number_of_entries] 每一项表示栈映射帧,这个就不展开了,有兴趣的小伙伴可以具体参阅 《Java虚拟机规范.Java SE 8版》4.7.4
Exceptions
Exceptions属于变长属性,保存方法抛出的异常,即throws关键字后边跟着的异常
Exceptions结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
number_of_exceptions 表示 exception_index_table[]表中的成员数量
exception_index_table[number_of_exceptions] 表中每个成员都是常量池的索引,每个索引处成员必须是CONSTANT_Class_info结构
InnerClasses
InnerClasses属于变长属性,位于ClassFile属性表中,保存了内部类列表,用于记录内部类和宿主类之间的关系
InnerClasses结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
number_of_classes 表示 classes[]表中的成员数量
classes[number_of_classes] 表中每个成员就是每个内部类的具体信息了,成员包含以下几个属性:
- inner_class_info_index 指向常量池的有效索引,常量池在该索引处类型,必须是CONSTANT_Class_info结构,代表内部类的符号引用
- outer_class_info_index 指向常量池中CONSTANT_Class_info类型常量的索引,代表宿主类的符号引用,如果是匿名类或者局部类,此项为0
- inner_name_index 指向常量池中CONSTANT_Utf8_info型常量的索引,代表内部类的名称,如果是匿名内部类,那么这项值为0
- inner_class_access_flags 内部类的访问标志,取值如下:
标志 | 标志值 | 说明 |
---|---|---|
ACC_PUBLIC | 0x0001 | 是否是public |
ACC_PRIVATE | 0x0002 | 是否是private |
ACC_PROTECTED | 0x0004 | 是否是protecte |
ACC_STATIC | 0x0008 | 声明是静态 |
ACC_FINAL | 0x0010 | 声明是final |
ACC_INTERFACE | 0x0200 | 声明是接口 |
ACC_ABSTRACT | 0x0400 | 声明是抽象 |
ACC_SYNTHETIC | 0x1000 | 声明不是由用户代码产生的 |
ACC_ANNOTATION | 0x2000 | 声明是注解类型 |
ACC_ENUM | 0x4000 | 声明是枚举类型 |
EnclosingMethod
EnclosingMethod属于变长属性,局部类或者匿名类才有的属性
EnclosingMethod结构如下:
attribute_name_index 如上
attribute_length 值固定为4
class_index 指向常量池的有效索引,常量池在该索引处类型,必须是CONSTANT_Class_info结构,表示当前类声明的最内层类
method_index 指向常量池索引,常量池在该索引处类型,必须是CONSTANT_NameAndType_info结构,表示内部类所在方法名和方法类型。注:如果内部类处于构造方法、静态构造方法,变量初始化、静态变量初始化中,则method_index为0。拿个例子看一下吧,再掏出我们熟悉的HelloWorld,改成如下代码:
import java.io.Serializable;
@Deprecated
public class HelloWorld implements Cloneable, Serializable {
private final int a = 123;
private int b;
private Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("thread run");
}
};
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread run");
}
});
System.out.println("hello world");
}
}
编译后生成三个类,我们只看HelloWorld$1.class、HelloWorld$2.class,如下:
HelloWorld$1.class
HelloWorld$2.class
很明显的看到,HelloWorld$1.class的method_index指向的是0,而HelloWorld$2.class的method_index指向的是main方法
Synthetic
Synthetic属于定长属性,表示类、方法、属性由编译器生成的,不存在于源代码中
Synthetic结构如下:
attribute_name_index 如上
attribute_length 值固定为0
Signature
Signature属于定长属性,表示类、方法、属性的泛型签名信息
Signature结构如下:
attribute_name_index 如上
attribute_length 值必须为2
signature_index 指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示类签名或方法签名或属性签名
SourceFile
SourceFile属于定长属性,存在于ClassFile结构属性中。用于记录源文件名称
SourceFile结构如下:
attribute_name_index 如上
attribute_length 值必须为2
signature_index 指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示源文件名称
SourceDebugExtension
SourceDebugExtension属于可选属性,存在于ClassFile结构属性中。用于存储额外的调试信息
SourceDebugExtension结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
debug_extension[attribute_length] 用于保存额外的调试信息,对jvm没有实际语义
LineNumberTable
LineNumberTable属于可选变长属性,存在于Code结构属性中。用于存储Java 源码的行号与字节码指令的对应关系
LineNumberTable结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
line_number_table_length 给出line_number_table[] 数组成员个数
line_number_table[line_number_table_length] 每个成员表示Java源码的行号与字节码指令的对应关系,成员包含以下几个属性:
- start_pc 指向code[] 数组的索引
- line_number 指向源码行号
LocalVariableTable
LocalVariableTable属于可选变长属性,存在于Code结构属性中。用于执行方法过程中,确定某个局部变量的值
LocalVariableTable结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
local_variable_table_length 给出local_variable_table[] 数组成员个数
local_variable_table[local_variable_table_length] 每个成员都以偏移量的形式给出code数组的某个范围,当局部变量在此范围内是有值的。此项还会给出当前栈帧的局部变量表中的索引。成员包含以下几个属性:
- start_pc 指向code[] 数组的索引
- length 表示从start_pc - start_pc+length,该局部变量有值
- name_index 指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示局部变量名
- descriptor_index 与name_index一致,用于表示局部变量类型
- index 指向局部变量在当前栈帧的局部变量表中的索引
LocalVariableTypeTable
LocalVariableTypeTable属于可选变长属性,存在于Code结构属性中。用于执行方法过程中,确定某个局部变量的值。注:瞅着和LocalVariableTable差不多,区别就是LocalVariableTypeTable用于存储签名信息,即泛型信息
LocalVariableTypeTable结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
local_variable_type_table_length 给出local_variable_type_table[] 数组成员个数
local_variable_type_table[local_variable_type_table_length] 每个成员都以偏移量的形式给出code数组的某个范围,当局部变量在此范围内是有值的。此项还会给出当前栈帧的局部变量表中的索引。成员包含以下几个属性:
- start_pc 指向code[] 数组的索引
- length 表示从start_pc - start_pc+length,该局部变量有值
- name_index 指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示局部变量名
- signature_index 与name_index一致,用于表示局部变量类型的字段签名
- index 指向局部变量在当前栈帧的局部变量表中的索引
Deprecated
Deprecated属于可选变长属性,存在于类、方法、属性结构属性中,用于声明该项已过时。即被 @Deprecated注解修饰的类、方法、属性
Deprecated结构如下:
attribute_name_index 如上
attribute_length 固定值是0
RuntimeVisibleAnnotations
RuntimeVisibleAnnotations属于变长属性,存在于类、方法、属性结构属性中,用于声明运行时可见的注解信息
RuntimeVisibleAnnotations结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_annotations 表示运行时可见注解个数
annotations[num_annotations] 每一项表示注解的具体信息了,包含以下几个属性::
type_index 指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示注解类型,与当前annotation一样
num_element_value_pairs 表示当前注解键值个数
element_value_pairs[num_element_value_pairs] 每一个元素表示注解中的键值对。成员包含以下几个属性:
- element_name_index 指向常量池的有效索引,表示键值对键名称,与attribute_name_index类似
- value表示键值对值信息,结构如下:
tag表示值类型,取值如下:
value包含以下几个属性:
- const_value_index 使用此项表示值是常量或者String类型的字面量
- enum_const_value使用此项表示值是枚举常量,enum_const_value包含以下两项:
- type_name_index指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示枚举常量二进制名称的内部形式
- const_name_index指向常量池索引,常量池在该索引处类型,必须是CONSTANT_Utf8_info结构,表示枚举常量简单名称 - class_info_index使用此项表示值是类字面量,字面量和类型对应关系:
- 类、接口、数组 => 常量池ObjectType或ArrayType
- 基础类型 => 常量池BaseType字符
- void => 常量池返回描述符V - annotation_value使用此项表示值是注解
- array_value使用此项表示值是数组,array_value包含以下两项:
- num_values 表示当前数组长度
- values 表示数组元素
这个注解也有点复杂,接下来还是用例子说明一下吧,修改HelloWorld,添加一个运行时注解,代码如下:
import java.io.Serializable;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
@AnnoTest(name = "HelloWorld", age = 10)
public class HelloWorld implements Cloneable, Serializable {
private final int a = 123;
private int b;
public static <T> void test(T t) {
System.out.println(t);
}
public static void main(String[] args) {
System.out.println("Hello World");
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE})
@interface AnnoTest {
String name() default "";
int age() default 0;
}
再来看下class文件,如下:
其他几个没啥好说的,主要来看下annotations,如下:
type_index指向的是常量池索引,标明了注解类的类型,num_element_value_pairs标明的是键值对的个数,那什么是键值对呢?实际上键就是注解的方法名,值就是注解标明的值,验证下:
这边的element_name_index,也就是所谓的键,指向常量池的索引,即方法名。再来看下value,上述代码中,我们定义了AnnoTest注解的name(String类型)值为HelloWorld、age(int类型)值为10,value值如下:
可以看到,name的tag值为s,对应tag表格,即String类型,const_value_index指向常量池的索引,即HelloWorld。剩下的age也是类似的,tag值为I,对应int类型,const_value_index也是指向值为10的常量池索引
RuntimeInVisibleAnnotations
RuntimeInVisibleAnnotations属于变长属性,存在于类、方法、属性结构属性中,用于声明运行时不可见的注解信息
RuntimeInVisibleAnnotations结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_annotations 表示运行时不可见注解个数
annotations[num_annotations] 每一项表示注解的具体信息了,元素属性同上
RuntimeVisibleParameterAnnotations
RuntimeVisibleParameterAnnotations属于变长属性,存在于方法参数上,用于声明运行时可见的方法参数注解信息
RuntimeVisibleParameterAnnotations结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_parameters 表示方法参数个数(不管参数有没有注解)
parameter_annotations[num_parameters] 每一项表示参数上的运行时注解信息,结构如下:
- num_annotations 即参数注解个数
- annotations[num_annotations] 每一项表示注解的具体信息了,元素属性同上
RuntimeInVisibleAnnotationsAnnotations
RuntimeVisibleParameterAnnotations属于变长属性,存在于方法参数上,用于声明运行时不可见的方法参数注解信息
RuntimeVisibleParameterAnnotations结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_parameters 表示方法参数个数(不管参数有没有注解)
parameter_annotations[num_parameters] 每一项表示参数上的运行时不可见注解信息,结构如下:
- num_annotations 即参数注解个数
- annotations[num_annotations] 每一项表示注解的具体信息了,元素属性同上
RuntimeVisibleTypeAnnotations
RuntimeVisibleTypeAnnotations属于变长属性,存在于类、方法、字段、Code属性上,用于声明运行时可见的类型注解,(注:类型注解是jdk8的新特性,允许注解出现在更多的位置),具体可以参阅JSR308
RuntimeVisibleTypeAnnotations结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_annotations 表示运行时类型注解个数
annotations[num_annotations] 每一项表示类型注解的具体信息了,元素属性如下:
- target_type即参数注解类型,用来确定使用target_info中的哪一项,target_type取值如下:
- target_info 用来描述注解的具体信息,取值类型如下:
- type_parameter_target 意味着注解添加在泛型类、泛型接口、泛型方法或者泛型构造器第i个类型参数声明上,其结构如下:
- type_parameter_index 指出注解添加在哪个类型参数上,如果为0,则就是第一个参数声明上。文档不大好懂,但是例子简单,修改HelloWorld如下:
- type_parameter_target 意味着注解添加在泛型类、泛型接口、泛型方法或者泛型构造器第i个类型参数声明上,其结构如下:
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE_USE;
public class HelloWorld<A, B, @AnnoTest T> {
private final int a = 123;
private int b;
public static void main(String[] args) {
System.out.println("Hello World");
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE_USE})
@interface AnnoTest {
String name() default "";
int age() default 0;
}
再看下字节码,如下:
type_parameter_index 值为2,说明注解在第三个泛型上
- supertype_target 意味着注解添加在extends或者implements子句里的某个类型上,其结构如下:
- supertype_index 值如果是65535,则表示注解添加在类声明的extends子句中那个超类名称上,除此之外,都是对外围ClassFile结构interfaces数组的索引,要么是在类声明的implements子句中的某个接口上,要么是在接口声明的extends子句中的某个超类接口上。老样子,看个例子就明白了,修改HelloWorld代码如下:
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE_USE;
public class HelloWorld extends @AnnoTest Object implements @AnnoTest(name = "cloneable", age = 1) Cloneable {
private final int a = 123;
private int b;
public static void main(String[] args) {
System.out.println("Hello World");
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE_USE})
@interface AnnoTest {
String name() default "";
int age() default 0;
}
字节码如下:
这样就清楚了,值为65535,即是Object前边的注解,值为0就是Cloneable 前边的注解
-
type_paramter_bound_target 意味着注解添加在泛型类、泛型接口、泛型方法或者泛型构造器第j个类型参数声明中的第i个界限上面,其结构如下:
- type_parameter_index 值指出带有注解那个界限是针对哪一个类型参数声明而言的。如果是0,则表示该界限针对首个参数类型声明
- bound_index 指出由type_parameter_index所确定的那个类型参数声明中,哪一个界限上添加了注解,如果值为0,则表示添加在类型参数声明中的首个界限上巴拉巴拉,完全看不懂。还是拿例子说话吧,修改HelloWorld代码如下:
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE_USE;
public class HelloWorld<A extends @AnnoTest Integer, B extends Number & @AnnoTest Comparable> {
private final int a = 123;
private int b;
public static void main(String[] args) {
System.out.println("Hello World");
}
}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {TYPE_USE})
@interface AnnoTest {
String name() default "";
int age() default 0;
}
再来看下字节码,如下:
可以看到 type_parameter_index即泛型的下标,bound_index 即泛型界限的下标(注:可能存在多重界限)
-
empty_target 意味着注解添加在字段声明、方法返回值类型、新构造器的对象类型、构造器接受者类型上,其结构如下:
-
formal_paramter_target 意味着注解添加在方法、构造器、lambda表达式的形参声明上,其结构如下:
- formal_paramter_index 值指出带有注解类型位于哪一个形参上,如果值为0,则位于首个形参上 -
throws_target 意味着注解添加在方法声明或者构造器声明的throws子句第i个类型上,其结构如下:
- throws_type_index 值指出exception_index_table数组的索引,位于RuntimeVisibleTypeAnnotations属性外围的method_info结构中的Exceptions中 -
localvar_target 意味着注解添加在局部变量声明中,其结构如下:
- table_length 值指出table的个数
- table[table_length] 每一个元素都以字节码code[] 数组中的偏移量来限定某个范围内有效,并指出此局部变量在当前栈帧局部变量表中的索引,元素结构如下:- start_pc 指向code[] 数组的索引
- length 表示从start_pc - start_pc+length,该局部变量有值
- index 指向局部变量在当前栈帧的局部变量表中的索引,long和double则占两个slot,表示index和index+1位置
-
catch_target 意味着注解添加在异常参数声明中第i个类型上,其结构如下:
- exception_table_index 值指出exception_table数组的索引,位于RuntimeVisibleTypeAnnotations属性外围的Code属性中 -
offset_target 意味着注解添加在instanceof表达式类型上面、new表达式类型上面、方法引用表达式的::符号上面,其结构如下:
- offset 值对应code[] 字节码指令数组中的偏移量 -
type_argument_target 意味着注解添加在类型转换器表达式第i个类型上、方法调用参数上、方法引用表达式。结构如下:
- offset 值对应code[] 字节码指令数组中的偏移量
- type_argument_index 参数所在下标,0为第一个
- type_path 用来对复杂类型的补充,比如:多维数组(String[][] @AnnoTest [] strs)、内部类(HelloWorld.Inner.@AnnoTest Inner1 inner)、泛型界限等,取值类型如下:
- path_length 给出path元素个数,如果为0,则注解直接添加到类型本身
- path[path_length] 给出每个元素指出注解所在的精确位置,元素结构如下:- type_path_kind 取值如下:
- type_argument_index 如果type_path_kind值为0、1、2,则type_argument_index 值就是0,如果type_path_kind值为3,则指出带有注解的那个类型参数(注:0为第一个),例子如下:
- type_path_kind 取值如下:
RuntimeInvisibleTypeAnnotations
RuntimeInvisibleTypeAnnotations属于变长属性,存在于类、方法、字段、Code属性上,用于声明运行时不可见的类型注解
RuntimeInvisibleTypeAnnotations结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_annotations 表示运行时不可见类型注解个数
annotations[num_annotations] 每一项表示类型注解的具体信息了,元素属性同上(RuntimeVisibleTypeAnnotations)
AnnotationDefault
AnnotationDefault属于变长属性,存在于某些方法属性上,用于声明注解类型中的元素默认值,即注解类中的方法提供default默认值
AnnotationDefault结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
default_value 同RuntimeVisibleAnnotations中的element_value一致
BootstrapMethods
BootstrapMethods属于变长属性,存在于ClassFile结构中,用于保存invokedynamic指令引用的引导方法限定符,即lambda表达式的代码
BootstrapMethods结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
num_bootstrap_methods 表示bootstrap_methods[] 数组个数
bootstrap_methods[num_bootstrap_methods] 每个元素表示指向CONSTANT_MethodHandle_info结构的索引值,指明了一个引导方法,并指明了有索引组成的序列,此序列的索引指向引导方法的静态参数,元素结构如下:
- bootstrap_method_ref指向常量池CONSTANT_MethodHandle_info结构的索引值
- num_bootstrap_arguments给出bootstrap_arguments数组个数
- bootstrap_arguments[num_bootstrap_arguments] 每个元素指向常量池的有效索引,索引处结构必须是一下结构:CONSTANT_String_info、CONSTANT_Class_info、CONSTANT_Integer_info、CONSTANT_Long_info、CONSTANT_Float_info、CONSTANT_Double_info、CONSTANT_MethodHandle_info、CONSTANT_MethodType_info
MethodParameters
MethodParameters属于变长属性,存在于方法结构中,用于记录形参相关的信息,如形参名称等
MethodParameters结构如下:
attribute_name_index 如上
attribute_length 表示当前属性的长度,不包括之前6个字节
parameters_count 表示parameters[] 数组个数
parameters[parameters_count] 元素结构如下:
- name_index值为0,则表示描述符是个无名称的形参,否则则指向常量池索引
- access_flags取值如下:
总算把Class文件结构说完了,虽然篇幅比较长,但是大部分都是规范性的东西,并不难的,静下心来认真看几遍就能懂了
二、类加载
1、类加载流程
说完Class文件结构,接下来就需要把Class加载进内存,这就涉及到类加载流程了
在此之前,需要先思考一个问题:什么时候会触发类加载呢?按照虚拟机规范里的说明,以下几种情况会触发类加载,如下:
- 遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,字节码指令还没说到(注:第四节会说),这里先简单说下几个指令的含义:new即创建对象时产生的指令、getstatic即获取静态变量时的指令、putstatic即设置静态变量时的指令、invokestatic即调用静态方法时的指令
- 初次调用java.lang.invoke.MethodHanlde实例时
- 使用java.lang.reflect包的反射方法时
- 对类的某个子类进行初始化时,即当一个子类初始化时,会先触发父类初始化
- main方法所在类
接下来重点看一下JVM做了什么,是如何把一个Class文件解析并运行起来的
按照JVM规范主要分为5个阶段:加载=>链接(验证、准备、解析)=>初始化=>使用=>退出。如下图:
注:图来自《深入理解Java虚拟机》
-
加载:这个阶段主要是把Class读取进内存(不一定是在磁盘,也可能是网络或者其他来源),然后转换为对应的数据结构以及在内存中生成Class对象
-
链接:
1)验证:这个阶段主要验证Class文件符不符合JVM规范,是否存在危害JVM的行为。验证阶段主要分为以下几种:文件格式检验
文件格式校验主要是检查Class文件是否符合Class文件规范,比如:是否以魔数开头、版本号是否在指定范围内、常量池类型是否符合规范、常量池索引是否有效等等
元数据验证
元数据验证主要是对字节码描述的语义进行验证,是否符合java的语法规范,比如:是否存在单个父类(除了Object之外)、final类是否存在子类、是否有不符合规范的重写等等
字节码验证
字节码验证主要是对方法的code[] 数组进行验证,保证指令运行时不危害虚拟机,比如:goto到方法体之外的指令上、操作栈是int类型,却按long类型读取等等
符号引用验证
符号引用验证主要是对类自身以外的各类信息进行匹配性校验,通俗地说,该类是否缺少或禁止访问它依赖的某些外部类、方法、字段等。比如:符号引用中通过字符串描述是否能找到对应的类、符号引用中的类、字段、方法是否可以被访问等等
2)准备:这个阶段是对类变量分配内存和初始化的(这边的初始化是指把静态变量赋默认值。比如:int类型赋0,引用类型赋null),如果是final修饰,则会根据ConstatnValue的值进行赋值
3)解析:将常量池中的符号引用替换为直接引用(内存地址)的过程。虚拟机规范并未指出解析的具体时间,只是要求出现以下指令时,触发解析,如下:
解析主要是针对 类、接口、字段、方法、方法类型、方法句柄、调用点限定符解析。简单来说,假如A类方法使用到B类,而在Class文件里,A的Class文件中生成的指令存的是B类的符号引用,即常量池的引用信息,这时候需要把引用信息转换成直接引用(内存地址),还是来看个例子吧,修改HelloWorld为以下代码,如下:
public class HelloWorld {
private static Object b = new Object();
public static void main(String[] args) {
System.out.println(b);
}
}
接下来看下字节码,如下:
可以看到new指令加载的是常量池下标5的位置,这个5并非是运行时的内存地址,jvm运行时是无法使用的,所以需要把这个5(符号引用)转成运行时常量池中记录的内存地址
符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中
直接引用:直接引用可以使直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是与虚拟机实现的内存布局相关的,同一个符号引用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在
- 初始化:在准备阶段时,JVM已经为类变量赋了初始值,在初始化阶段这个阶段调用类的 <clinit> 方法进行类初始化,对静态变量进行赋值,以及执行static代码块,父类的 <clinit> 方法先于子类执行
- 使用:虚拟机运行期间
- 退出:程序结束退出
2、类加载器
既然说完了类加载,顺便把类加载器也说一说吧。类通过类加载器进行加载,如果同一个类有两个不同的类加载器进行加载,那么他们也是不相等的(注:这里的相等指java层面,执行equals、isAssignableFrom等方法时)
说到类加载器,必然就会提到双亲委派模型,相信大伙儿也都有所耳闻,这里还是简单说说吧
什么是双亲委派模型?其实就是一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。如下:
注:图来自《深入理解Java虚拟机》
java主要分为以下三种类加载器,除却这三种,开发也可以自定义类加载器或者是破坏双亲委派模型,这个就不在讨论范围了,这里只看自带的三种类加载器,如下:
- 启动类加载器(Bootstrap ClassLoader)
这个类加载器负责将存放在 <JRE_HOME>/lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中 - 扩展类加载器(Extension ClassLoader)
这个类加载器负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中 - 应用程序类加载器(Application ClassLoader)
这个类加载器负责加载用户类路径(ClassPath)上所指定的类库
三、内存管理
1、运行时数据区
运行时数据区也是老生常谈的问题了,面试时也是热点题目,接下来就来看看吧。JVM的内存划分(jdk8),如下:
除却堆和方法区是线程共享的,其他都是线程私有的,接下来具体来看看每块内存区域的作用吧,如下:
-
程序计数器
这个内存区域是线程私有的,用来记录字节码指令执行的位置,由于java是支持多线程的,如果运行时发生线程切换,需要记录当前线程指令执行的位置,方便之后线程恢复继续执行。假如方法是native方法,则程序计数器 为undefined -
虚拟机栈
用于存储栈帧(Stack Frame)的,栈帧用于存储:局部变量表、操作栈、动态链接、返回地址等信息。java的每个方法执行都会创建一个栈帧,然后压入java虚拟机栈中,java方法的执行的过程就是栈帧入栈出栈的过程 -
本地方法栈
本地方法栈和虚拟机栈作用差不多,只不过虚拟机栈用于执行java方法,而本地方法栈执行native方法 -
堆
这块内存区域可以物理上不连续,只要逻辑上连续即可,是所有线程共享的内存区域。主要存放对象实例,由于对象无法显示回收,所以是GC的主要区域 -
方法区
方法区也是所有线程共享的内存区域。用于存储类的结构信息,如:字段、方法、运行时常量池表等。由于jdk8删除了永久代,所以方法区位于元空间中(这里以HotSpot为例,虚拟机规范并没有规定方法区的具体实现位置) -
运行时常量池
运行时常量池其实就是每一个类加载时,将每个Class文件中的常量池表转换成内存中的结构,就是运行时常量池。位于方法区中 -
直接内存
jvm规范并没有对这一块内存的说明,这里就直接引用 《深入理解Java虚拟机》 的说法了,如下:在jdk1.4中加入了NIO(New Input/Putput)类,引入了一种基于通道(channel)与缓冲区(buffer)的新IO方式,它可以使用native函数直接分配堆外内存,然后通过存储在java堆中的DirectByteBuffer对象作为这块内存的引用进行操作,这样可以在一些场景下大大提高IO性能,避免了在java堆和native堆来回复制数据
2、关于GC
虽然此次jjvm不涉及GC,但还是顺便说说吧,谈谈实现思路
引用算法
要想进行GC,首先得确定哪些对象是存活对象,哪些对象是垃圾。关于这个问题,主要涉及到的算法有两个,如下:
- 引用计数器
引用计数比较简单,即给对象加了个计数器。每当这个对象被引用了,计数器加一,引用失效,计时器减一。当计数器为0时,则为垃圾,可以被回收。引用计数主要存在的问题在于循环引用,假设A对象引用B对象,B对象引用A对象,则引用计数器永远不会为0,也就不会被回收 - 可达性分析
可达性分析通过“GC Roots” 的引用链来判断对象是否存活,如果不在“GC Roots” 的引用链中,则对象不可达,可以被回收。那么那些对象可以作为“GC Roots” 呢? 关于“GC Roots” ,首先当前正在运行的对象是不可以被回收的,也就是方法栈中的栈帧的局部变量表中的对象(两栈:虚拟机栈、本地方法栈),再有就是常量和类静态属性
回收算法
区分开来存活对象和垃圾之后,接下来就得进行回收了。关于回收算法有以下几种,如下:
-
标记-清除算法
标记-清除算法由标记阶段和清除阶段构成。标记阶段是把所有存活对象都做上标记。清除阶段是把那些未标记的对象,也就是垃圾对象进行回收。这个算法的优点就是实现简单,而且不需要移动存活对象。如果要模拟实现,可以优先考虑。当然缺点也很明显,未对内存空间进行整理,会产生小块内存无法利用(碎片化),分配效率也比较慢(需要遍历空闲链表)
-
复制算法
复制算法会把内存分成两块(From和To)大小一致的空间(放不下就尴尬了),每次只使用一块内存空间。当From空间占满之后,将所有存活对象复制到To空间,然后回收整个From空间,To空间变成From空间,如此反复。这个算法的优点就是分配、回收效率高(只需遍历复制存活对象即可),而且不会产生碎片化。缺点就是内存利用率很低,只能使用一半的内存 -
标记-整理算法
标记-整理算法可以看做标记-清理算法的plus版,针对其碎片化问题进行改进。首先还是标记阶段,和标记-清理算法一致,之后就是不一样的地方了,标记-整理算法会对存活对象往内存一端进行移动,然后清理存活对象边界之外的内存。这个算法的优点是堆空间利用率高,缺点就是压缩开销大 -
分代垃圾回收
在上述算法中,可以看到各有优缺点。有没有办法让他们各自发挥其长处,扬长避短呢?答案是肯定的,那就是对象引入年龄的概念,将堆分为新生代和老年代。对于新生代的对象,大部分是“朝生夕死”,所以更适合复制算法(存活对象少,复制的对象就少)。而老年代对象经历过多次GC(年龄增长),存活对象比较多,就用标记-整理算法或者标记-清除算法
安全点和安全区域
除却以上内容,GC时机也是需要注意的一个点,进行垃圾回收的时候都需要STW(stop the world),这时候需要进行可达性分析,就需要对象的引用关系不再发生变化,这样才能正确的进行标记
- 安全点
JVM 会在字节码指令中,选一些指令,作为“安全点”。GC 时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断。主动式中断是设置一个标志,这个标志是中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为 True,就会在自己最近的“安全点”上主动中断挂起 - 安全区域
为什么需要安全区域呢?安全区域其实是对安全点的补充。假如业务线程处于Sleep 或者是 Blocked 状态,无法继续执行,无法到达安全点,对于这种情况,就必须引入安全区域。安全区域是指在某一段代码块中,引用关系不会再发生变化
四、指令集和执行引擎
1、指令集
jvm指令存在于method_info中的Code属性里,指令虽然比较多,但是很多都是类似的功能,接下来看下指令的助记词和分类,如下:
常量
加载
存储
栈
数学
转换
比较
控制
引用
扩展
保留
2、执行引擎
有了指令集之后,要如何执行这些指令呢?要知道这些指令是jvm定义的,CPU可不认识这群家伙,这就需要用到执行引擎了,执行引擎主要有以下几种,如下:
解释器(Interpreter)
- 字节码解释器
对于这个问题,最简单的方式就是用写JVM的语言(C++)去一个个把指令进行翻译运行,这也就是字节码解释器。但是这个实在是太慢了,这也是java前期被诟病的原因 - 模板解释器
针对字节码解释器运行过慢的问题,模板解释器将每一条字节码和一个模板函数相关联,模板函数中直接产生这条字节码执行时的本地机器代码,这样就可以大大提高执行效率了
JIT编译器(Just In Time Compiler)
当 JVM 发现某个方法或代码块执行特别频繁时(热点代码探测),就将其认定为 热点代码(Hot Spot Code),将其字节码直接编译为机器指令执行,从而提升热点代码的执行效率提高性能(对于jit就不展开了,有兴趣的小伙伴自行查阅相关的资料吧)
总结
原本不打算写的,但是不写又无从开始,这种规范性的东西大部分只能从书上来,写了感觉在抄书  ̄へ ̄。既然说到 jvm,又绕不过去,权当梳理和复习了。还是建议大家直接看 《Java虚拟机规范.Java SE 8版》,这才是最官方的、最权威的。如果有错误欢迎大家指出额 (๑>ڡ<)☆