前言
Java里有如下几种类加载器
- 启动类加载器:负责加载支撑JVM运行的位于JRE的lib目录下的核心类库比如 rt.jar、charsets.jar等。
- 扩展类加载器(ExtClassLoader):负责加载支撑JVM运行的位于JRE的lib目录下的ext扩展目录中的JAR类包。
- 应用程序类加载器(AppClassLoader):负责加载ClassPath路径下的类包,主要就是加载你自己写的那些类。
- 自定义加载器:负责加载用户自定义路径下的类包。
通过以下实例来了解各个类加载器:
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(Object.class.getClassLoader());
// java提供的与DNS服务交互的api
System.out.println(DNSNameService.class.getClassLoader());
System.out.println(ClassLoaderTest.class.getClassLoader());
}
}
运行结果如下:
null
sun.misc.Launcher$ExtClassLoader@6d6f6e28
sun.misc.Launcher$AppClassLoader@58644d46
启动类加载器是在有jvm底层创建的实例,所以在获取时为null,Object类是有启动类加载器进行加载的,所以获取其加载器时为null,而DNSNameService为JAVA_HOME/jre/lib目录下ext文件夹在的dnsns.jar包中的类,由扩展类加载器(ExtClassLoader)加载。而自己编写的类ClassLoaderTest 则由AppClassLoader进行加载。
Java中各个类加载器的层次关系
在上面已经介绍过,java的类加载器也是普通的类,ExtClassLoader和AppClassLoader均是URLClassLoader的子类,而URL的继承关系如下:
那么AppClassLoader和ExtClassLoader为ClassLoader的子类。在上面已经已经介绍过在jvm启动时会通过sun.misc.Launcher的getLauncher方法从而获取Launcher的实例,那么在这个过程中Launcher会通过构造方法创建该类的实例。sun.misc.Launcher的构造方法如下:
public Launcher() {
Launcher.ExtClassLoader var1;
try {
// 创建ExtClassLoader
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
// 创建AppClassPoader
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
// 省略代码 ....
}
}
通过分析sun.misc.Launcher构造方法我们知道在sun.misc.Launcher类的实例创建是会创建AppClassLoader实例和ExtClassLoader实例。同时由于两个类加载器均继承自ClassLoader,而ClassLoader中有一个ClassLoader的全局变量parent,该类类型也是ClassLoader:
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// 省略代码 ....
}
而在sun.misc.Launcher创建时,实例化ExtClassLoader和AppClassLoader时均指定其parent属性分别为null和ExtClassLoader。那么java中的类加载器的机构就如下:
自定义类加载器
自定义类加载器需要继承 java.lang.ClassLoader 类,该类有两个核心方法,一个是 loadClass(String, boolean),实现了双亲委派机制,大体逻辑
首先,检查一下指定名称的类是否已经加载过,如果加载过了,就不需要再 加载, 直接返回。
如果此类没有加载过,那么,再判断一下是否有父加载器;如果有父加载器, 则由父加载器加载(即调用parent.loadClass(name, false);).或者是调用 Bootstrap类加载器来加载。
如果父加载器及bootstrap类加载器都没有找到指定的类,那么调用当前类 加载器 的findClass方法来完成类加载。还有一个方法是findClass,默认 实现是抛出异常,所以我们自定义类加载器主要是重写 findClass方法。
接下来看一个示例,首先我们编写需要自定义类加载器加载的类,如下:
package com.dp.jvm;
import java.io.PrintStream;
public class User
{
public void say()
{
System.out.println("hello");
}
}
需要注意的是,该类编写完成需要在工程中删除,避免AppClassLoader加载。编译完成后将该类的class文件放置指定的目录下:
然后编写自定义的类加载器,代码如下:
class MyClassLoader extends ClassLoader{
private final String path;
MyClassLoader(String path) {
this.path = path;
}
/**
* 重写ClassLoader的findClass方法,获取到类的Class对象
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] byteArrayFromClassName = getByteArrayFromClassName(name);
return defineClass(name, byteArrayFromClassName, 0, byteArrayFromClassName.length);
}
/**
* 通过类的全限定名称获取到类的二进制数据
* @param name
* @return
*/
private byte[] getByteArrayFromClassName(String name) {
String classPath = convertNameToPath(name);
byte[] data = null;
int off = 0;
int length = 0;
try(BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(classPath))) {
data = new byte[bufferedInputStream.available()];
while ((length = bufferedInputStream.read(data, off, data.length - off)) > 0) {
off += length;
}
} catch (Exception ex) {
ex.printStackTrace();
}
return data;
}
/**
* 通过类的全限定名称获取到对应类文件的的字节码文件路径
* @param name
* @return
*/
private String convertNameToPath(String name) {
String relativePath = name.replace(".", File.separator);
String absolutePath = path + File.separator + relativePath + ".class";
return absolutePath;
}
}
编写测试类,通过使用自定义的类加载将User加载并实例化,然后调用其say方法,如下:
public class CustomClassLoaderTest {
public static void main(String[] args) throws Exception {
MyClassLoader myClassLoader = new MyClassLoader("F:\\test");
Class<?> clazz = myClassLoader.loadClass("com.dp.jvm.User");
Object o = clazz.newInstance();
Method say = clazz.getDeclaredMethod("say");
say.invoke(o);
}
}
通过上面了实例,简单的实现了一个自定义的类加载器。接留下来了解一下类加载器的双亲委派机制。
双亲委派机制
JVM类加载器是有亲子层级结构的,如下图:
需要注意的是,这里的额亲子层级结构不是指的java中的继承关系,而是每一个类加载实现类都具有一个parent全局变量,而该全局变量的类型为ClassLoader。这里可能有一个疑问,在自定义类加载器中并未看到名称parent的全局变量。这是因为这个全局变量是在ClassLoader中定义声明的。
public abstract class ClassLoader {
private static native void registerNatives();
static {
registerNatives();
}
// The parent class loader for delegation
// Note: VM hardcoded the offset of this field, thus all new fields
// must be added *after* it.
private final ClassLoader parent;
// 省略代码 ....
还有一个问题也需要说明一下,我们自定义的类加载器的parent属性是如何设置的呢?怎么知道设置的为AppClassLoader呢?因为自定义的类加载器继承自ClassLoader,而ClassLoader中有一个无参的构造函数,如下:
protected ClassLoader() {
//调用有参构造函数
this(checkCreateClassLoader(), getSystemClassLoader());
}
private ClassLoader(Void unused, ClassLoader parent) {
//设置父加载器
this.parent = parent;
if (ParallelLoaders.isRegistered(this.getClass())) {
parallelLockMap = new ConcurrentHashMap<>();
package2certs = new ConcurrentHashMap<>();
domains =
Collections.synchronizedSet(new HashSet<ProtectionDomain>());
assertionLock = new Object();
} else {
// no finer-grained lock; lock on the classloader instance
parallelLockMap = null;
package2certs = new Hashtable<>();
domains = new HashSet<>();
assertionLock = this;
}
}
从ClassLoader的实现来看,通过getSystemClassLoader()方法获取系统类加载器然后将其赋值给parent属性。那么来看一下getSystemClassLoader()具体实现:
public static ClassLoader getSystemClassLoader() {
initSystemClassLoader();
if (scl == null) {
return null;
}
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
checkClassLoaderPermission(scl, Reflection.getCallerClass());
}
return scl;
}
// 初始化系统类加载器
private static synchronized void initSystemClassLoader() {
if (!sclSet) {
if (scl != null)
throw new IllegalStateException("recursive invocation");
sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
if (l != null) {
Throwable oops = null;
scl = l.getClassLoader();
try {
scl = AccessController.doPrivileged(
new SystemClassLoaderAction(scl));
} catch (PrivilegedActionException pae) {
oops = pae.getCause();
if (oops instanceof InvocationTargetException) {
oops = oops.getCause();
}
}
if (oops != null) {
if (oops instanceof Error) {
throw (Error) oops;
} else {
// wrap the exception
throw new Error(oops);
}
}
}
sclSet = true;
}
}
在getSystemClassLoader方法中获取sun.misc.Lanucher实例(单例),然后调用其getClassLoader方法获取系统类加载器,然后设置给parent方法。最后来看一下sun.misc.Lanucher的getClassLoader方法:
public ClassLoader getClassLoader() {
return this.loader;
}
public Launcher() {
Launcher.ExtClassLoader var1;
try {
var1 = Launcher.ExtClassLoader.getExtClassLoader();
} catch (IOException var10) {
throw new InternalError("Could not create extension class loader", var10);
}
try {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
}
结合sun.misc.Lanucher的getClassLoader和构造方法可知系统类加载器就是AppClassLoader。
在了解了jvm中类加载器的组成结构后,我们再来看一下jvm中各个类加载器的组成的结构:
在了解了jvm中各个类加载器的层次结构之后,加下来来解析双亲委派机制就相对来说简单多了,首先从双亲委派的流程说起。
双亲委派流程
双亲委派流程如下:
加载某个类时会先委托父加载器寻找目标类,找不到再委托上层父加载器加载,如果所有父加载器在自己的加载类路径下都找不到目标类,则在自己的类加载路径中查找并载入目标类。
比如我们的PrintTest 类,最先会找应用程序类加载器加载,应用程序类加载器会先委托扩展类加载器加载,扩展类加载器再委托启动类加载器,顶层启动类加载器在自己的类加载路径里找了半天没找到PrintTest 类,则向下退回加载PrintTest 类的请求,扩展类加载器收到回复就自己加载,在自己的类加载路径里找了半天也没找到PrintTest 类,又向下退回PrintTest 类的加载请求给应用程序类加载器,应用程序类加载器于是在自己的类加载路径里找Math类,结果找到了就自己加载了。
双亲委派机制说简单点就是,先找父亲加载,不行再由儿子自己加载。
那么为什么要设置双亲委派机制呢?
- 沙箱安全机制:自己写的java.lang.String.class类不会被加载,这样便可以防止核心API库被随意篡改。
- 避免类的重复加载:当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次,保证被加载类的唯一性
双亲委派机制源码剖析
双亲委派的原理体现在ClassLoader的loadClass方法中:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先检查类是够已经被加载
Class<?> c = findLoadedClass(name);
// 类还未加载则加载,使用双亲委派机制
if (c == null) {
long t0 = System.nanoTime();
try {
// 判断当前类加载器是否设置了父加载器,设置了则
// 调用父加载器的loadClass进行加载,如果父加载也是ClassLoader
// 的子类则会再次进入该方法,判断是否有父类加载器,依次递归
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 当类加载没有设置parent父加载,那么就使用启动类加载器加载
// 由于启动类加载器是底层创建的实例,所以该方法会调用本地
// native方法
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
// 向上委派所有父加载器仍然没有加载到参数类,那么调用当前
// 类加载器进行类的加载
long t1 = System.nanoTime();
c = findClass(name);
// ... 省略
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
打破双亲委派
以Tomcat类加载为例,Tomcat 如果使用默认的双亲委派类加载机制行不行?
我们思考一下:Tomcat是个web容器, 那么它要解决什么问题:
- 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一 个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份, 因此要保证每个应用程序的类库都是独立的,保证相互隔离。比如:在tomcat容器中存在存在两个应用A和B,A使用的是Spring4,而应用B使用的是Spring5,如果使用双亲委派,那么可能会导致版本冲突从而报错,如果在版本4中不存在x方法,但是先加载了版本4的字节码,那么版本5的就不会在加载了(类限定名相同),那么在程序B中调用x方法则会抛出方法不存在异常。
- 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。我们常见的web应用中的Servlet依赖,一般在maven中依赖作用于都是provided的,web程序都是使用的容器的Serlvert,如果都是各自的那么造成类的重复加载。
- web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
- web容器要支持jsp的修改,我们知道,jsp文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,web容器需要支持 jsp修改后不用重启.了解jsp机制的都知道,是将jsp解析成一个对应的Servlet(就是常说的一个jsp就是一个servlet),jsp就是通过动态生成.class文件从而实现动态资源的。
再看看我们的问题:
Tomcat 如果使用默认的双亲委派类加载机制行不行?
- 第一个问题,如果使用默认的类加载器机制,那么是无法加载两个相同类库的不同版本的,
默认的类加器是不管你是什么版本的,只在乎你的全限定类名,并且只有一份。 - 第二个问题,默认的类加载器是能够实现的,因为他的职责就是保证唯一性。
- 第三个问题和第一个问题一样。
- 我们再看第四个问题,我们想我们要怎么实现jsp文件的热加载,jsp文件其实也就是class文件,那么如果修改了,但类名还是一样,类加载器会直接取方法区中已经存在的,修改后的jsp是不会重新加载的。那么怎么办呢?我们可以直接卸载掉这jsp文件的类加载器,所以你应该想到了,每个jsp文件对应一个唯一的类加载器,当一个jsp文件修改了,就直接卸载这个jsp类加载器。重新创建类加载器,重新加载jsp文件。
最后
感谢你看到这里,看完有什么的不懂的可以在评论区问我,觉得文章对你有帮助的话记得给我点个赞,每天都会分享java相关技术文章或行业资讯,欢迎大家关注和转发文章!