在开始讲述之前简单回顾一下之前的类加载过程
具体内容大家可以看我上一篇关于类加载过程的详细介绍 Java类是如何被加载到内存中的?
类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。
字节码可以是 Java 源程序(.java
文件)经过 javac
编译得来,也可以是通过工具动态生成或者通过网络下载得来。
需要注意的是
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
每个 Java 类都有一个引用指向加载它的 ClassLoader
。
数组类不是通过 ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
Class.forName()和ClassLoader.loadClass()区别?
启动类加载器
: Bootstrap ClassLoader,负责加载存放在JDK\jre\lib
(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被Java程序直接引用的。
扩展类加载器
: Extension ClassLoader,该加载器由sun.misc.Launcher$ExtClassLoader
实现,它负责加载JDK\jre\lib\ext
目录中,或者由java.ext.dirs
系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。
应用程序类加载器
: Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader
来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
🌈 拓展一下:
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终会传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子类加载器才会尝试自己去加载该类。
这种类加载器之间的层次关系被称为类加载器的“双亲委派模型(Parents Delegation Model)”。
java.lang.Object
的同名类并放在ClassPath
中,多个类加载器都能加载这个类到内存中,系统中将会出现多个不同的Object
类,那么类之间的比较结果及类的唯一性将无法保证,同时,也会给虚拟机的安全带来隐患。注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//首先,检查该类是否已经加载过
Class c = findLoadedClass(name);
if (c == null) {
//如果 c 为 null,则说明该类没有被加载过
long t0 = System.nanoTime();
try {
if (parent != null) {
//当父类的加载器不为空,则通过父类的loadClass来加载该类
c = parent.loadClass(name, false);
} else {
//当父类的加载器为空,则调用启动类加载器来加载该类
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父类的类加载器无法找到相应的类,则抛出异常
}
if (c == null) {
//当父类加载器无法加载时,则调用findClass方法来加载该类
//用户可通过覆写该方法,来自定义类加载器
long t1 = System.nanoTime();
c = findClass(name);
//用于统计类加载器相关的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//对类进行link操作
resolveClass(c);
}
return c;
}
}
构造自定义加载器,需要继承 ClassLoader
。如果我们不想打破双亲委派模型,就重写 ClassLoader
类中的 findClass()
方法即可,无法被父类加载器加载的类最终会通过这个方法被加载。但是,如果想打破双亲委派模型则需要重写 loadClass()
方法。
实现自定义类加载器的实现,主要分三个步骤
public class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = getClassBytes(name);
Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
return c;
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
private byte[] getClassBytes(String name) throws Exception {
name = name.replaceAll("\\.", "/");
FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
byte[] data = new byte[len];
fis.read(data);
fis.close();
return data;
}
}
定义CoderWorldClass.java
文件,并使用javac
编译成.class
文件
public class CoderWorldClass {
public CoderWorldClass(){
System.out.println("CoderWorldClass:"+getClass().getClassLoader());
System.out.println("CoderWorldClass Parent:"+getClass().getClassLoader().getParent());
}
public String print(){
System.out.println("CoderWorldClass method for print NEW"); //修改了打印语句,用来区分被加载的类
return "CoderWorldClass.print()";
}
}
在MyClassLoader类中,重写loadClass
方法,代码如下
@Override
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) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//非自定义的类还是走双亲委派加载
if (!name.equals("CoderWorldClass")) {
c = this.getParent().loadClass(name);
} else { //自己写的类,走自己的类加载器。
c = findClass(name);
}
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
通过重写loadClass方法,使得自己创建的类,让第一个加载器直接加载,不委托父加载器寻找,从而实现双亲委派的破坏
Tomcat是如何实现应用jar包的隔离的?
在思考这个问题之前,我们先来想想Tomcat作为一个JSP/Servlet容器,它应该要解决什么问题?
Tomcat类加载机制
Common类加载器
作为 Catalina类加载器
和 Shared类加载器
的父加载器。Common类加载器
能加载的类都可以被 Catalina类加载器
和 Shared类加载器
使用。因此,Common类加载器
是为了实现公共类库(可以被所有 Web 应用和 Tomcat 内部组件使用的类库)的共享和隔离。
Catalina类加载器
和 Shared类加载器
能加载的类则与对方相互隔离。Catalina类加载器
用于加载 Tomcat 自身的类,为了隔离 Tomcat 本身的类和 Web 应用的类。
Shared类加载器
作为 WebApp类加载器
的父加载器,专门来加载 Web 应用之间共享的类比如 Spring、Mybatis。
每个 Web 应用都会创建一个单独的 WebApp类加载器
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader
,各个 WebAppClassLoader
实例之间相互隔离,进而实现 Web 应用之间的类隔。
Tomcat如何破坏双亲委派?
假设 Tomcat 服务器加载了一个 Spring Jar 包。项目中用到的基础类库,由于其是 Web 应用之间共享的,因此会由 SharedClassLoader
加载。项目中一些用到了 Spring 的业务类,比如实现了 Spring 提供的接口、用到了 Spring 提供的注解。所以,加载 Spring 的类加载器(也就是 SharedClassLoader
)也会用来加载这些业务类。但是业务类在 Web 应用目录下,不在 SharedClassLoader
的加载路径下,所以 SharedClassLoader
无法找到业务类,也就无法加载它们。
如何解决这个问题呢?
这个时候就需要用到 线程上下文类加载器(ThreadContextClassLoader
) 。
当 Spring 需要加载业务类的时候,它不是用自己的类加载器,而是用当前线程的上下文类加载器。还记得我上面说的吗?每个 Web 应用都会创建一个单独的 WebAppClassLoader
,并在启动 Web 应用的线程里设置线程线程上下文类加载器为 WebAppClassLoader
。这样就可以让高层的类加载器(SharedClassLoader
)借助子类加载器( WebAppClassLoader
)来加载业务类,从而破坏了 Java 类加载的双亲委托机制。
参考内容