类加载器介绍

JVM预定义了三种类加载器:

  • 引导类加载器(Bootstrap),引导类加载器是用 C++ 编写的(所以获取BootstrapClassLoader会获得null,开发者无法直接获取到启动类加载器的引用)。它主要负责核心的类库加载,其加载路径(没有修改情况下)如下所示:
    • <JAVA_HOME>/jre/lib/*.jar
    • <JAVA_HOME>/jre/lib/classes/<br />
  • 扩展类加载器(Extension),扩展类加载器的具体实现类是 sun.misc.Launcher$ExtClassLoader (它属于 Launcher 的内部类,所以有个 $ 符号)。它的类加载路径(没有修改情况下)如下所示:
    • <JAVA_HOME>/jre/lib/ext/
    • C:/WINDOWS/Sun/Java/lib/ext/
  • 应用类加载器(Application),应用类加载器的具体实现类是 sun.misc.Launcher$AppClassLoader ,它也是 Launcher 的内部类。它的加载路径如下所示:

    • 环境变量的CLASS_PATH指定的目录(不使用命令行参数)
    • -Djava.class.path=<path> (或者 -classpath 指定的加载路径,该条会覆盖前面那条 )

      总之,生效优先级: -classpath > -Djava.class.path=<path> > 环境变量的CLASS_PATH

Tips: AppClassLoader 和 ExtClassLoader 都是由 BootstrapClassLoader加载的,因为这两个都在 **rt.jar** 包下的。

自定义类库加载路径

创建一个类 Test4Jar (当然读者可以看各自对的喜好命名,记得要设置包名)

  1. package codeleven;
  2. import java.io.*;
  3. public class Test4Jar {
  4. public static void sayWhichClassLoader() {
  5. // 输出当前这个类的类加载器
  6. System.out.println(Test4Jar.class.getClassLoader());
  7. }
  8. }

Test4Jar 打包为jar包

  1. jar -cvf Test4Jar.jar ./codeleven/Test4Jar.class

编写一个测试程序调用 Test4Jar.sayWhichClassLoader() 方法

  1. import codeleven.Test4Jar;
  2. public class AppTest {
  3. public static void main(String[] args) {
  4. Test4Jar.sayWhichClassLoader();
  5. }
  6. }

接下来会使用上面两个东西,测试三种加载器如何加载自定义类库的方法:

引导加载器加载自定义类库

Test4Jar.jar 放入某个目录(笔者放到 E:\java\ 目录下),随后启动 AppTest
注意 -Xbootclasspath 有三种形式:

  • -Xbootclasspath: 会使用指定的jar包**不能是路径)覆盖**java的核心类库(不推荐,因为每个类库都要手动加到路径里面去)
  • -Xbootclasspath/a:追加 指定jar包/文件夹 给引导类加载器,即先加载java核心类库再加载自定类库
  • -Xbootclasspath/p: 会在 加载java核心类库 **之前** 加载指定jar包/文件夹
    1. # windows powershell
    2. java "-Xbootclasspath/a:E:\java\Test4Jar.jar" AppTest
    输出结果是null,证明是 BootstrapClassLoader (读者可以获取 sun.boot.class.path 确认加载路径)。
    image.png

扩展类加载器加载自定义类库

将Test4Jar.jar放到 JAVA_HOME /lib/ext(或者指定 -Djava.ext.dirs=<path> ,启动 AppTest
注意 -Djava.ext.dirs覆盖原先的扩展类库路径,所以在使用时一般都会指定原来的扩展类库。
另外 -Djava.ext.dirs 参数一定要使用文件夹,单个jar包无效

# windows powershell
java "-Djava.ext.dirs=%JAVA_HOME%\lib\ext;E:\java" AppTest

image.png

应用类加载器加载自定义类库

把Test4Jar.jar放到某个目录下,然后指定 -Djava.class.path=<path> ,启动 AppTest ,就能看到打印出
image.png

总结

如果在开发环境使用到类似 -Djava.ext.dirs 亦或者 -Xbootclasspath 这类参数,一定要注意它是支持文件夹导入还是单个库导入,另外还得注意是否会覆盖原来配置的问题(除了Bootstrap加载器可以指定,后面两个都会覆盖原来的java类库),否则很容易发生找不到java核心类库的错误。

懒加载LazyLoading(Lazy Initializing)

JVM 规范里没有定义何时应该加载字节码,但是规定了遇到以下情况必须加载字节码

  1. 遇到newputstaticgetstaticinvokestatic这四条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。上述四条指令分别对应:创建新对象对静态字段赋值获取静态字段(对final属性无效,因为final属性在编译阶段就在常量池里)调用静态方法 这四个场景。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先触发其初始化
  3. 当初始化一个类时,该类的父类尚未初始化,则先初始化父类
  4. 虚拟机启动时,初始化用户指定执行的 包含main()方法的主类
  5. 当使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果是REF_getstaticREF_putstaticREF_invokestatic的方法句柄,并且这个方法句柄对应的类没有初始化,则触发初始化。

双亲委派模型

双亲委派模型是一种委派关系,内部是使用组合来实现“继承”关系。比如想要加载 XXOO.class 时:

  1. 先去询问 AppClassLoader 是否加载过 XXOO.class ;如果加载过就直接返回,没加载过就去问父类加载器(即 ExtClassLoader )是否加载过;
  2. ExtClassLoader 收到请求,也先去查自己是否加载过 XXOO.class ;如果加载过就直接返回,反之去问父类加载器(即 BootstrapClassLoader )是否加载过;
  3. BootstrapClassLoader 去检查自己是否加载过,加载过就直接返回;没有加载过就返回
  4. BootstrapClassLoader 没加载过, ExtClassLoader 尝试自己去查找(调用 findClass() );找到就返回,没找到就让子加载器去找(即 AppClassLoader )去找
  5. ExtClassLoader 没查找到, AppClassLoader 尝试自己去查找,找到就返回;没找到就抛出 ClassNotFound 异常

双亲委派.jpg
FROM 《百度百科》

常见问题

Q1:如果我引入了两个相同包名、类名但版本不同的包,应用会报错吗?

A1:不会报错,它会按引入的顺序选择加载;比如我 classpath给定1.1版本再给定1.0版本

G:\workspace\test\target\classes>java -classpath ".;C:\Users\Administrator\Desktop\test-overview-1.1-SNAPSHOT.jar;C:\Users\Administrator\Desktop\test-overview-1.0-SNAPSHOT.jar"  cn.codeleven.TestApp

I'm Test Library v1.1
只出现在1.1版本里

--------------------------  -verbose:class ----------------------------------------
[Loaded cn.codeleven.TestLibrary from file:/C:/Users/Administrator/Desktop/test-overview-1.0-SNAPSHOT.jar]
I'm Test Library v1.0
[Loaded cn.codeleven.Just1_1Version from file:/C:/Users/Administrator/Desktop/test-overview-1.1-SNAPSHOT.jar]
只出现在1.1版本里

比如我先给定1.0版本再给定1.1版本

G:\workspace\test\target\classes>java -classpath ".;C:\Users\Administrator\Desktop\test-overview-1.0-SNAPSHOT.jar;C:\Users\Administrator\Desktop\test-overview-1.1-SNAPSHOT.jar"  cn.codeleven.TestApp

I'm Test Library v1.0
只出现在1.1版本里


--------------------------  -verbose:class ----------------------------------------
[Loaded cn.codeleven.TestLibrary from file:/C:/Users/Administrator/Desktop/test-overview-1.1-SNAPSHOT.jar]
I'm Test Library v1.1
[Loaded cn.codeleven.Just1_1Version from file:/C:/Users/Administrator/Desktop/test-overview-1.1-SNAPSHOT.jar]
只出现在1.1版本里

所以引入两个一毛一样的库, JVM 只会根据找到的先后顺序来加载。但是,我们仍要注意,不同版本的库里面使用的类可能也不同,依赖的库也有所不同。
那有的小伙伴可能就问了,那我把所有包都塞进去不就好了吗?

  • 如果开发人员手动把所有依赖都塞进去,常见的异常都是 NoSuchMethodException
    • 举个例子,我现在把 A1_0.jarA1_1.jar 的测试包都加入了项目,但是 A1_0.jar 依赖的是 B1_0.jarA1_1.jar 依赖的是 B1_1.jar ,当启动项目的时候可能就会出现 A1_1.jar 的类本来要使用 B1_1.jar 的类,但是因为顺序使用成了 B1_0.jar 的类,如果这个类因为版本原因少了一些重要的方法,那就会报 NoSuchMethodException ~
  • 如果开发人员是通过 maven 导入依赖的,常见的异常包括 NoSuchMethodExceptionClassNotFoundException 。因为 maven 会自动除掉相同的包~

引用

  1. 深入理解Java类加载器(一):Java类加载原理解析
  2. 你所不知道的Java之ClassLoader并行加载
  3. Working With Classloader By Oracle
  4. Java指令-Djava.ext.dirs的陷阱