0 前言
在【JVM学习】3.JVM中的对象一文中,我们给出了一个JVM对象的详细创建流程,整个流程图如下:
本章将会对类的加载过程
及常见的类加载器
做出详细梳理和介绍。
1 类的生命周期
类
从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:
- 加载(Loading)
- 验证(Verification)
- 准备(Preparation)
- 解析(Resolution)
- 初始化(Initialization)
- 使用(Using)
- 卸载(Unloading)
7 个阶段。其中验证
、准备
、解析
3个部分统称为连接
(Linking)。
加载
、验证
、准备
、初始化
和卸载
这五个阶段的顺序是确定的
,但是对于解析
阶段则不一定
,它在某些情况下可以在初始化之后再开始,这样做是为了支持java的运行时绑定特征
(也称为动态绑定
或晚期绑定
)。
1.1 加载
1.1.1 时机
关于在什么情况下需要开始类加载过程的第一个阶段加载
,《Java虚拟机规范》
中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段
,《Java虚拟机规范》则是严格规定了有且只有六种情况
必须立即对类进行初始化
(而加载、验证、准备自然需要在此之前开始):
- 遇到
new
、getstatic
、putstatic
或invokestatic
这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。 - 使用
java.lang.reflect包
的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化。 - 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含
main()方法
的那个类),虚拟机会先初始化这个主类。 - 当使用
JDK 7
新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例
最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
、REF_newInvokeSpecial
四种类型的方法句柄
,并且这个方法句柄对应的类
没有进行过初始化,则需要先触发其初始化。 - 当一个接口中定义了
JDK 8
新加入的默认方法(被default
关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
1.1.2 干什么?
- 通过一个
类的全限定名
来获取定义此类的二进制字节流
- 将这个字节流所代表的
静态存储结构
转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class对象
,作为方法区这个类的各种数据的访问入口。
1.2 连接
1.2.1 验证
验证
是连接阶段的第一步,这一阶段的目的
是
确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当作代码运行后不会危害虚拟机自身的安全。
从整体上看,验证阶段大致上会完成下面四个阶段的检验动作(FMBS
):
1.2.1.1 文件格式验证(File Verification,FV)
- 主要目的:保证输入的
字节流
能正确地解析
并存储
于方法区之内,格式上符合描述一个Java类型
信息的要求。 - 验证点:
- 是否以魔数
0xCAFEBABE
开头。 主、次版本号
是否在当前Java虚拟机接受范围之内。- 常量池的常量中是否有
不被支持
的常量类型(检查常量tag标志)。 - 指向常量的各种索引值中是否有指向
不存在的常量
或不符合类型的常量
。 CONSTANT_Utf8_info
型的常量中是否有不符合UTF-8
编码的数据。- Class文件中各个部分及文件本身是否有
被删除的
或附加的
其他信息。 - ……
- 是否以魔数
1.2.1.2 元数据验证(Metadata Verification,MV)
- 主要目的:对类的
元数据信息
进行语义校验
(对元数据信息中的数据类型校验),保证不存在
与《Java语言规范》定义相悖的元数据信息。 - 验证点:
- 这个类
是否有父类
(除了java.lang.Object
之外,所有的类都应当有父类)。 - 这个类的父类
是否继承
了不允许被继承的类(被final
修饰的类)。 - 如果这个类不是抽象类,
是否实现
了其父类或接口之中要求实现的所有方法
。 - 类中的
字段
、方法
是否与父类产生矛盾(例如覆盖了父类的final
字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)。 - ……
- 这个类
1.2.1.3 字节码验证(Bytecode Verification,BV)
- 主要目的:通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。(对类的方法体(Class文件中的Code属性)进行校验分析)
- 验证点:
- 保证任意时刻
操作数栈的数据类型
与指令代码序列
都能配合工作 - 保证任何
跳转指令
都不会跳转到方法体以外的字节码指令上 - 保证方法体中的类型转换总是有效的
- ……
- 保证任意时刻
1.2.1.4 符号引用验证(Symbol Verification,SV)
最后一个阶段的校验行为发生在虚拟机将符号引用
转化为直接引用
的时候,这个转化动作将在连接的第三阶段——解析阶段
中发生。
- 主要目的:确保解析行为能正常执行,如果无法通过符号引用验证,Java虚拟机将会抛出一个
java.lang.IncompatibleClassChangeError
的子类异常(对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验) - 验证点:
- 符号引用中通过字符串描述的
全限定名
是否能找到对应的类
。 - 在指定类中是否存在符合方法的字段描述符及简单名称所描述的方法和字段。
- 符号引用中的
类
、字段
、方法
的可访问性(private
、protected
、public
、<package>
)是否可被当前类访问。 - ……
- 符号引用中通过字符串描述的
1.2.2 准备
准备阶段是正式为类中定义的变量(即静态变量
,被static
修饰的变量)分配内存并设置类变量初始值的阶段。
注意:
这时候进行内存分配的仅包括
类变量
,而不包括实例变量
,实例变量将会在对象实例化时随着对象一起分配在Java堆
中。
其次是这里所说的初始值“通常情况”下是数据类型的零值
。
“特殊情况”:基本类型的常量
直接赋值,如:public static final int value = 123
,在准备阶段虚拟机就会根据ConstantValue
的设置将value
赋值为123
。
1.2.3 解析
解析阶段是Java虚拟机将常量池
内的符号引用
替换为直接引用
的过程。(动态链接
)
符号引用(Symbolic References):是以一组符号来描述所引用的目标,符号可以是任何形式的
字面量
,只要使用时能无歧义地定位到目标即可。比如
类和接口的全限定名
字段名称和描述符
方法名称和描述符
方法句柄和方法类型
动态调用点和动态常量
- ……
等都是
符号引用
。
直接引用(Direct References):直接引用是可以直接指向目标的
指针
、相对偏移量
或者是一个能间接定位到目标的句柄
。
1.3 初始化
类的初始化阶段是类加载过程的最后一个步骤。其主要工作就是:
根据程序猿通过程序制定的
主观计划
去初始化
类变量和其他资源,或者说:初始化阶段是执行类构造器<clinit>()方法
的过程.
1.4 使用
略。
1.5 卸载
略。
2 类加载器
除了在加载阶段
用户应用程序可以通过自定义类加载器
的方式局部参与外,其余动作都完全由Java虚拟机
来主导控制。
实现类加载的代码即为类加载器
。
类加载器做的就是上面 5 个步骤的事(
加载、验证、准备、解析、初始化
)。
2.1 JDK 提供的三层类加载器
2.1.1 Bootstrap ClassLoader
加载核心类库,也就是 rt.jar
、resources.jar
、charsets.jar
等。当然这些 jar包
的路径是可以指定的,-Xbootclasspath
参数可以完成指定操作。
这个加载器是C++
编写的,随着JVM启动。
2.1.2 Extention ClassLoader
扩展类加载器,主要用于加载lib/ext
目录下的jar包
和.class
文件。同样的,通过系统变量java.ext.dirs
可以指定这个目录。
这个加载器是个Java类
,继承自URLClassLoader
。
2.1.3 Application/System ClassLoader
程序员自己写的Java 类
的默认加载器
。一般用来加载classpath
下的其他所有jar包
和.class
文件,我们写的代码,会首先尝试使用这个类加载器进行加载。
2.1.4 Custom ClassLoader
自定义加载器,支持一些个性化的扩展功能。
2.2 类加载器的问题
如果你在项目代码里,写一个java.lang
的包,然后改写String类
的一些行为,编译后,发现并不能生效。JRE的类当然不能轻易被覆盖,否则会被别有用心的人利用,这就太危险了。
对于任意一个类,都需要由加载它的类加载器
和这个类本身
一同确立其在Java虚拟机中的唯一性
,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:
比较两个类是否“相等”,只有在这两个类是由
同一个类加载器
加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同
,那这两个类就必定不相等
。
这里所指的“相等”,包括代表类的Class对象的equals()
方法、isAssignableFrom()方法
、isInstance()方法
的返回结果,也包括使用instanceof
关键字做对象所属关系判定等情况。
2.3 双亲委派
双亲委派模型
要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器
。
这里类加载器之间的父子关系一般不会以继承
(Inheritance)的关系来实现,而是都使用组合
(Composition)关系来复用父加载器的代码。
2.3.1 工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器
去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
子类加载器加载之前,有限询问父级类加载器是否加载过,父类未加载过在进行加载.
2.3.2 解决的问题
- 类重复加载
- 避免java核心API被篡改
2.3.2 破坏双亲委派模型
2.3.2.1 Tomcat 类加载机制
对于一些需要加载的非基础类
,会由一个叫作WebAppClassLoader
的类加载器优先加载
。等它加载不到的时候,再交给上层的ClassLoader
进行加载。这个加载器用来隔绝不同应用的.class 文件
,比如你的两个应用,可能会依赖同一个第三方的不同版本,它们是相互没有影响的。
如何在同一个JVM
里,运行着不兼容的两个版本,当然是需要自定义加载器
才能完成的事。
那么tomcat
是怎么打破双亲委派机制的呢?可以看图中的WebAppClassLoader
,它加载自己目录下的.class文件
,并不会传递给父类的加载器。
2.3.2.2 SPI
全称是Service Provider Interface
,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
JNDI
已经是JAVA的标准服务,其存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath
下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码。
即:使用线程上下文类加载器
(Thread Context ClassLoader)去加载所需的SPI服务代码,这是一种父类加载器
去请求子类加载器
完成类加载的行为,这种行为实际上是打通了双亲委派模型的层次结构来逆向使用
类加载器,已经违背了双亲委派模型的一般性原则。
2.3.2.3 OSGi
OSGi实现模块化热部署
。其关键是它自定义的类加载器机制
的实现,每一个程序模块(OSGi中称为Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。