Java类加载
一. Java类加载
Java 的类加载需要完成三件事情:
1)通过一个类的全限定名来获取定义此类的二进制字节流。
2)将这个二进制字节流转换为方法区的运行时数据结构。
3)在堆中创建一个java.lang.Class对象的实例。
总结成一句话就是,读取符合字节码(.class文件)格式的二进制字节流读取到方法区中保存,并在堆中创建一个class对象的实例用于创建类实例。
何时加载?
JVM虚拟机规范并没有强制约束何时加载类,由各个虚拟机自己实现。主流的JVM实现并不会在程序启动时就把所有的.class文件等都加载一遍,这样会消耗很多的时间,而是等用到了这个类再去加载。
如何加载?
JVM的类加载由类加载器完成,JDK提供了一个抽象类ClassLoader,这个抽象类中定义了三个重要的方法和一个重要的字段:
public abstract class ClassLoader { //每个类加载器都有个父加载器 private final ClassLoader parent; public Class<?> loadClass(String name) { //查找一下这个类是不是已经加载过了 Class<?> c = findLoadedClass(name); //如果没有加载过 if( c == null ){ //先委托给父加载器去加载,注意这是个递归调用 if (parent != null) { c = parent.loadClass(name); }else { // 如果父加载器为空,查找Bootstrap加载器是不是加载过了 c = findBootstrapClassOrNull(name); } } // 如果父加载器没加载成功,调用自己的findClass去加载 if (c == null) { c = findClass(name); } return c; } protected Class<?> findClass(String name){ //1. 根据传入的类名name,到在特定目录下去寻找类文件,把.class文件读入内存 ... //2. 调用defineClass将字节数组转成Class对象 return defineClass(buf, off, len); } // 将字节码数组解析成一个Class对象,用native方法实现 protected final Class<?> defineClass(byte[] b, int off, int len){ ... } }
从源码中我们可以看到这样几个信息:
1)加载器是有父加载器的,要注意这里的父加载器是通过parent字段以组合的形式实现的,而不是通过继承。注:组合和继承的优缺点可自行百度组合设计模式。
2)loadClass方法会优先递归交给parent父加载器去加载类,如果父加载器没找到才由自己加载,这个就是面试常问的问题-双亲委派机制,稍后我们会详细讲解。
3)findClass方法的作用就是类加载器在自己的查找范围内寻找".class文件",可能来源于文件、数据库甚至是网络。(对应了Java 的类加载需要完成三件事情的第一件和第二件)
4)defineClass是个工具方法,它会调用native方法将字节码解析成一个class对象。(对应了Java 的类加载需要完成三件事情的第三件)
注:上诉ClassLoader的三个方法中,只有findClass方法没有提供默认实现,如果要自定义一个简单的类加载器,继承ClassLoader并重写实现findClass方法即可。
二. JVM类加载器
JDK自带了三种类加载器:
1)BootstrapClassLoader:启动类加载器,用来加载核心类库,如rt.jar(java基础类库java.time.、java.nio.、java.lang、java.util)、resource.jar等。通过-Xbootclasspath 参数可以完成指定操作
2)ExtentionClassLoader:扩展类加载器,用来加载lib/ext目录下的jar包和.class文件。同样的,通过系统变量java.ext.dirs可以指定这个变量。
3)AppClassLoader:系统类加载器,用来加载classpath下的类,应用程序默认用它来加载类,例如执行 Class.forName("类名")时。
除此之外,你还可以定义自己的类加载器,实现方法上一节有介绍。
类加载器的层级关系:自定义类加载器 -》AppClassLoader-》ExtentionClassLoader-》BootstrapClassLoader。
类加载器的双亲委派机制:
既然每个类加载器都有自己的加载范围,那么它们的加载顺序就至关重要。想象这样一个情景,有人在开源项目中写了一个Object类,如果你用了这个开源项目,那么他写的Object类会替换掉JRE的Object类吗?如果能替换的话,其后果不堪设想。
JVM为了保护自己的核心类库不被人为的篡改,提供了双亲委派机制。双亲委派的源码在ClassLoader#loadClass方法中。具体的逻辑是,首先由AppClassLoader从缓存中查找是否已经被加载过。如果缓存中没有就由其父加载器也就是ExtentionClassLoader查询缓存。如果ExtentionClassLoader的缓存中也没有,就交给BootstrapClassLoader查询缓存,如果缓存还没有,BootstrapClassLoader会尝试在自己的范围内加载这个类,如果加载成功直接返回,如果加载失败再交给子类加载器去它的加载范围内加载。
三. 打破双亲委派机制
既然双亲委派机制的逻辑写在ClassLoader#loadClass里,那么只要重写这个方法就可以打破这个机制了。
3.1 Tomcat
Tomcat通过自定义的类加载器WebAppClassLoader打破双亲委派机制。它首先自己尝试去加载某个类,如果找不到再交给父类加载器,其目的是优先加载Web自己定义的类。具体实现就是重写 ClassLoader 的两个方法:findClass 和 loadClass。
findClass方法源码:
public Class<?> findClass(String name) throws ClassNotFoundException { ... Class<?> clazz = null; try { //1. 先在Web应用目录下查找类 clazz = findClassInternal(name); } catch (RuntimeException e) { throw e; } if (clazz == null) { try { //2. 如果在本地目录没有找到,交给父加载器去查找 clazz = super.findClass(name); } catch (RuntimeException e) { throw e; } //3. 如果父类也没找到,抛出ClassNotFoundException if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
在findClass方法里,主要有三个步骤。
1. 先在Web应用本地目录下查找要加载的类。
2. 如果没有找到,交给父加载器去查找,它的父加载器就是上面提到的系统类加载器 AppClassLoader。
3. 如何父加载器也没找到这个类,抛出 ClassNotFound 异常。
loadClass 方法
接着我们再来看 Tomcat 类加载器的 loadClass 方法的实现,同样我也去掉了一些细节:
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> clazz = null; //1. 先在本地cache查找该类是否已经加载过 clazz = findLoadedClass0(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } //2. 从系统类加载器的cache中查找是否加载过 clazz = findLoadedClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } // 3. 尝试用ExtClassLoader类加载器类加载,为什么? ClassLoader javaseLoader = getJavaseClassLoader(); try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 4. 尝试在本地目录搜索class并加载 try { clazz = findClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } // 5. 尝试用系统类加载器(也就是AppClassLoader)来加载 try { clazz = Class.forName(name, false, parent); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } //6. 上述过程都加载失败,抛出异常 throw new ClassNotFoundException(name); }
loadClass 方法稍微复杂一点,主要有六个步骤:
1. 先在本地 Cache 查找该类是否已经加载过,也就是说 Tomcat 的类加载器是否已经加载过这个类。
2. 如果 Tomcat 类加载器没有加载过这个类,再看看系统类加载器是否加载过。
3. 如果都没有,就让 ExtClassLoader 去加载,这一步比较关键,目的防止 Web 应用自己的类覆盖 JRE 的核心类。因为 Tomcat 需要打破双亲委托机制,假如 Web 应用里自定义了一个叫 Object 的类,如果先加载这个 Object 类,就会覆盖 JRE 里面的那个 Object 类,这就是为什么 Tomcat 的类加载器会优先尝试用 ExtClassLoader 去加载,因为 ExtClassLoader 会委托给 BootstrapClassLoader 去加载,BootstrapClassLoader 发现自己已经加载了 Object 类,直接返回给 Tomcat 的类加载器,这样 Tomcat 的类加载器就不会去加载 Web 应用下的 Object 类了,也就避免了覆盖 JRE 核心类的问题。
4. 如果 ExtClassLoader 加载器加载失败,也就是说 JRE 核心类中没有这类,那么就在本地 Web 应用目录下查找并加载。
5. 如果本地目录下没有这个类,说明不是 Web 应用自己定义的类,那么由系统类加载器去加载。这里请你注意,Web 应用是通过Class.forName调用交给系统类加载器的,因为Class.forName的默认加载器就是系统类加载器。
6. 如果上述加载过程全部失败,抛出 ClassNotFound 异常。
从上面的过程我们可以看到,Tomcat 的类加载器打破了双亲委托机制,没有一上来就直接委托给父加载器,而是先在本地目录下加载,为了避免本地目录下的类覆盖 JRE 的核心类,先尝试用 JVM 扩展类加载器 ExtClassLoader 去加载。那为什么不先用系统类加载器 AppClassLoader 去加载?很显然,如果是这样的话,那就变成双亲委托机制了,这就是 Tomcat 类加载器的巧妙之处。
3.2 SPI机制
SPI是Java提供的一套用于第三方实现或扩展的接口,它约定服务提供者在jar包下的 META-INF/services/ 目录下创建一个以服务名全限定名命名的文件,并在文件中写上此服务接口的实现。当服务启动时就会去检查这个目录下的文件,并通过反射将配置的实现类实例化出来以供使用。
SPI接口的定义一般是在BoostrapClassLoader或者ExtClassLoader下的,如果让SPI通过反射去实例化AppClassLoader中的实现类显然不可能。怎么解决?用AppClassLoader去实例化就好了。现在问题变成了如何让SPI拿到AppClassLoader。答案是通过线程Thread对象,将AppClassLoader保存在Thread中,那么在同一线程内加载的SPI也就可以获取到这个AppClassLoader了。不知你有没有发现,Thread这个类很多情况下都有保存传递数据的功效,你也可以在代码里使用它。