JVM类加载机制
1. 类加载的时机
一个类从加载到虚拟机内存中开始,到卸载出内存位置,将经历七个阶段。
《Java虚拟机规范》严格规定了有且只有六种必须立即对类进行初始化的场景。
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。
- 使用new实例化对象时
- 读取或设置静态字段时
- 调用静态方法时
- 使用java.lang.reflect包的方法对类型进行反射调用的时候。
- 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类。
- 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
- 当一个接口中定义了JDK 8新加入的默认方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。
2. 类加载的过程
2.1 加载
在加载阶段,Java虚拟机需要完成三件事情。
- 通过一个类的全限定名来获取定义此类的二进制字节流。
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入 口。
这个加载阶段是开发人员在类加载过程中可控性最强的阶段,它既可以由Java虚拟机中内置的引导类加载器来完成,也可以由用户自定义的类加载器完成。
2.2 连接
2.2.1 验证
验证是连接阶段的第一步,目的是检查字节流中的信息符合《Java虚拟机规范》的约束要求。大致上有四个阶段。
- 文件格式验证。第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证。第二阶段是对字节码描述的信息进行语义分析。
- 字节码验证。第三阶段主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
- 符号引用验证。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
2.2.2 准备
准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域。
- 在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;
- 而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中
2.2.3 解析
解析阶段是Java虚拟机将常量池内的符号引用替换为直接引用的过程。
- 符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义定位到目标即可。
- 直接引用是可以直接指向目标的指针、相对偏移量或者是一个能直接定位到目标的句柄。
常见的有类或接口的解析、字段解析、方法解析、接口方法解析。基本遵循一个按继承关系查找的原则。
2.3 初始化
在初始化阶段,会根据程序员在Java代码中制定的主管计划去初始化类变量和其他资源。初始化阶段就是执行类构造器<clinit>()方法的过程。
<clinit>是Javac编译器的自动生成物,它由编译器自动收集类中的所有变量的赋值操作和静态语句块(static{}块)中的语句合并产生的。
3. 类加载器
3.1 概念
Java虚拟机设计团队有意把类加载阶段中的“通过一个类的全限定名来获取描述该类的二进制字节 流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需的类。实现这个动作的代码被称为类加载器(Class Loader)。
Java中任意一个类都必须有一个独立的类加载,类和类加载器是一一对应的。我们说两个类相等,那么加载他们的类加载器一定相等。
3.2 双亲委派机制
Java虚拟机的角度看只有两种类加载器
- 启动类加载器,是虚拟机的一部分,由C++实现。
- 其他所有类加载器,由Java实现,继承自抽象类
java.lang.ClassLoader
。
3.2.1 三层类加载器
- 启动类加载器(引导类加载器)。这个类加载器负责将存放在 \lib目录,或者被-Xbootclasspath参数所指定的路径中存放的,而且是Java虚拟机能够 识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机的内存中。启动类加载器无法被Java程序直接引用,如果需要把加载请求委派给引导类加载器处理,那么直接用null代替即可。
- 扩展类加载器。这个类加载器是在类sun.misc.Launcher$ExtClassLoader 中以Java代码的形式实现的。它负责加载\lib\ext目录中,或者被java.ext.dirs系统变量所指定的路径中所有的类库。
- 应用程序类加载器。这个类加载器由 sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem$ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。它负责加载用户类路径 (ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有 自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
3.1.2 双亲委派机制
上图中类加载器的层次关系成为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码。
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。
3.3 破坏双亲委派模型
详见《深入理解Java虚拟机》p285
Java历史上出现过三次大规模的双亲委派机制被破坏。
- 双亲委派机制在JDK1.2被引入,而类加载器在Java的第一个版本就已经存在。为了兼容过去的代码,JDK1.2后的ClassLoader类中添加了一个新的protected方法
findClass()
。如果父类加载失败,会调用自己的findClass()
方法完成加载。 - 第二个问题出现于双亲委派模型天然的缺陷。假如那些非常基础的类需要调用用户代码应该怎么办呢。那么不就是由父类加载器去请求了子类加载器吗。一个典型的例子就是JNDI服务。Java引入了线程上下文类加载器的机制,从父类中继承一个类加载器,加载所需的服务代码。
- 第三个问题是由于用户对程序动态性的追求。出现在OSGi技术中,它可以实现模块化热部署。当收到类加载求时,OSGi按照下面的规则进行类搜索,而不是双亲委派模型。