面试连环call
- 双亲委派机制 是什么?如何打破双亲委派机制?
- JVM都有哪些类加载器?
- 如何构造一个自定义类加载器?
- Tomcat 的类加载机制?Spring的类加载机制
- Class.forName ()和ClassLoader.loadClass()区别?
在开始讲述之前简单回顾一下之前的类加载过程
- 类加载过程:加载->连接->初始化。
- 其中连接 过程又分为:验证->准备->解析。
具体内容大家可以看我上一篇关于类加载过程的详细介绍 Java类是如何被加载到内存中的?
类加载器作用
类加载器的主要作用就是加载 Java 类的字节码( .class
文件)到 JVM 中(在内存中生成一个代表该类的 Class
对象)。
字节码可以是 Java 源程序(.java
文件)经过 javac
编译得来,也可以是通过工具动态生成或者通过网络下载得来。
需要注意的是
-
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载这一步。
-
每个 Java 类都有一个引用指向加载它的
ClassLoader
。 -
数组类不是通过
ClassLoader
创建的(数组类没有对应的二进制字节流),是由 JVM 直接生成的。
Class.forName()和ClassLoader.loadClass()区别?
- Class.forName() : 将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块
- ClassLoader.loadClass() : 只是将.class文件加载到jvm中,不会执行static中的内容, 只有在newInstance才会去执行static中内容
类加载器分类
-
启动类加载器
: 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)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。
🌈 拓展一下:
- rt.jar:rt 代表"RunTime",rt.jar是 Java 基础类库 ,包含 Java doc 里面看到的所有的类的类文件。也就是说,我们常用内置库 java.xxx.*都在里面,比如java.util .*、java.io .*、java.nio .*、java.lang .*、java.sql .*、java.math.*。
- Java 9 引入了模块系统 ,并且略微更改了上述的类加载器。扩展类加载器被改名为平台类加载器(platform class loader)。Java SE 中除了少数几个关键模块,比如说 java.base 是由启动类加载器加载之外,其他的模块均由平台类加载器所加载。
双亲委派机制
定义
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类 ,而是把请求委托给父加载器 去完成,依次向上,因此,所有的类加载请求最终会传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子类加载器才会尝试自己去加载该类。
这种类加载器之间的层次关系被称为类加载器的"双亲委派模型(Parents Delegation Model)"。
为什么要使用?
- 防止内存中出现多份同样的字节码 。如果没有该机制而是由各个类加载器自行加载的话,用户编写了一个
java.lang.Object
的同名类并放在ClassPath
中,多个类加载器都能加载这个类到内存中,系统中将会出现多个不同的Object
类,那么类之间的比较结果及类的唯一性将无法保证,同时,也会给虚拟机的安全带来隐患。 - 双亲委派机制能够保证多加载器加载某个类时,最终都是由一个加载器加载,确保最终加载结果相同。
- 这样可以保证系统库优先加载,即便是自己重写,也总是使用Java系统提供的System,自己写的System类根本没有机会得到加载,从而保证安全性。
注意 ⚠️:双亲委派模型并不是一种强制性的约束,只是 JDK 官方推荐的一种方式。如果我们因为某些特殊需求想要打破双亲委派模型
执行流程
双亲委派模型的实现代码非常简单,逻辑非常清晰,都集中在 java.lang.ClassLoader
的 loadClass()
中,相关代码如下所示。
java
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()
方法。
不破坏双亲委派
实现自定义类加载器的实现,主要分三个步骤
- 创建一个类继承ClassLoader抽象类
- 重写findClass()方法
- 在findClass()方法中调用defineClass()
java
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
文件
java
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
方法,代码如下
java
@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类加载
Tomcat是如何实现应用jar包的隔离的?
在思考这个问题之前,我们先来想想Tomcat作为一个JSP/Servlet容器,它应该要解决什么问题?
- 一个web容器需要部署多个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本 ,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机,必然会带来内存消耗过高的问题。
- web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开。
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 类加载的双亲委托机制。
参考内容