微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

JVM类加载机制

JVM类加载机制

1. 类加载的时机

一个类从加载到虚拟机内存中开始,到卸载出内存位置,将经历七个阶段。

image-20210916102832896

《Java虚拟机规范》严格规定了有且只有六种必须立即对类进行初始化的场景。

  1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时。
    • 使用new实例化对象时
    • 读取或设置静态字段时
    • 调用静态方法
  2. 使用java.lang.reflect包的方法对类型进行反射调用的时候。
  3. 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先 初始化这个主类。
  5. 当使用JDK 7新加入的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解 析结果为REF_getStatic、REF_putStatic、REF_invokeStatic、REF_newInvokeSpecial四种类型的方法句 柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化。
  6. 一个接口中定义了JDK 8新加入的方法(被default关键字修饰的接口方法)时,如果有 这个接口的实现类发生了初始化,那该接口要在其之前被初始化。

2. 类加载的过程

2.1 加载

在加载阶段,Java虚拟机需要完成三件事情。

这个加载阶段是开发人员在类加载过程中可控性最强的阶段,它既可以由Java虚拟机中内置的引导类加载器来完成,也可以由用户自定义的类加载器完成。

2.2 连接

2.2.1 验证

验证是连接阶段的第一步,目的是检查字节流中的信息符合《Java虚拟机规范》的约束要求。大致上有四个阶段。

  • 文件格式验证。第一阶段要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。
  • 元数据验证。第二阶段是对字节码描述的信息进行语义分析。
  • 字节码验证。第三阶段主要目的是通过数据流分析和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证。符号引用验证可以看作是对类自身以外(常量池中的各种符号引用)的各类信息进行匹配性校验,通俗来说就是,该类是否缺少或者被禁止访问它依赖的某些外部类、方法、字段等资源。
2.2.2 准备

准备阶段是正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。

从概念上讲,这些变量所使用的内存都应当在方法区中进行分配,但必须注意到方法区本身是一个逻辑上的区域。

  • 在JDK 7及之前,HotSpot使用永久代来实现方法区时,实现是完全符合这种逻辑概念的;
  • 而在JDK 8及之后,类变量则会随着Class对象一起存放在Java堆中

image-20210916104411730

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 三层类加载器

image-20210916110416894

  • 启动类加载器(引导类加载器)。这个类加载器负责将存放在 \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 双亲委派机制

image-20210921154505848

上图中类加载器的层次关系成为类加载器的双亲委派模型。双亲委派模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。不过这里类加载器之间的父子关系一般不是以继承(Inheritance)的关系来实现的,而是通常使用组合(Composition)关系来复用父加载器的代码

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

3.3 破坏双亲委派模型

详见《深入理解Java虚拟机》p285

Java历史上出现过三次大规模的双亲委派机制被破坏。

  • 双亲委派机制在JDK1.2被引入,而类加载器在Java的第一个版本就已经存在。为了兼容过去的代码,JDK1.2后的ClassLoader类中添加一个新的protected方法findClass()。如果父类加载失败,会调用自己的findClass()方法完成加载。
  • 第二个问题出现于双亲委派模型天然的缺陷。假如那些非常基础的类需要调用用户代码应该怎么办呢。那么不就是由父类加载器去请求了子类加载器吗。一个典型的例子就是JNDI服务。Java引入了线程上下文类加载器的机制,从父类中继承一个类加载器,加载所需的服务代码
  • 第三个问题是由于用户对程序动态性的追求。出现在Osgi技术中,它可以实现模块化热部署。当收到类加载求时,Osgi按照下面的规则进行类搜索,而不是双亲委派模型。

image-20210916114223958

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐