Java类加载机制

Java类加载机制

  1. 类是怎样被加载的?
  2. 类的加载顺序是什么?

类的加载机制

TODO

详细的流程,看一下《深入理解 Java虚拟机》虚拟机的类加载章节

类加载流程分为一下5个阶段:

每个阶段做什么内容?

  • 加载(Loading)
  • 链接(Linking)
    • 验证(Verification)
    • 准备(Preparation)
    • 解析(Resolution)
  • 初始化(Initialization)

加载是全部加载吗?还是用到哪些加载哪些?

resolution (解析)步骤中的 符号引用是什么?

JDK9之前是一种,JDK9之后,由于引入了平台模块化系统(JPMS),文件结构发生了变化

JVM 是动态加载、链接和初始化类和接口的

加载:通过类和接口的名字找到二进制描述并创建类和接口的过程;

链接:将类和接口合并进jvm运行时状态,使类和接口可以被运行的过程;

初始化:类和接口的初始化包括执行类和接口的初始化方法。

5.1. jvm 如何从 class 文件中获取符号引用的;(运行时常量池的构造)

对比 run-time constant pool 的描述,和 Java虚拟机规范第5章 中表述,可知 the binary representation of a class or interface 基本可以用编译后生成的 class file 代替;

  • 运行时常量池,每个 .class 文件中都会有一个 constant pool表(constant_pool table),

    jvm01constant_pool

    • 运行时常量池是.class文件中的constant_pool表的运行时表示;

    • 存储内容:运行时常量池的功能类似于传统编程语言的符号表(symbol table),尽管它包含的数据范围比典型的符号表更广。

    • 分配和构造时间:每个运行时常量池都是从Java虚拟机的方法区(method area)分配的(§2.5.4)。类或接口的运行时常量池是在Java虚拟机创建类或接口时构造的(§5.3)。

    • 在创建类或接口时,如果构造类或接口的运行时常量池所需要的内存比jvm方法区提供的更多,jvm抛出OOM错误

  • 方法区,jvm中各线程共享,

    • 在jvm启动时创建,逻辑上属于heap的一部分;
    • 方法区类似于常规语言中用于编译代码的存储区,或类似于操作系统进程中的“文本”段(”text” segment)。它存储每个类的结构,如运行时常量池、字段和方法数据,以及方法和构造函数的代码,包括在类和实例初始化以及接口初始化中使用的特殊方法(2.9)。
    • 不要求连续的内存空间,具体的实现方式由各jvm厂商决定,
    • 大小可以是固定的,也可以根据计算需要进行扩展。可以不实现垃圾回收和压缩功能。
  • 堆,jvm中各线程共享,

    • jvm启动时创建;

    • 为所有类实例(对象)和数组(arrays)分配内存的运行时数据区域;

    • 大小与方法区一样,可以是固定的(fixed),也可以根据计算需要进行扩展,同时如果过大也可以收缩,堆的内存同样不需要是连续的。

  • class 文件中的constant_pool表,用于在创建类和接口时构造运行时常量池;

  • 运行时常量池中的所有引用最初都是符号的,这些符号引用都是从 class 文件结构中获得(问题:什么是符号引用?)

  • L ClassName ; ,例如:Ljava/lang/String; -> String

  • [ 字段类型,数组; 例如:[Ljava/lang/String; -> String[]

  • 如果元素类型是基本类型,则由相应的字段描述符表示(4.3.2)。

  • 否则,如果元素类型是引用类型,则用ASCII“L”字符后接元素类型的二进制名称(4.2.1),再接ASCII“;”字符表示。

  • the binary name ,文档中提到的表示,正常类的全限定名为:java.lang.String -> 二进制名称为: java/lang/String, 即将 .分隔符换为/分隔符;

  • 浮点数,不管是 float,double,都遵循 IEEE 754

  • 问题:class 文件中 constant_pool 表示的常量池怎样在运行时使用?都是些字段名,字段类型,方法签名等符号集,无法使用啊?

5.2. 解释了jvm启动时如何进行加载、链接和初始化的过程;

  • Java虚拟机通过使用 bootstrap class loader(5.3.1)创建一个初始类来启动,(该初始类以依赖于实现的方式指定)。然后,Java虚拟机链接初始类,初始化它,并调用公共类方法void main(String[])。该方法的调用驱动所有进一步的执行。组成主方法的Java虚拟机指令的执行可能导致链接(并因此创建)其他类和接口,以及调用其他方法。
  • 在Java Virtual Machine的实现中,可以将初始类作为命令行参数提供。或者,实现可以提供一个初始类,该类设置一个类装入器,然后装入应用程序。初始类的其他选择也是可能的,只要它们与前一段给出的规范一致。

5.3. 类和接口的二进制描述如何被加载的,同时类和接口如何被创建的;

  • 创建类或接口的方式
    1. 在类或方法D的运行时常量池中引用了C,会触发对类或接口C的创建(creation);
    2. 调用了java se平台库中的方法,例如反射,也可能触发对类或接口的创建
  • 类加载器和类的关系?
    • 定义加载器(一个加载器L 直接创建了类C)
    • 初始加载器 (直接定义或者委托加载了一个类)
  • 运行时,一个类或接口由二进制名字和其定义类加载器决定,它的运行时包(package)由包名和这个类的定义类加载器决定;
  • an array class 是被 JVM 直接创建的,并不是通过类加载器加载的。(问题,JVM的哪部分创建的 数组类)
  • 为什么在5.3章节说,只有 bootstrap class loader 和 user-defined class loader 两种类加载器?extension class loader 和 application class loader 去哪里了?
  • jvm 可以在 verification 和 resolution 阶段load a class
  • 定义了类加载过程中遇到异常,如何抛出不同的异常;
  • 一个行为良好的类加载器应该维护三个属性:
    • 给定相同的名称,一个好的类类加载器应该始终返回相同的class对象。
    • 第二点没有看懂,像是在说加载的类是泛型类时的处理方式;?
    • 第三点也没有看懂,提到了用户自定义类加载器的处理方式?

5.3.1. 使用 Bootstrap 类加载器,加载非数组类

5.3.5. 从 class 文件中获取Class

  • 检查二进制内容是满足 ClassFile 结构,否则报错;
  • 检查class文件的jdk版本要求,与机器上安装的jdk不兼容也报错,UnsupportedClassVersionError

jvm 加载、链接、初始化的过程,是分工处理,并不是加载一个方法处理全部,而是每个方法只做该方法的部分,然后交给下一个,依次循环。同时,每个方法中可以检查并调用前面方法来动态处理。

5.4. 链接的过程;

主要是 verify 和 preparation 两个阶段,resolution 是可选阶段。

链接过程会涉及新内存的分配,可能发生 OOM;

链接阶段具体的实现比较灵活,只要满足三个前提即可:

  • 链接前,类或接口需要被完全加载;
  • 初始化前,类或接口需要被完全验证和准备;
  • 链接过程中检测的错误需要被抛出?

验证,确保类或接口的二进制表示在结构上是正确的。?这与load过程中的检查功能不是重复了吗?

  • 不重复,load过程是检查 ClassFile结构,41.和4.8章节的内容,verify 阶段验证4.9章节的内容;

验证可能导致加载额外的类和接口,但不需要对它们进行验证或准备。

  • 问题:谁,在什么阶段对额外触发的load内容进行验证或准备?

preparation

对类或接口的静态字段进行默认值赋值(2.3,2.4章节);(真正的初始化还是在初始化阶段)

准备阶段可以在类创建后的任何时候执行,但是要在初始化前完成;

resolution

Java虚拟机指令anewarray, checkcast, getfield, getstatic, instanceof, invokedynamic, invokeinterface, invokespecial, invokestatic, invokvirtual, ldc, ldc_w, multianewarray, new, putfield,和putstatic 对运行时常量池进行符号引用。这些指令的执行都需要解析其符号引用

上面文本中提到的指令,除 invokedynamic 之外,其他指令解析一次其符号引用即可;

invokedynamic 指令解析的具体值是绑定到具体的 invokedynamic 调用位置的,其他 invokedynamic 需要重新解析;

解析是动态地从运行时常量池中的符号引用确定具体值的过程。

具体值,是内存中可以真实查到的

5.5. 类和接口如何被初始化的;

以下操作会对类或接口C,进行初始化操作:

  • 执行任何引用C的Java虚拟机指令new, getstatic, putstatic或invokestatic (new, getstatic, putstatic, invokestatic)。这些指令通过字段引用或方法引用直接或间接引用类或接口C,则会初始化C。

  • 在类库(2.12)中调用某些反射方法,例如,在类class或包java.lang.reflect中调用。

  • 如果C是一个类,它的一个子类的初始化,则类C也会被初始化;

  • 如果C是一个声明非抽象、非静态方法的接口,则直接或间接实现C的类的初始化。

  • 如果C是一个类,它在Java虚拟机启动时被指定为初始类(5.2)。

因为jvm是多线程的,所以初始化类也需要保证同步(因为不同线程可能会初始化相同的类),所以每个类或接口都有唯一的一个初始化锁LC,具体初始化过程如下:

5.6. 绑定本地方法的概念;

5.6. jvm退出;

在jvm层面,每个java 类的构造函数都是实例初始化的方法。该方法名为 init,由编译器提供。

类加载器

Bootstrap ClassLoader (启动类加载器)

  • 加载 /lib 目录下符合要求的类,如 rt.jar

    是指 jre 的安装目录

Extension ClassLoader (扩展类加载器)

  • 加载 /lib/ext 目录下的类

Application ClassLoader / System ClassLoader (应用类加载器/系统类加载器)

  • 加载用户程序中 classpath 指定的所有 jar 或目录

自定义 ClassLoader

什么是双亲委派模型?

双亲委派过程:如果一个类加载器需要加载类,那么首先它会把这个类加载请求委派给父类加载器去完成,如果父类还有父类则接着委托,每一层都是如此,一直递归到顶层(即启动类加载器),当父类加载器无法完成这个请求(父类加载器中没有加载过这个类)时,子类才会尝试去加载。

  • 一般情况加载会从应用类加载器委托给扩展类加载器,然后再委托给启动类加载器,启动类加载器找不到然后扩展类加载器找,扩展类加载器找不到再由应用类记载器找。

双亲委派解决了什么问题?

在实际应用中,双亲委派解决了 Java 基础类统一加载的问题。

java.lang.Object 来说,加载它经过一层层委托,最终由 Bootstrap ClassLoader 去找 lib目录下 rt.jar 里面的 java.lang.Object 加载到 JVM 中。

这样如果有不法分子自己造一个 java.lang.Object ,里面嵌了不好的代码,如果我们按照双亲委派模型来实现的话,最终加载到 JVM 中的只会是我们 rt.jar 里面的代码,这样核心的基础代码就得到了保护。JVM 中不会出现两个 Object。

违反双亲委派的例子

问题:SPI 怎么违反双亲委派模型了?

我觉得不能叫打破或者违反,而应该是没有依据双亲委派模型实现的类加载方式。

BootstrapClassloader无法委派AppClassLoader来加载类,也就是说BootstrapClassloader中加载的类中无法使用由AppClassLoader加载的类。

这个规则是哪里规定的?

  • 很明显java.sql包是由BootstrapClassloader加载器加载的;而接口的实现类com.mysql.jdbc.Driver是由第三方实现的类库,由AppClassLoader加载器进行加载的,我们的问题是DriverManager再获取链接的时候必然要加载到com.mysql.jdbc.Driver类,这就是由BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖

线程上下文类加载器

1
getCallerClass();

调用类的静态方法会初始化该类?初始化动作做了哪些事情?

当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的 ClassLoader 找到并加载该类。

问题:怎样判断是高层需要加载低层的类?而不是低层本身需要加载该类?

怎样判断一个类应该由哪个类加载器加载?

Class.forName()?

ClassLoader?

类加载顺序

  1. 我们知道,jvm中发现一个类(通过类的package+ClassName来确定唯一)已经被加载了,那么它会直接使用而不会再加载同名的类了
  2. 如何查看jvm启动过程中的类加载顺序?
    1
    2
    在 jvm 启动脚本(用户java程序启动脚本)中,添加 -verbose 参数或者 -XX:+TraceClassLoading,
    日志中就会打印类的加载顺序日志
  3. 那么如果有两个相同的类,如何确认先加载哪个?

参考jdk1.7官方文档

jdk1.8的相同内容的位置

在文档的Understanding class path wildcards章节中写道

1
2
The order in which the JAR files in a directory are enumerated in the expanded class path is not specified and may vary from platform to platform and even from moment to moment on the same machine.
If a specific order is required then the JAR files can be enumerated explicitly in the class path.

上面两段的大意为:在同一个目录下的jar的加载顺序是不能指定的,可能因平台而异,甚至在同一台机器上不同时刻也有所不同。

如果希望指定顺序,那么可以显示的在classpath中枚举jar文件。

同时官方文档在Specification order段落中,还写道:

1
The order in which you specify multiple class path entries is important. The Java interpreter will look for classes in the directories in the order they appear in the class path variable. In the example above, the Java interpreter will first look for a needed class in the directory /java/MyClasses. Only if it doesn't find a class with the proper name in that directory will the interpreter look in the /java/OtherClasses directory.

大意为:当你的classpath中有多个路径时,指定这些路径的顺序很重要,jvm会按照classpath路径中的顺序依次加载其中的类,如果已经找到了需要的类,就不会从后面加载,如果没有才会向后查找。

综上,同一个classpath目录下的jar的加载顺序是无法指定的,但是可以通过在classpath中显示指定jar文件顺序或者目录的顺序来达到指定加载顺序的目的。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 ligongzhao
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信