类加载器介绍
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 (当然读者可以看各自对的喜好命名,记得要设置包名)
package codeleven;
import java.io.*;
public class Test4Jar {
public static void sayWhichClassLoader() {
// 输出当前这个类的类加载器
System.out.println(Test4Jar.class.getClassLoader());
}
}
将 Test4Jar
打包为jar包
jar -cvf Test4Jar.jar ./codeleven/Test4Jar.class
编写一个测试程序调用 Test4Jar.sayWhichClassLoader()
方法
import codeleven.Test4Jar;
public class AppTest {
public static void main(String[] args) {
Test4Jar.sayWhichClassLoader();
}
}
接下来会使用上面两个东西,测试三种加载器如何加载自定义类库的方法:
引导加载器加载自定义类库
将 Test4Jar.jar
放入某个目录(笔者放到 E:\java\
目录下),随后启动 AppTest
注意 -Xbootclasspath
有三种形式:
- -Xbootclasspath:
会使用指定的jar包**(不能是路径)覆盖**java的核心类库(不推荐,因为每个类库都要手动加到路径里面去) - -Xbootclasspath/a:
会追加 指定jar包/文件夹 给引导类加载器,即先加载java核心类库再加载自定类库 - -Xbootclasspath/p:
会在 加载java核心类库 **之前** 加载指定jar包/文件夹
输出结果是null,证明是# windows powershell
java "-Xbootclasspath/a:E:\java\Test4Jar.jar" AppTest
BootstrapClassLoader
(读者可以获取sun.boot.class.path
确认加载路径)。
扩展类加载器加载自定义类库
将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
应用类加载器加载自定义类库
把Test4Jar.jar放到某个目录下,然后指定 -Djava.class.path=<path>
,启动 AppTest
,就能看到打印出
总结
如果在开发环境使用到类似 -Djava.ext.dirs
亦或者 -Xbootclasspath
这类参数,一定要注意它是支持文件夹导入还是单个库导入,另外还得注意是否会覆盖原来配置的问题(除了Bootstrap加载器可以指定,后面两个都会覆盖原来的java类库),否则很容易发生找不到java核心类库的错误。
懒加载LazyLoading(Lazy Initializing)
JVM
规范里没有定义何时应该加载字节码,但是规定了遇到以下情况必须加载字节码:
- 遇到
new
、putstatic
、getstatic
、invokestatic
这四条字节码指令时,如果类没有进行过初始化,则需先触发其初始化。上述四条指令分别对应:创建新对象、 对静态字段赋值、 获取静态字段(对final属性无效,因为final属性在编译阶段就在常量池里)、 调用静态方法 这四个场景。 - 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则先触发其初始化
- 当初始化一个类时,该类的父类尚未初始化,则先初始化父类
- 虚拟机启动时,初始化用户指定执行的 包含main()方法的主类
- 当使用JDK7的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果是REF_getstatic
、REF_putstatic
、REF_invokestatic
的方法句柄,并且这个方法句柄对应的类没有初始化,则触发初始化。
双亲委派模型
双亲委派模型是一种委派关系,内部是使用组合来实现“继承”关系。比如想要加载 XXOO.class
时:
- 先去询问
AppClassLoader
是否加载过XXOO.class
;如果加载过就直接返回,没加载过就去问父类加载器(即ExtClassLoader
)是否加载过; ExtClassLoader
收到请求,也先去查自己是否加载过XXOO.class
;如果加载过就直接返回,反之去问父类加载器(即BootstrapClassLoader
)是否加载过;BootstrapClassLoader
去检查自己是否加载过,加载过就直接返回;没有加载过就返回BootstrapClassLoader
没加载过,ExtClassLoader
尝试自己去查找(调用findClass()
);找到就返回,没找到就让子加载器去找(即AppClassLoader
)去找ExtClassLoader
没查找到,AppClassLoader
尝试自己去查找,找到就返回;没找到就抛出ClassNotFound
异常
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.jar
和A1_1.jar
的测试包都加入了项目,但是A1_0.jar
依赖的是B1_0.jar
,A1_1.jar
依赖的是B1_1.jar
,当启动项目的时候可能就会出现A1_1.jar
的类本来要使用B1_1.jar
的类,但是因为顺序使用成了B1_0.jar
的类,如果这个类因为版本原因少了一些重要的方法,那就会报NoSuchMethodException
~
- 举个例子,我现在把
- 如果开发人员是通过
maven
导入依赖的,常见的异常包括NoSuchMethodException
、ClassNotFoundException
。因为maven
会自动除掉相同的包~