Java类加载
Java类加载过程
加载->[验证->准备->解析]->初始化
加载
通过一个类的完全限定查找此类字节码文件,并利用字节码文件创建一个Class对象。
验证
确保Class文件的字节流中包含信息符合当前虚拟机要求,不会危害虚拟机自身安全。主要包括四种验证:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证。
准备
为类变量(即static修饰的字段变量)分配内存并且设置该类变量的初始值即0(如static int i=5;这里只将i初始化为0,至于5的值将在初始化时赋值),这里不包含用final修饰的static,因为final在编译的时候就会分配了,注意这里不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是会随着对象一起分配到Java堆中。
解析
将常量池中的符号引用替换为直接引用的过程。 包括:
- 类或接口的解析;
- 字段解析;
- 类方法解析;
- 接口方法解析。
初始化
类加载最后阶段,若该类具有超类,则对其进行初始化,执行静态初始化器和静态初始化成员变量(如前面只初始化了默认值的static变量将会在这个阶段赋值,成员变量也将被初始化)。
类加载器
引导类加载器(Bootstrap ClassLoader)
启动类加载器主要加载的是JVM自身需要的类,这个类加载使用C++语言实现的,是虚拟机自身的一部分,它负责将 <JAVA_HOME>/lib
路径下的核心类库或-Xbootclasspath
参数指定的路径下的jar包加载到内存中,注意必由于虚拟机是按照文件名识别加载jar包的,如rt.jar,如果文件名不被虚拟机识别,即使把jar包丢到lib目录下也是没有作用的(出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun
等开头的类)。
扩展类加载器(Extension ClassLoader)
扩展类加载器是指Sun公司(已被Oracle收购)实现的sun.misc.Launcher$ExtClassLoader
类,由Java语言实现的,是Launcher的静态内部类,它负责加载<JAVA_HOME>/lib/ext
目录下或者由系统变量-Djava.ext.dir
指定位路径中的类库,开发者可以直接使用标准扩展类加载器。
系统类加载器(System ClassLoader)
也称应用程序加载器是指 Sun公司实现的sun.misc.Launcher$AppClassLoader
。它负责加载系统类路径java -classpath
或-D java.class.path
指定路径下的类库,也就是我们经常用到的classpath路径,开发者可以直接使用系统类加载器,一般情况下该类加载是程序中默认的类加载器,通过ClassLoader#getSystemClassLoader()
方法可以获取到该类加载器。
双亲委派模式
对于任意一个类,都需要由加载它的类加载器和这个类本身来一同确立其在Java虚拟机中的唯一性。
原理
双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:
如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式。
优势
- 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次;
- 考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为
java.lang.Integer
的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer
,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改;
自定义类加载
实现自定义类加载器需要继承ClassLoader
或者URLClassLoader
,继承ClassLoader则需要自己重写findClass()方法并编写加载逻辑,继承URLClassLoader
则可以省去编写findClass()方法以及class文件加载转换成字节码流的代码。
- 加载特定路径下的class文件生成class对象;
- 通过网络传输并且可能会进行相应的加密操作时,需要先对class文件进行相应的解密后再加载到JVM内存;
- 需要实现热部署功能时(一个class文件通过不同的类加载器产生不同class对象从而实现热部署功能),需要实现自定义ClassLoader的逻辑;
破坏双亲委派模型
线程上下文类加载器
在Java应用中存在着很多服务提供者接口(Service Provider Interface,SPI
),这些接口允许第三方为它们提供实现,如常见的 SPI 有 JDBC、JNDI等,这些 SPI 的接口属于 Java 核心库,一般存在rt.jar包中,由Bootstrap
类加载器加载,而 SPI 的第三方实现代码则是作为Java应用所依赖的 jar 包被存放在classpath路径下,由于SPI接口中的代码经常需要加载具体的第三方实现类并调用其相关方法,但SPI的核心接口类是由引导类加载器来加载的,而Bootstrap类加载器无法直接加载SPI的实现类,同时由于双亲委派模式的存在,Bootstrap类加载器也无法反向委托AppClassLoader
加载器SPI的实现类。
从图可知rt.jar核心包是有Bootstrap
类加载器加载的,其内包含SPI核心接口类,由于SPI中的类经常需要调用外部实现类的方法,而jdbc.jar包含外部实现类(jdbc.jar存在于classpath路径)无法通过Bootstrap类加载器加载,因此只能委派线程上下文类加载器把jdbc.jar中的实现类加载到内存以便SPI相关类使用。显然这种线程上下文类加载器的加载方式破坏了“双亲委派模型”,它在执行过程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器,当然这也使得Java类加载器变得更加灵活。
DriveManager
加载mysql驱动包中的com.mysql.jdbc.Driver
源码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DriverManager {
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
//...去除无关代码
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//...去除无关代码
}
}
ServiceLoader.load()
源码:
1
2
3
4
5
public static <S> ServiceLoader<S> load(Class<S> service) {
//使用线程上下文类加载器加载
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Launcher初始化的时候,会获取AppClassLoader
,然后将其设置为上下文类加载器,源码:
1
2
3
4
5
6
7
8
9
10
public Launcher() {
//...
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
//...
}
核心包的SPI类对外部实现类的加载都是基于线程上下文类加载器执行的,通过这种方式实现了Java核心代码内部去调用外部实现类。我们知道线程上下文类加载器默认情况下就是AppClassLoader
,那为什么不直接通过getSystemClassLoader()
获取类加载器来加载classpath路径下的类的呢?其实是可行的,但这种直接使用getSystemClassLoader()
方法获取AppClassLoader
加载类有一个缺点,那就是代码部署到不同服务时会出现问题,如把代码部署到Java Web应用服务或者EJB之类的服务将会出问题,因为这些服务使用的线程上下文类加载器并非AppClassLoader
,而是Java Web应用服自家的类加载器,类加载器不同。所以我们应用该少用getSystemClassLoader()
。总之不同的服务使用的可能默认ClassLoader
是不同的,但使用线程上下文类加载器总能获取到与当前程序执行相同的ClassLoader
,从而避免不必要的问题。