开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情
Java 类加载器
类加载就是将.class
文件通过IO
读入内存,并生成一个Class对象的过程。
类加载通常由JVM
提供默认的类加载器,这些被称之为系统加载器,还可以通过继承ClassLoader
来实现自定义加载器。
一个类被加载到JVM
中后,通常不会被加载第二次,因为通过双亲委派模型可以很好的避免重复加载的问题。
一、类加载器种类
在Java
中默认的类加载器有三种,分别为:
- BootStrap ClassLoader (启动类加载器)
- 执行优先级最高的类加载器
- 用于加载
JRE
核心类库的.jar
文件,如rt.jar
- 使用
C
语言实现,双亲委派顶级祖宗加载器
- Extensions ClassLoader 也称 (Ext ClassLoader) (扩展类加载器)
- 用于加载
JRE
扩展目录下的.jar
文件 - 使用
Java
实现
- 用于加载
- Application ClassLoader (应用程序类加载器)
- 用于加载用户自己编写的类文件,和项目中引用其他
.jar
包中的类文件
- 用于加载用户自己编写的类文件,和项目中引用其他
- Custom ClassLoader (自定义加载器)
- 用于加载指定目录下的类,通常用于自定义实现
- 关系类图如下
二、双亲委派模型
**1. ** 双亲委派模型其表达的意思是,类加载器与父亲加载器嵌套,当需要加载类的时候回去现寻找父亲加载器,如果有父亲加载器的话就优先使用父亲加载器,如果父亲加载器无法完成加载的话,再使用自己进行加载。
说人话:现在有一个苹果(Class),你不吃你给你妈妈吃,你妈妈不吃又给你奶奶吃,你奶奶不吃有给你妈妈吃,你妈妈不吃又给你吃。
2. 使用双亲委派模型有以下好处
- 可以避免重复加载类
- 虽然有多个类加载器,但是因为每个类都是唯一的,且都会通过父亲加载器加载,所以可以避免类加载重复。
- 可以避免
Java
核心类被用户篡改- 如果用户自定义了一个类为
java.lang.String
,那么会一层一层向上加载,直到BootStrap ClassLoader
祖宗类加载器(入口加载器),但是在这个加载器里已经将JDK
包下的java.lang.String
加载过了,所以会直接跳过加载用户编写的String
类。 - 但是此处有个
BUG
,就是双亲委派可以被破坏!
- 如果用户自定义了一个类为
3. 双亲委派模型图
三、破坏双亲委派
双亲委派有两个弊端:
- 不可以不委派
- 不可以向下委派
但是这两个弊端都可以被打破
- 通过自定义类加载器可以破坏:不可以不委派
- 通过
SPI
机制可以破坏:不能向下委派
源码中可以得知,双亲委派机制是由此处进行调用的
// AppClassLoader || ExtClassLoader
public Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
int i = name.lastIndexOf('.');
// 权限验证
if (i != -1) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPackageAccess(name.substring(0, i));
}
}
// 判断类在classPath中是否存在
if (ucp.knownToNotExist(name)) {
// The class of the given name is not found in the parent
// class loader as well as its local URLClassPath.
// Check if this class has already been defined dynamically;
// if so, return the loaded class; otherwise, skip the parent
// delegation and findClass.
// 查找此类是否已经被加载过了,如果是的话则返回Class<?>对象,否则返回null,则调用父类loadClass()方法进行加载
Class<?> c = findLoadedClass(name);
if (c != null) {
if (resolve) {
resolveClass(c);
}
return c;
}
throw new ClassNotFoundException(name);
}
// 调用父类的loadClass,内部实现了双亲委派
return (super.loadClass(name, resolve));
}
父类的loadClass()
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 获得加载锁
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 查询是否被加载过了,如果加载过了,直接跳过
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 查找当前加载器是否有父亲加载器,(双亲委派机制)
if (parent != null) {
// 调用父亲加载器进行加载
c = parent.loadClass(name, false);
} else {
// 如果父亲加载器等于null的话,则代表执行当前方法的加载器为BootStrap ClassLoader,并使用此加载器进行加载
// native方法,由C实现
c = findBootstrapClassOrNull(name);
}
// 父亲加载器无法加载
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
// 如果父亲加载器无法加载的话,则自己在进行加载
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;
}
}
1. 不委派父亲加载器
从上述源码中可以得知,双亲委派机制是由ClassLoader -> loadClass()
方法进行实现的,而且此方法非final
修饰,则代表此方法是可以被我们的自定义加载器重写的。
双亲委派的核心源码为:
// 查找当前加载器是否有父亲加载器,(双亲委派机制)
if (parent != null) {
// 调用父亲加载器进行加载
c = parent.loadClass(name, false);
}
如果我们将loadClass()
方法进行重写,且不执行这段逻辑的话,则双亲委派机制就会被打破,从而遭到破坏!
2. 为什么建议重写findClass()
方法
因为是出于历史原因,且JDK
是向上兼容的,所以该方法并不能随便进行更改,但是JDK
开发者也意识到了这个问题,所以在ClassLoader
抽象类中新增了findClass()
方法,供开发者进行重写,和实现自定义类加载器。
在JDK
源码中的loadClass()
方法上有一段注释说明了此方法不建议被重写:
/*加载具有指定二进制名称的类。该方法的默认实现按照以下顺序搜索类:调用findLoadedClass(String)检查类是否已经加载。在父类装入器上调用loadClass方法。如果父类为空,则使用虚拟机内置的类装入器。调用findClass(String)方法来查找类。如果使用上述步骤找到了类,并且resolve标志为真,则该方法将对生成的class对象调用resolveClass(class)方法。ClassLoader的子类被鼓励重写findClass(String),而不是这个方法。除非被重写,否则这个方法在整个类加载过程中同步getClassLoadingLock方法的结果。*/
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {...}
2. 向下委派子加载器
在Java
中通过SPI
机制可以实现向下委派子加载器,至于为什么这么做也是因为一些历史原因
在
JDK
中引入了SPI
机制后,例如JDBC
有个驱动管理类DriverManager
,他是在rt.jar
下的,由BootStrap ClassLoader
进行加载,但是由于这个类里需要加载所有的Driver
驱动,但是,像是MySQL
、Oracle
等数据库驱动都是由各家数据库厂商实现的,是以外部依赖的模式进行引入的,此时如果SPI
机制想要顺利的加载驱动,就不能使用BootStrap ClassLoader
因为各个数据库驱动并不在rt.jar
下,所以无法加载,因此JDK
搞了一个线程上下文类加载器,和setContextClassLoader()
来手动设置类加载器,才解决了这个问题,此时其实已经是调用了线程上下文类加载器,一般为Application ClassLoader
,进行加载驱动的动作了。
四、自定义Java内部类
如果在自己的工程目录中自定义了一个和JDK
中同包,同类名的一个类,例如:java.lang.String
,会有什么样的结果?
首先我们了解类加载器和类加载器的双亲委派机制,因此得知,要加载这个类,可以使用Application ClassLoader
或Custom ClassLoader
进行加载,当其中一个类加载器加载时,会调用父亲的loadClass()
方法,最终调用到BootStrap ClassLoader
中去,但是由于java.lang.String
为JDK
核心类,并且已经被祖宗加载器加载过了,所以会跳过自己工程下的java.lang.String
。从而避免了核心类篡改和重复加载的问题。
此时如果调用自定义java.lang.String
类的方法,会报**NoSuchMethodError
**异常。因为自定义的类并没有被加载。
五、附
类加载图解