一、类加载的过程
通过Java命令执行代码的大体流程如下:
其中loadClass的类加载过程分为几个步骤:
加载:把编译好的字节码文件加载到JVM内存中
验证:验证加载进来的字节码文件的格式是否正确
准备:给类的静态变量分配内存并赋予初值
解析:将符号引用替换为直接引用,该阶段会把一些静态方法(比如main方法)替换为指向数据所存内存的指针或者句柄【指向指针的指针】。这就是静态链接【类加载期间完成的】的过程。后面的动态链接是在对象的创建过程中完成的。
初始化:给类的静态变量进行赋予指定的值的操作
使用:使用类
卸载:卸载类加载器
注意:主类在运行过程中如果使用到其他类,会逐步加载这些类,而不是一次性加载的,是使用到时才加载.
二、Launcher类在类加载的作用
Launcher类是JVM的启动类,用力启动JVM。Launcher是由引导类加载器创建的,它主要负责创建我们的扩展类加载器ExtClassLoader和应用类加载器AppClassLoader以及设置它们的父加载器,将AppClassLoader设置为系统类加载器等等。
那么Launcher它具体是如何做的呢?我们通过源码来看一看:
获取ExtClassLoader
try {
//获取扩展类加载器的方法
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
//扩展类是Launcher类的一个静态内部类,通过双重if判断 + synchronized关键字实现的单例模式获取
//ExtClassLoader类
public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
if (instance == null) {
Class var0 = Launcher.ExtClassLoader.class;
synchronized(Launcher.ExtClassLoader.class) {
if (instance == null) {
instance = createExtClassLoader();
}
}
}
return instance;
}
获取AppClassLoader
try {
//获取AppClassLoader类的方法
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
//获取类加载器的加载地址-由此可以看出我们的AppClassLoader是负责加载classPath下的类
final String var1 = System.getProperty("java.class.path");
final File[] var2 = var1 == null ? new File[0] : Launcher.getClassPath(var1);
return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction<Launcher.AppClassLoader>() {
public Launcher.AppClassLoader run() {
URL[] var1x = var1 == null ? new URL[0] : Launcher.pathToURLs(var2);
//获取到AppClassLoader
return new Launcher.AppClassLoader(var1x, var0);
}
});
}
所以,现在大家可以知道我们的类加载器是哪来得了吧!!!接下来我们看一下类加载过程中的双亲委派机制是如何实现的。
三、双亲委派机制
类加载器负责加载的路径
引导类加载器(BootStrapClassloader):负责加载支撑JVM运行的位于JRE的lib目录下的核心类库,比如 rt.jar、charsets.jar等
扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR 类包
应用程序类加载器(AppClassLoader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类
自定义加载器:负责加载用户自定义路径下的类包
双亲委派机制
什么是双亲委派机制?
此处默认无自定义类加载器
当我们加载一个类的时候,首先从应用类加载器所加载的路径找是否已经加载过该类。如果已经加载了就直接返回就可以了;如果没有加载则向它的父加载器也就是扩展类加载器去查看是否已经加载该类。一样的,如果存在则返回,如果不存在则继续向上委托。如果一直委托到启动类加载器还是没有获取到该类的话,那么就先由启动类加载器加载该类。如果加载不到该类则向下一级类加载器也就是扩展类加载器加载,如果还是没有加载到则向下一级列类加载器也就是应用类加载器负责加载直到加载到该类。这中向上委托再从上到下加载的机制就是双亲委派机制。
总结一句话:先找父类加载,父类加载不到再找子类加载
双亲委派机制的作用
说到这大家应该也都明白了双亲委派机制的作用。主要有两个作用:
1.保证沙箱安全机制:防止JDK的核心类库被篡改。
2.保证类加载的唯一性:保证类加载的唯一性,不重复加载相同的类。
源码分析
我将从源码的角度验证双亲委派机制是不是上面所说的那样:
所有的类都会继承ClassLoader类,调用ClassLoader.loadClass(“classPath”)方法来找这个类,如果找不到就调用调用URLClassLoader的findClass()方法来加载该类。主要的类加载机制就在ClassLoader的loadClass方法中
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 查看类是否已经被加载了
Class<?> c = findLoadedClass(name);
//c == null
if (c == null) {
long t0 = System.nanoTime();
try {
//判断父加载器是否为null,此时这里的parent为ExtClassLoader
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//当ExtClassLoader也没找到则进入这一行逻辑,此时调用的是ExtClassLoader.loadClass方法,它的parent == null。因为BootStrapClassLoader是C++代码写的,所以Java中是获取不到的。如果都没有则先由BootStrapClassLoader加载该类。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
//如果没有加载到则返回上一级方法,ExtClassLoader-->AppClassLoader
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;
}
}
上面从AppClassLoader中查找类是否加载再向上ExtClassLoader查找最后向上BootStrapClassLoader查找的过程 + BootStrapClassLoader到ExtClassLoader再到AppClassLoader的过程。两个过程在一起就是双亲委派机制的全部过程。
不知道大家明白了没有,仔细看一下代码逻辑,还是比较简单的。
那我们还可以想,我们也想自己实现一个类加载器可以不?答案是肯定的
上面我们说了,所有的类加载器最后都实现了ClassLoader类,然后通过loadClass方法来加载类。那我们实现自定义类加载也就很简单了。我们创建一个类基础ClassLoader类,然后直接使用它写好的loadClass()方法就可以了,我们只需要改变一下findClass()方法即可。
public class MyClassLoaderDemo {
public static void main(String[] args) throws Exception {
//初始化自定义加载器,自动将系统的类加载器赋值给myClassLoader的parent属性---AppClassLoader
//问题:怎么跳转到赋值的方法中的???
//初始化自定义类加载器,会先初始化父类ClassLoader,其中会把自定义类加载器的父加载器设置为应用程序类加载器AppClassLoader---也就是以下代码
/**
//父类的构造方法
protected ClassLoader() {
this(checkCreateClassLoader(), getSystemClassLoader());
}
//将系统类加载器赋值给自定义类加载器的parent属性
private ClassLoader(Void unused, ClassLoader parent) {
this.parent = parent;
}
*/
//定义类加载的路径
MyClassLoader myClassLoader = new MyClassLoader("/Users/lulupengcheng/Desktop/ClassLoader");
//使用自定义加载器加载目标类,双亲委派机制
Class clazz = myClassLoader.loadClass("com.itlaobing.jvm.domain.User");
//通过反射调用类的方法
Object obj = clazz.newInstance();
Method method = clazz.getMethod("print", null);
method.invoke(obj, null);
//输出目标类的加载器
System.out.println(clazz.getClassLoader().getClass().getName());
}
//创建一个类继承ClassLoader,重写findClass方法---加载类的方法
static class MyClassLoader extends ClassLoader {
private String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
//转换为字节数组
private byte[] loadByte(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;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
byte[] bytes = loadByte(name);
//加载类的方法---返回Class对象
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
}
这样我们就自己实现了一个类加载器,很简单吧。
那接下来我们玩个大的,我们打破双亲委派机制!!!
如何打破双亲委派机制呢?上面我们看到双亲委派机制的逻辑主要是在loadClass方法中实现的,我们只需要在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);
/*
1.由自定义类加载器获取该类,如果获取不到就直接通过自定义的类加载器加载类,不向上委托父加载器获取或者加载了.
* */
if (c == null) {
long t0 = System.nanoTime();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
//跳过之前的逻辑,如果类没被加载就直接使用我们自己写的findClass方法。
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;
}
}
如果这样执行的话会报错:java.io.FileNotFoundException: /Users/lulupengcheng/Desktop/ClassLoader/java/lang/Object.class (No such file or directory):加载不到Object.class类
因为所有的类都继承Object类,那么加载子类首先需要加载Object类,所以会报异常。
如何解决???
1.在该路径下将Object.class文件拷贝进去----不可以----Java核心库的类是不能在外面被加载的
2.根据包名来判断是否需要打破双亲委派机制如果是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);
/*
1.有自定义类加载器获取该类,如果获取不到就直接通过自定义的类加载器加载类,不向上委托父加载器获取或者加载了.
* */
if (c == null) {
long t0 = System.nanoTime();
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
if (!name.startsWith("com.itlaobing.jvm.domain")){
//如果不是加载com.itlaobing.jvm.domain下面的类就保持双亲委派机制
c = this.getParent().loadClass(name);
}else {
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;
}
}
双亲委派机制的使用场景
Tomcat中打破了双亲委派机制。
为什么Tomcat要打破双亲委派机制呢?
由于一个Tomcat下面可能会部署多个web项目,那么这不同的web项目所使用的技术肯定不一样。就比如Spring,可能会版本的差异。那么如果按照双亲委派机制的话,就只能加载一种版本的Spring相关的类,那显然是不能支持这种场景的。所以需要打破双亲委派机制。不仅如此还有很多原因:
-
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的
不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是 独立的,保证相互隔离。 -
部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程
序,那么要有10份相同的类库加载进虚拟机。 -
web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的 类库和程序的类库隔离开来。
-
web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中 运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改后不用重启。
再看看我们的问题:Tomcat 如果使用默认的双亲委派类加载机制行不行?
答案是不行的。为什么?
第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,默认 的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。
第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
第三个问题和第一个问题一样。
我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp 文件其实也就是class文
件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp
是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想
到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载 器。重新创建类加载器,重新加载jsp文件。
那么我们看看Tomcat内部是怎么做的?
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader 实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的 就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?答案是:违背了。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个 webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
自定义Tomcat的类加载机制—打破双亲委派机制
package com.itlaobing.jvm.classLoader;
import java.io.FileInputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @Author: 路鹏程
* @Date: 2021/5/3
* @Description: //TODO
*/
public class MyClassLoaderDemo {
public static void main(String[] args) throws Exception {
//初始化自定义加载器,将系统的类加载器赋值给myClassLoader的parent属性
MyClassLoader myClassLoader = new MyClassLoader("/Users/lulupengcheng/Desktop/ClassLoader");
//使用自定义加载器加载目标类,双亲委派机制
Class clazz = myClassLoader.loadClass("com.itlaobing.jvm.domain.User");
//通过反射调用类的方法
Object obj = clazz.newInstance();
Method method = clazz.getMethod("print", null);
method.invoke(obj, null);
//输出目标类的加载器
System.out.println("加载器1---"+ clazz.getClassLoader());
MyClassLoader myClassLoader1 = new MyClassLoader("/Users/lulupengcheng/Desktop/ClassLoader1");
//使用自定义加载器加载目标类,双亲委派机制
Class clazz1 = myClassLoader1.loadClass("com.itlaobing.jvm.domain.User");
//通过反射调用类的方法
Object obj1 = clazz.newInstance();
Method method1 = clazz1.getMethod("print", null);
method.invoke(obj1, null);
//输出目标类的加载器
System.out.println("加载器2---"+ clazz1.getClassLoader());
}
/*---------------------------------以下的代码和上面打破双亲委派机制一样---------------------------------*/
结果
自定义类加载器
加载器1---com.itlaobing.jvm.classLoader.MyClassLoaderDemo$MyClassLoader@610455d6
自定义类加载器
加载器2---com.itlaobing.jvm.classLoader.MyClassLoaderDemo$MyClassLoader@5e2de80c