1、java类加载器的分类
JDK8及之前
- 启动类加载器,BootStrap Class Loader,加载核心类,加载jre/lib目录下的类,C++实现的
- 拓展类加载器, Extension Class Loader,加载java拓展类库,jre/lib/ext目录下,比如javax,java.util包等
- 应用程序类加载器,Application Class Loader, 也成系统类加载器,负责加载应用程序类,classpath下的类文件,maven依赖的类文件
- 自定义加载器:继承ClassLoader,并重写findClass方法。
类加载器的作用?
负责在类的加载过程中,获取字节码文件并加载到内存中。通过加载字节码数据转换为byte[]数组,接下来调用虚拟机底层方法将byte[]数组转换成方法区和堆中的数据。
2、双亲委派机制
2.1 双亲委派机制为了解决什么问题?
核心是为了解决一个类到底是由哪个类加载器来加载
2.2 介绍一下双亲委派机制?
当一个类加载器需要去加载类时,会 首先委派给其父类加载器进行加载,如果父类加载器无法加载,才由该类加载器自己去加载
2.3、双亲委派机制有什么好处?
这种层级关系使得类加载器能实现类的共享,避免类被重复加载,
提高了代码的安全性和可靠性,避免恶意覆盖jdk的核心类库。
比如,你重写了java.lang.String类,因为双亲委派模型机制,jvm只会通过bootstrap类加载器加载jre/lib包下的String类,由于父类加载器已经加载过这个类,就不会重复加载自己定义的,这样一定程度上保障了jvm的安全。
2.4、双亲委派机制有什么缺点?
有时候,需要多次加载同名的目录下的类,比如,当在Tomcat上部署多个服务时,不同服务可能依赖了不同版本的第三方jar,如果使用双亲委派机制进行加载,那么多个服务中的第三方jar就只会加载一次,那么依赖另外一个版本的jar的服务就有可能产生异常。
为了解决这种情况,需要打破双亲委派机制,不在让父类加载器加载,而是每个服务创建自己的子类加载器。
3、双亲委派机制原理?
不多说,看代码
java.lang.ClassLoader
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
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 {
// 拓展类加载器的父加载器是null,
// 但是上层还有一个C++实现的启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
// 父加载器没有加载到,就自己尝试加载
if (c == null) {
long t1 = System.nanoTime();
// 加载方法,protected修饰的,因此如果自定义类加载器,不想破坏双亲委派模型的话,就重写这个方法
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();
}
}
// resolveClass方法就是类加载阶段的连接 阶段,loadClass方法是赋值为false的,不会进行连接
if (resolve) {
resolveClass(c);
}
return c;
}
}
4、如何破坏双亲委派机制呢?
4.1:自定义类加载器,重写loadClass方法
但是如果只为了定义自定义加载器,建议重写findClass方法,这样不会破坏双亲委派机制。
public class MyClassLoader extends ClassLoader {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
// 核心类库还是交给双亲委派机制进行加载
if (name.startsWith("java.")) {
super.loadClass(name);
}
// 通过类的全限定名,找到字节码文件,并转为byte数组 不写了
byte[] data = loadClassData(name);
// 类的加载就是由defineClass这个底层方法实现的。
return defineClass(name, data, 0, data.length);
}
}
4.2 打破双亲委派机制的另外一种案例:JDBC
介绍:JDBC中使用了DriverManger来管理项目中引入的不同数据库的驱动,比如mysql,oracle驱动等。DriverManger这个类,位于rt.jar包中,由启动类加载器进行加载。但是DriverManger这个类是要去加载引入的jar包中的驱动类的,但是引入的jar包应该是由应用程序类加载器加载的,
这就出现了一个问题:启动类加载器加载完成DriverManger后,会委托应用类加载器去加载jar包中的驱动。这种委托关系就打破了双亲委派机制。
DriverManger怎么知道jar包中要加载的驱动在哪里?
spi机制(service provider interface),jdk内置的一种服务发现机制。查找classpath路径下的META-INF/services 文件夹下面的文件,并自动加载文件里所定义的类(这个文件中定义的就是需要加载的类的全限定名称).
这个文件中定义的就是需要加载的类的全限定名称,在JDBC中,比如mysql,oracle等第三方jar包驱动,就在META-INF/services 文件下面定义了以接口为名称的文件,文件内容就是对接口具体的实现类。
在springboot中,会引入一些其他依赖,自动注入的过程中,因为第三方jar包类和当前启动项不在一个包中,所以不能通过扫描进行注入,而且注入也要尽可能的和第三方依赖解耦。
springboot就规定了在META-INF文件夹中可以定义spring.factories文件,项目启动的时候会遍历所有jar包中的spring.factories,将里面定义的类加载到IOC中。
---- 上述就是springboot 中SPI机制的实现。
4.3 怎么理解JDBC案例中打破双亲委派机制?
- 首先是DriverManager这个类,是由启动类加载器加载的,毫无疑问符合双亲委派机制
- DriverManager加载驱动类时,不知道这些驱动类在哪里(比如引入了mysql或者oracle),就在初始化阶段的时候通过SPI机制去寻找,把加载这些类交给了应用类加载器去加载。(问题就出在这里了,启动类加载器委托应用类加载器去加载具体的驱动类。从这一层理解,好像是打破了双亲委派机制。)
- 但是启动类加载器知道要加载哪些类了之后,加载这些类的过程,还是符合双亲委派机制的。
- 通过上面的描述,JDBC是否打破双亲委派机制这个事情,见仁见智吧。