JVM之类加载器

jvm的ClassLoader

在JVM中,类加载器有三个分别是BootStrapClassLoader、ExtClassLoader和AppClassLoader,三个类加载器都有自己的分工。

BootStrapClassLoader

BootStrapClassLoader又称为引导类加载器,它是由C++实现的,负责将<JAVA_HOME>/lib下面的核心类库 或 -Xbootclasspath选项指定的jar包等 虚拟机识别的类库 加载到内存中,例如rt.jar包就是通过它加载的。在双亲委派模型中,它是最顶层的类加载器,所以它是没有父类加载器的,而且我们也无法通过代码获取到这个它,由它加载的类,在调用class.getClassLoader时,得到的是null。

ExtClassLoader

ExtClassLoader称为扩展类加载器,它负责将 <JAVA_HOME >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库 加载到内存中。开发者可以直接使用标准扩展类加载器。

AppClassLoader

AppClassLoader称为系统类加载器,它负责将用户类路径(java -classpath或-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径。我们编写的Java类一般都是由AppClassLoader加载的。

双亲委派机制

jvm加载类有一套自己的机制,就是每次遇到要加载的类时,先交给父类去加载,然后父类又交给父类的父类去加载,以此类推,这样做的目的有两点,一是能够保障jvm的正常运行,不会加载恶意的代码去替换java自带的类,二是能够保障java类不会重复加载。判断一个类是不是同一个类首先要先判断是不是由同一个类加载器加载,相同的类分别由不同的类加载器加载在jvm中都是两个不同的类。
在双亲委派机制中有一个大家经常容易忽略的点,就是当遇到一个新的类要去加载的时候,决定用哪个类来加载也是双亲委派模型的一部分,双亲委派模型中,当A类中引用B类时,如果B类没有加载,这时会用加载A类的加载器加载B类,这样就能确保双亲委派模型中没有重复加载的类。

ClassLoader

除了BootStrapClassLoader是由C++实现的以外,其他的类加载器都必须直接或间接继承ClassLoader
在classloader中,主要方法有四个,分别是loadClass、findClass、defineClass和resolveClass,接下来我们就分别介绍这个四个方法的作用。

loadClass

源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 查询是否被加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 没有的话就交给父类加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 父类没有加载到,就调用findClass加载
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
// 判断是否解析
if (resolve) {
resolveClass(c);
}
return c;
}
}

classLoader中,loadClass主要的作用就是实现了双亲委派机制,所以如果我们要遵循双亲委派模型,就不用重写loadClass方法,想破坏双亲委派模型就需要我们自定义的类加载器重写loadClass方法。

findClass

Finds the class with the specified binary name.
This method should be overridden by class loader implementations that
follow the delegation model for loading classes, and will be invoked by the {@link #loadClass loadClass} method after checking the parent class loader for the requested class. The default implementation throws a ClassNotFoundException.

上面这段话是ClassLoader中findClass的注释,翻译就是,按照特定的二进制名称查找类,如果要遵循委派模型要重写这个方法,这个方法在loadClass中会被调用,默认实现是抛出一个异常。
所以总结一下就是,findClass会被loadClass调用,如果要定义一个自己的类加载器,遵循双亲委派机制,只要实现findClass找到需要加载的类就可以了。

defineClass

Converts an array of bytes into an instance of class Class, with an optional ProtectionDomain. If the domain is null, then a default domain will be assigned to the class as specified in the documentation for {@link #defineClass(String, byte[], int, int)}. Before the class can be used it must be resolved.
The specified name cannot begin with “java.“, since all classes in the “java.* packages can only be defined by the bootstrap class loader. If name is not null, it must be equal to the binary name of the class specified by the byte array “b“, otherwise a {@link NoClassDefFoundError NoClassDefFoundError} will be thrown.

defineClass的作用就是把一个二进制流转换成一个Class类,这个过程中就完成了JVM类加载过程的中装载、验证、准备、解析、初始化过程,可以开始创建该类的实例。要注意的一点就是第二段话中描述的,以java.开头的只能通过bootstrap class loader加载,这个就表明即使我们破坏了双亲委派模型,自定义一个类加载器去加载一个java.lang.String也不会被jvm加载到内存,在defineClass就会被限制掉。

resolveClass

Links the specified class. This (misleadingly named) method may be used by a class loader to link a class. If the class c has already been linked, then this method simply returns. Otherwise, the class is linked as described in the “Execution” chapter of The Java™ Language Specification.

resolveClass这个类具体做了什么我还没了解清楚,debug的时候一直没有进入到这个类里面,所以不清楚什么时候会用到,等后面有机会了解到再更新。

SPI

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制,我的理解就是定义一个接口,让第三方厂商来实现,然后通过一个Manager来管理这些实现类,比如在Java中定义了一个java.sql.Driver接口,mysql和oracle都可以实现自己的驱动器,然后通过DriverManager来管理这些实现类,这里就出现一个问题,因为Driver和DriverManager是通过BootStrapClassLoader加载的,但是DriverManager中需要加载Mysql的Driver(MysqlDriver),但是MysqlDriver类是不在JAVA_HOME/lib下的,在双亲委派模型中,都是使用当前类的加载器去加载引用的类,DriverManager类是通过BootStrapClassLoader加载的,所以MysqlDriver按照双亲委派模型也要通过BootStrapClassLoader加载,但是MysqlDriver不在BootStrapClassLoader加载的目录下,这样就导致BootStrapClassLoader无法加载MysqlDriver,就必须破坏双亲委派模型来加载,在实现中,MysqlDriver就是通过上下文类加载器加载到内存中的。
上下文类加载器通过Thread.currentThread().getContextClassLoader()可以获得,如果没有设置,默认的上下文类加载器就是AppClassLoader。

总结

AppClassloader和ExtClassLoader的调用链就是loadClass->findClass->defineClass完成类的加载的。JVM的类加载模型是双亲委派模型,这样可以保障JVM的安全,保障类不重复加载,不过SPI打破了双亲委派模型。如果我们要定义自己的类加载器,遵循原有的委派模型,只需要实现findClass拿到二进制流就可以了。

<参考> https://blog.csdn.net/irelandken/article/details/7048817
<参考> http://www.importnew.com/24036.html