《深入理解JVM》第七章 虚拟机类加载机制
第六章 类文件结构
第六章只写了一些简单的笔记,其实后面第七章的笔记也很简单= =
6.1 概述
- 计算机只能识别0和1
6.2 无关的基石
- Java的一个口号:一次编写,到处运行。
- JVM不仅可以编译java运行java,而且可以讲其他语言编译成.class文件然后通过JVM来运行,例如Jython,JRuby,Groovy等。
- 实现语言无关的基础任然是虚拟机和字节码存储格式。
6.3 类文件结构
Class文件是一组由8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件中,没有任何空隙。
根据JVM规范的规定,Class文件格式采用类似结构体的伪结构来存储数据,并且只存在两种数据类型:
- 无符号数
无符号数分别以u1、u2、u4、u8来代表1,2,4,8个字节。
- 表
表是一种复合数据类型。
6.3.1 魔数与Class文件版本
- 一个很有意思的事情:每一个.class文件的开头4个字节都是0xCAFEBABE
- 紧接着魔数后面的第5,6个字节是次版本号,第7,8个字节是主版本号
以我以下java版本为例, 我前8个字节是:CAFEBABE00000037
java 11.0.11 2021-04-20 LTS JRE 18.9 (build 11.0.11+9-LTS-194) JVM 18.9 (build 11.0.11+9-LTS-194, mixed mode)
6.3.2 常量池
紧接着次主版本号的后面是常量池入口,常量池的数量不是固定的,所以在最开始设置了一个u2类型的数constant_pool_count
来代表常量池的数量。
每一项常量池开头都是一个u1类型的标志字段
跳过….
6.3.3 访问标志
在常量池结束之后,紧接着有2个字节代表访问标志(access_flags)
6.3.4 ….
- this_class
- super_class
- interfaces_count
- fidlds_count
- methods_count
- method_info[]
- attributes_count
- struct attribute_info
6.4 字节码指令简介
我会个屁!跳过
第七章 虚拟机类加载机制
7.1 概述
Java类型的加载、连接和初始化都是在程序运行期间完成了。Java里天生可以动态扩展的语言特性就是依赖运行期间动态加载和动态连接的这个特点实现的。
例如:如果面写一个面向接口的应用程序,可以等到运行时再指定其实际的实现类;用户可以通过Java预定义和自定义加载器,让一个本地的应用程序可以在运行时从网络或其他地方加载一个二进制流作为程序代码的一部分。
7.2 类加载的时机
类从被加载到虚拟机内存中开始,到卸载出内存为止,它的生命周期包括:加载(Loading)、验证(Vertification)、准备(preparation)、解析(Resolution)、初始化(Inittialization)、使用(Using)、卸载(Unloading)7个阶段。其中验证、准备、解析3个部分统称为连接(Linking)。
主动引用
类初始化的5个时机(有且仅有这5个时机会触发类的初始化):
- 使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译器把结果放入常量池的静态字段除外)的时候、以及调用一个类的静态方法的时候。
- 使用
java.lang.reflect
包对类进行反射调用时,如果类没有被初始化,则需要先触发类的初始化。 - 当初始化一个类的时候,要先初始化其父类。
- 虚拟机启动时,指定的一个执行主类(包含 mian()方法的那个类)需要先进行初始化。
- 当使用JDK1.7的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后解析结果REF_getStatic
、REF_putStatic
、REF_invokeStatic
的方法句柄是,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
被动引用
以上5种行为称为对类的主动引用,当然还有被动引用:
第一种情况:
public class test {
public static void main(String[] args) {
System.out.println(SubClass.name);
}
}
class SuperClass {
static {
System.out.println("Super is initializing...");
}
static int name = 1;
}
class SubClass extends SuperClass{
static {
System.out.println("Sub is initializing....");
}
}
以上代码只会打印父类的静态代码块,说明通过引用父类的静态字段,不会导致子类的初始化。
第二种情况:
public class test {
public static void main(String[] args) {
MyArray[] a = new MyArray[5];
}
}
class MyArray {
static {
System.out.println("I am initializing");
}
}
以上代码不会触发MyArray类的静态代码块,这种情况没有触发其类的初始化阶段。
这里涉及到数组类的初始化过程,这里的代码触发了另一个叫
[LMyArray;
类的初始化,这并不是一个合法的类名,他是由虚拟机自动生成的,直接继承于java.lang.object
的子类,创建动作由字节码指令newarray触发。
第三种情况:
public class test {
public static void main(String[] args) {
System.out.println(SubClass.name);
}
}
class SuperClass {
static {
System.out.println("Super is initializing...");
}
static final int name = 1;
}
class SubClass extends SuperClass{
static {
System.out.println("Sub is initializing....");
}
}
上面的代码只比第一种情况多了一个final关键字,但是并不会触发子类和父类的初始化方法。
这是因为虽然在Java源码中引用了SuperClass中的常量name,但是其实在编译阶段通过常量传播优化,已经将
name = 1
储存到了SubClass类的常量池中,以后SubClass对常量SuperClass的引用实际都被转化为SubClass类对自身常量池的引用了。也就是说,实际上SubClass的class文件之中并没有SuperClass类的符号引用入口,这两个类在编译成Class之后就不存在任何联系了。
7.3 类的加载过程
7.3.1 加载
“加载”是“类加载”过程的一个阶段,这两个词不要混淆了。
具体动作:
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据的访问入口。
7.3.2 验证
验证是连接阶段的第一步,这一步的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式检验
- 是否以魔数0xCAFEBABE开头。
- 主次版本号是否在虚拟机的处理范围。
- 常量池的标志是否支持(检查tag 标志)。
- 指向常量的各种索引值是否指向不存在的常量或不符合类型的常量。
- CONSTANT_Utf8_info 型常量是否有不符合UTF-8编码规范的数据。
- Class文件的各个部分是否有被删除或者添加的其他信息。
- ……
- 元数据验证第二阶段主要是对类的元数据信息进行语义校验,保证不存在不符合Java语言规范的元数据信息。
- 这个类是否有父类(除了java.lang.Object类,其他类都应该有父类)
- 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
- 类如果不是抽象类,是否实现了其父类或接口之中要求实现的方法。
- 类中的字段、方法是否与父类产生矛盾(final字段、重载、参数、返回值、类型等)
- ……
- 字节码验证主要目的是通过数据流和控制流分析,确定程序语义是合法的,符合逻辑的。
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作。
- 保证跳转指令不会跳转到方法体以外的字节码指令上。
- 保证方法体中的类型转换是有效的。
- ……
- 符号引用验证发生在虚拟机将符号引用转化为直接引用的时候
- 符号引用中通过字符串描述的全限定名是否能找到对应的类。
- 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段。
- 类、字段、方法和访问性是否可以被当前类访问。
- ……
7.3.3 准备
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。(仅包括类变量[static])
7.3.4 解析
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用(Symbolic References)是以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用(Direct References)可以直接指向目标指针、相对偏移量或是一个能间接定位到目标的句柄。
7.4 类加载器(ClassLoader)
虚拟机设计团队把类加载阶段(7.3.1)中的“通过一个类的全限定名来获取定义此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。
7.4.1 类和类加载器
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性。所以类加载器并不单纯的只是起到加载一个类到内存中的作用。
在比较两个类是否”相等”的时候,只有在这两个类是由同一类加载器加载的前提下才有意义,这里的”相等”,包括代表类的Class对象的equal()
方法、isAssignableFrom()
方法、isInstance()
方法所返回的结果,也包括使用instanceof
关键字所判定的结果。如果没有注意到类加载器的影响,有时候得到的结果可能会产生迷惑性。
7.4.2 双亲委派机制
从Java虚拟机的角度来讲,只存在2中类加载器:启动类加载器(Bootstrap ClassLoader),以及其他类加载器。(这句话怎么感觉有点废话)
从开发角度,类加载器可以分的比较细致,绝大部分程序都会用到以下三种类加载器。
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用程序加载器(Application ClassLoader)我们的应用程序一般都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器,这些类加载器之间的关系一般如图所示:
如图所示的类加载器之间的层次关系称为双亲委派模型(Parents Delegation Model)。双亲委派模型要求除了顶层的类加载器都应当有自己的父类加载器。如果一个类加载器收到了类加载的请求,他们不会自己来加载这个类,而是委派给自己的父类加载器去完成,仅当父类加载器不能加载的时候才尝试自己来加载。
在LoadClass()方法逻辑里,如果自己的父类加载器加载失败,则会调用自己的findClass()方法来完成加载。
双亲委派机制的代码如下(jdk1.8.0_92)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
7.4.3 破坏双亲委派机制
以下介绍之前比较大规模的三次破坏双亲委派机制的情况。
1. 重写loadClass()
如果看了上面的代码,大概也就清楚如何通过重写loadClass方法来破坏双亲委派机制了。因所以在JDK1.2后提倡用户通过重写findClass来实现自定义ClassLoader并且符合双亲委派机制。
2. 线程上下文类加载器
例如JNDI的这类服务,可能会打破甚至逆转双亲委派机制来加载类。JNDI的代码是通过启动类加载器来加载的,但是这类服务不可避免的要加载到用户定义的代码,那该怎么办呢?为了解决这个问题,Java设计团队提出了如下解决方案:
线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread
类的setContextClassLoader()方法进行设置,如果创建线程时还未被设置,则会从父线程中继承一个,如果在应用程序的全局范围内都没设置过的话,那这个类加载器默认是应用程序类加载器。
3. OSGi模型
在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展成更加复杂的网状结构,当收到类加载请求时,OSGi讲按照下面的顺序进行类搜索:
- 将以
java.*
开头的类委派给父类加载器加载。 - 否则,将委派列表名单内的类委派给父类加载器加载。
- 否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
- 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
- 否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
- 否则,查找Dynamic Import列表的Bundle,委派给对应的Bundle的类加载器 加载。
- 否则,类查找失败。
——-也许未完待续——–