Java 类加载过程

zjun Lv4

类加载过程描述

JVM 类加载过程分为加载链接初始化使用卸载这五个阶段,在链接中又包括:验证准备解析

  • 加载:Java将字节码数据从不同的数据源读取到JVM中,并映射为JVM认可的数据结构(Class对象),这里的数据源可能是各种各样的形态,如jar文件、class文件,甚至是网络数据源等;如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。加载阶段是用户参与的阶段,我们可以自定义类加载器,去实现自己的类加载过程。
  • 链接:这是核心的步骤,简单说是把原始的类定义信息平滑地转化入JVM运行的过程中。这里可进一步细分为三个步骤
    • 验证,这是虚拟机安全的重要保障,JVM 需要核验字节信息是符合 Java 虚拟机规范的(例魔数 0xCAFEBABE,以及版本号等),否则就被认为是 VerifyError,这样就防止了恶意信息或者不合规的信息危害 JVM 的运行,验证阶段有可能触发更多 class 的加载。确保被加载类的正确性,验证字节流是否符合 class 文件规范。
    • 准备,创建类或接口中的静态变量,并初始化静态变量的初始值。但这里的“初始化”和下面的显式初始化阶段是有区别的,侧重点在于分配所需要的内存空间,不会去执行更进一步的JVM指令
    • 解析,在这一步会将常量池中的符号引用(symbolic reference)替换为直接引用。
  • 初始化:这一步真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。
  • 使用:程序代码执行使用阶段。
  • 卸载:程序代码退出、异常、结束等。

双亲委派模型

类加载器

启动类加载器(Bootstrap Class-Loader)

是由 C++ 实现的,它是用来加载 <JAVA_HOME>\jre\lib\rt.jarresources.jar 等 jar 包的。
注意:启动类加载器(Bootstrap ClassLoader)是由 C++ 实现的,而这个 C++ 实现的类加载器在 Java 中是没有与之对应的类的

扩展类加载器(Extension or Ext Class-Loader)

扩展类加载器是用来加载 <JAVA_HOME>\jre\lib\ext 目录下 jar 包的,这就是所谓的 extension 机制。该目录也可以通过设置 “java.ext.dirs”来覆盖。
java -Djava.ext.dirs=your_ext_dir HelloWorld

应用类加载器(Application or App Class-Loader)

应用程序类加载器是用来加载 classpath 也就是用户的所有类的

双亲委派过程

双亲委派模型的执行流程:

  1. 当加载一个类时,会先从应用程序类加载器的缓存里查找相应的类,如果能找到就返回对象,如果找不到就执行下面流程;
  2. 在扩展加载器缓存中查找相应的类,如果能找到就返回对象,如果找不到就继续下面流程;
  3. 在启动类加载器中查询相应的类,如果找到就返回对象,如果找不到就继续下面流程;
  4. 在扩展加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就继续下面流程;
  5. 在应用程序类加载器中查找并加载类,如果能找到就返回对象,并将对象加入到缓存中,如果找不到就返回 ClassNotFound 异常。
    注意:一般“双亲”指的是“父亲”和“母亲”,而在这里“双亲”指的是类加载类先向上找,再向下找的流程就叫做双亲委派模型

双亲委派加载方向

类加载器在加载类时,只能向上递归委托其双亲进行类加载,而不可能从双亲再反向委派当前类加载器来进行类加载。

比如:有两个类 TestA.classA.class, 其中 TestA.class 依赖 A.class ,如果把 TestA.class 放到扩展类加载器的加载路径中 $JAVA\_HOME/jre/lib/ext, 而把 A.class 放在当前目录。当我们运行程序: java TestMain 会抛出 NoClassFoundError: A 的错误。

原因就是上述说的委派加载具有方向性导致的:

  1. 运行 java 命令执行 TestMain 程序时,系统类加载器准备加载 TestMain,根据双亲委派机制,先委派给其双亲进行加载,最后,双亲扩展类加载器在其加载路径中的 TestMain. Jar 中找到了 TestMain. Class,完成了 TestMain 的加载。
  2. TestMain 中依赖了 A,此时,会根据加载了 TestMain 的类加载器:扩展类加载器去加载 A,加载方式根据委托机制递归委托给双亲加载,扩展类加载器的双亲为启动类加载器,在启动类加载器的加载路径中不存在 A,加载失败,此时由扩展类加载器在自己的加载路径中加载 A,也因为加载路径中没有A.class 存在,A.class 存在于系统类加载器的加载路径中,但是扩展类加载器不会再返回去委托系统类加载器进行加载,所以直接抛出加载失败异常,出现了上述的错误。

双亲委派的优缺点

优点

1. 安全

使用双亲委派模型时,用户就不能伪造一些不安全的系统类了,比如 jre 里面已经提供了 String 类在启动类加载时加载,那么用户自定义再自定义一个不安全的 String 类时,按照双亲委派模型就不会再加载用户定义的那个不安全的 String 类了,这样就可以避免非安全问题的发生了。

2. 避免重复加载

当一个类被加载之后,因为使用的双亲委派模型,这样不会出现多个类加载器都将同一个类重复加载的情况了。

缺点

加载 SPI 实现类

比如 JNDI(Java Naming and Directory Interface,Java 命名与目录接口)服务,它的代码由启动类加载器去加载(在 JDK 1.3 时放进 rt.jar),但 JNDI 的目的就是对资源进行集中管理和查找,它需要调用独立厂商实现部部署在应用程序的 classpath 下的 JNDI 接口提供者(SPI, Service Provider Interface)的代码,但启动类加载器不可能“认识”这些代码,这就双亲委派模型的问题,

JDBC 也是同样的问题。

总结

  • 双亲委派模型是和 Java 中多个类加载器(启动类加载器、扩展加载器、应用程序类加载器)的运行规则
  • 通过这个(双亲委派模型)规则可以避免类的非安全问题和类被重复加载的问题。
  • 但它也遇到了一些问题,比如 JNDI 和 JDBC 不能通过这个规则进行加载,它需要通过打破双亲委派的模型的方式来加载。
  • 标题: Java 类加载过程
  • 作者: zjun
  • 创建于 : 2016-02-11 22:05:34
  • 更新于 : 2023-12-11 22:16:05
  • 链接: https://zjun.site/2016/02/bffb0bede084.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论