文章目录
Why?:
其实,网络上已经有许多有关JVM的干货分享,而且也有许多优质的书籍,例如周志明大佬的《深入理解Java虚拟机》,还有官方发版的《JAVA虚拟机规范》等。但是鄙人每次温故或者解惑的时候,也不会随时随地都有书籍在旁,经常上网查询,而且内容良莠不齐,定位到满足需求文章也浪费时间。所以鄙人就向对技术知识进行具有个人需求特色的归档,同时也温习一下阅读过的优质干货和书籍。
以下内容都是基于《深入理解Java虚拟机》和 Oracle的官方文档JAVA SE 8 虚拟机规范 所总结
探索JVM类加载机制
JVM 将类的字节码数据加载到内存,并对数据进行校验解析和初始化,最终形成可以被运行状态的 JVM 执行的 Class 这一过程称之为类加载机制。注意,这一过程都是在程序运行期间完成的(详见 Loading, Linking, and Initializing)。
在JVM规范中,类加载的过程包含:加载(Loading)- 连接(Linking) - 初始化(Initialization)。而连接又分为验证,准备和解析。
1.加载
Java虚拟机规范中其实该过程为创建与加载,创建主要指类加载器如何委托或定义需加载的类。
加载,查找具有特定名称的类或接口的二进制字节流,并从将该二进制数据加载至内存,并生成一个对应的类或接口。
(1)通过类的全限定名来获取定义该类的二进制字节流(一般指字节码文件)。
(2)将这个字节流锁表示的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生存一个表示该类的 java.lang.Class
对象,作为方法区这个类的访问入口。
类加载器并不需要等待某个类被首次主动使用时再去加载这个类。JVM虚拟机规范中允许类加载器预料某个类将会被使用就预先加载它(预加载),如果在预先加载的过程中遇到了字节码文件缺失或者存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError 错误)。如果这个类一直没有被程序主动使用,那么类加载器就不会报错。
在虚拟机启动时会进行预加载,加载JAVA_HOME/lib/下的rt.jar下的字节码,这个jar包里面的内容是java程序运行时必定会用到的,像java.lang.*
、java.util.*
、java.io.*
等等。
2.连接
连接,将已经加入到内存的类的二进制字节码数据合并到JVM的运行时环境中,并在此期间进行一系列的验证解析。
2.1.验证
字节码文件的详细验证内容可见 Verification of class Files。主要验证内容大致如下:
(1)文件格式验证:魔数、版本号、编码等。
(2)元数据验证:语义校验,不存在与Java语言规范中为定义的元数据信息。
(3)字节码验证:数据流分析和控制流分析,确保代码符合逻辑并不存在不合法的字节码指令。
(4)符号引用验证:将符号引用解析为直接引用时,只会能找到对应的资源如类、方法、字段等。
2.2.准备
为类或接口创建静态字段,并将这些字段初始化为默认值。在此期间不需要执行任何Java虚拟机代码,静态字段的显式初始化是在初始化阶段中类构造器方法<clinit>()
中执行,而不是准备阶段。进行内存分配并进行初始化默认值的仅包括静态字段,不包括实例字段。在特殊情况下,也就是如果字段属性为ConstantValue
属性,也就是常量字段,就会在准备阶段被初始化为常量值,即ConstantValue所指定的值。
java
// 静态字段
// 准备阶段初始化为 0
public static int i = 1;
// 常量字段
// 准备阶段初始化为 1
public static final int i = 1;
2.3.解析
将运行时常量池中的符号引用动态替换为直接引用(具体值)的过程。
在Java 虚拟机规范中,5.4.3. Resolution指出以下字节码指令 anewarray, checkcast , getfield , getstatic , instanceof , invokedynamic , invokeinterface , invokespecial , invokestatic , invokevirtual , ldc , ldc_w , multianewarray , new , putfield, putstatic 在执行之前必须确保符号引用已解析。
2.3.1.类和接口的解析
如果当前类为D,存在一个还未被解析的符号引用N 指向类或接口C 需要去解析。解析过程如下:
(1)如果当前类C 不是数组类型,那么就会由D的定义类加载器(The defining class loader ),通常意义上代指D的类加载器,根据符号引用N 所表示的全限定名去加载C。在加载过程中,仍会按照类加载机制从头到尾走一遍,所以可能会在验证阶段或者其他阶段出现异常,而导致抛出异常。
(2)如果当前类C 是一个数组类型,并且数组元素类型为引用类型即对象,那么就会按照上一步根据D 的定义类加载器,去加载C中的元素类型,然后再由JVM 去生成一个代表该数组维度和元素的数组对象。
(3)以上步骤执行成功,那么表示C 以及完成了类加载过程,并且C 是有效并可用的。但是还会效验C 的访问权限,如果D 不具备对C 的访问权限,则会 throws an IllegalAccessError
。
2.3.2.字段解析
如果当前类为D,要解析一个未被解析过的字段引用,首先解析该字段所属的类或接口C 的符号引用(#2.3.1)。因此,任何在类和接口解析过程中失败出现的异常都可能会导致字段解析失败,并抛出相应异常。如果解析成功,就对C 和其父类进行字段搜索:
(1)如果C 中声明了一个名称和描述符与字段引用匹配的字段,则该声明字段为此次字段搜索的结果。
(2)否则,将会根据C 的继承关系从下到上递归从实现的接口列表及其父接口中搜索字段。
(3)否则,将会根据C 的继承关系从下到上递归从父类中搜索字段。
(4)否则,字段搜索失败,throws a NoSuchFieldError
。
如果字段搜索成功,但是D 不具备该字段的访问权限,则 throws an IllegalAccessError
。
2.3.3.方法解析
此处与上一小节类似,会先解析方法所属类C 的符号引用,如果解析失败,抛出相应异常。如果类C 解析成功则进行方法搜索:
(1)由于在Class文件格式中类的方法和接口方法的符号引用的常量类型定义是分开的,所以如果类C 是一个接口,则会throws an IncompatibleClassChangeError
。
(2)在类C 中搜索是否存在简单名称和描述符都与目标匹配的方法,如果有则返回目标方法的直接引用。
(3)在类C 的父类中递归查找,如果有则返回目标方法的直接引用。
(4)在类C 的实现的接口列表及其父类接口中递归查找,如果有则返回目标方法的直接引用。
此处特别说明一下,在《深入理解Java虚拟机》中有关这一步的说明任务类C是一个抽象类,并抛出异常。但是在JavaSE 8的虚拟机规范中5.4.3.3. Method Resolution 有关这一步的说明如下:
Otherwise, method resolution attempts to locate the referenced method in the superinterfaces of the specified class C:
- If the maximally-specific superinterface methods of C for the name and descriptor specified by the method reference include exactly one method that does not have its
ACC_ABSTRACT
flag set, then this method is chosen and method lookup succeeds.- Otherwise, if any superinterface of C declares a method with the name and descriptor specified by the method reference that has neither its
ACC_PRIVATE
flag nor itsACC_STATIC
flag set, one of these is arbitrarily chosen and method lookup succeeds.- Otherwise, method lookup fails.
- 如果在C中存在最具体的父接口方法与方法引用所指定的名称和描述符都匹配的方法并且没有设置ACC_ABSTRACT标记,那么方法查找成功。
- 否则,如果C中的任意父接口中声明了一个与方法引用所指定的名称和描述符都匹配的方法,并且既没有设置ACC_PRIVATE标记也没有设置ACC_STATIC标记,那么将会主观选择一个方法,查找成功。
- 否则,查找失败。
A maximally-specific superinterface method of a class or interface C for a particular method name and descriptor is any method for which all of the following are true:
- The method is declared in a superinterface (direct or indirect) of C.
- The method is declared with the specified name and descriptor.
- The method has neither its
ACC_PRIVATE
flag nor itsACC_STATIC
flag set.- Where the method is declared in interface I, there exists no other maximally-specific superinterface method of C with the specified name and descriptor that is declared in a subinterface of I.
在C中最具体的父接口方法,需满足特定的方法名称和描述符,并且满足以下所有条件:
- 该方法声明在C的父接口(直接或间接)中
- 该方法能匹配上指定的方法名称和描述符
- 该方法未设置ACC_PRIVATE或者ACC_STATIC标记
- 如果该方法被声明在接口 I 中,那么在 I 的子接口中将不会存在任何其他匹配指定方法名称和描述符的在C中最具体父接口方法。
(5)否则,方法解析失败 throws a NoSuchMethodError
。
如果方法查找成功,但是由于D 不具备该方法的访问权限,则 throws an IllegalAccessError
。
2.3.4.接口方法解析
先解析方法所属接口C 的符号引用,如果解析失败,抛出相应异常。如果接口C 解析成功则进行方法搜索:
(1)如果C不是接口,则会throws an IncompatibleClassChangeError
。
(2)在接口C 中搜索是否存在简单名称和描述符都与目标匹配的方法,如果有则返回目标方法的直接引用。
(3)否则,沿着父接口递归查找,直到 java.lang.Object 类(接口查找范围也包括了Object类中的方法,因为本质上接口就是类),并且目标方法设置了ACC_PUBLIC标记,不包含ACC_STATIC标记。
(4)否则,方法解析失败, throws a NoSuchMethodError
。
3.初始化
类或接口的初始化包括执行其类或接口初始化方法方法。
类或接口最多有一个类或接口初始化方法,并通过调用该方法进行初始化。类或接口的初始化方法具有特殊名称<clinit>()
,不接受任何参数,并且返回类型为void。
<clinit>()
方法是由编译器自动收集类中所有类变量的赋值动作和静态代码块中的语句合并产生的,编译器收集的顺序是由语句出现在源文件中出现的顺序决定的,静态代码块中只能访问到定义在静态代码块之前的变量,定义在它之后的变量,在前面的静态代码块可以赋值,但是不能访问。
java
public class Test {
static {
i = 10;
System.out.println(i); // Illegal forward reference
}
static int i = 0;
}
在Java虚拟机规范中规定,只能有且只有以下六种情况会对类进行初始化:
- 当遇到 new , getstatic , putstatic , or invokestatic 这四条字节码指令时,如果指令中引用的类型还未初始化,会触发该类型初始化。
- new:实例化对象
- getstatic , putstatic:获取或者设置类变量
- invokestatic:调用静态方法
- 使用 java.lang.reflect 包中的方法对类型进行反射调用,如果类型还未初始化,会触发其初始化。
- 如果 java.lang.invoke.MethodHandle 实例的解析结果为
REF_getStatic
,REF_putStatic
,REF_invokeStatic
,REF_newInvokeSpecial
这四种类型中的一种,如果类型还未初始化,会触发其初始化。 - 子类进行初始化,但是其父类还未初始化,会先初始化父类。
- 如果类型继承了一个声明了non-
abstract
, non-static
方法的接口,并且调用了该方法(jdk8中接口新增了default方法),会导致此接口初始化。 - 当虚拟机启动时,指定执行的主类,也就是
main()
所在的类。