一、JVM 类加载机制
1. JVM 类加载机制共有五步,分别是加载、验证、准备、解析、初始化。下面简单地介绍一下每个步骤。
加载:从硬盘中查找并通过 IO 流读取字节码文件至 JVM 的方法区,并在堆中创建一个 java.lang.Class 对象的实例作为方法区中这些数据的访问入口。
验证:确保字节码文件符合当前虚拟机的规范。
准备:为类中的类变量在方法区中分配内存,并进行初始化。对于 final static 关键字修饰的变量,在编译阶段就分配了内存,所以在准备阶段就不会分配。
解析:将符号引用转换成直接引用。
初始化:为类变量赋值并执行静态代码块。
这五步均会由类加载器完成。
2. 有四种情况会触发类的加载,分别是:
(1) 类中有入口方法 main
(2) new 一个类
(3) 使用反射机制加载实例化一个类
(4) 加载子类,但是父类还没有加载,就会触发父类的类加载。
二、双亲委派机制。
双亲委派机制是 Java 虚拟机加载一个类时为该类确定类加载器的一种机制。
1. 三种类加载器及其负责加载的类
有三种类加载器,分别是 引导类加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)、应用类加载器(AppClassLoader),下面简单介绍一下每个类加载器所加载类。
(1) 引导类加载器(BootstrapClassLoader):加载 jre/lib 目录下的核心类库,例如 charsets.jar rt.jar
其中 rt.jar 包中有我们经常使用的各种包
(2) 扩展类加载器(ExtClassLoader):加载 jre/lib/ext 目录下的 jar 包
(3) 应用类加载器(AppClassLoader):加载用户自定义 classpath 下的字节码文件。
代码:打印相应包下的类的类加载器来验证:
// Class getClassLoader() 返回该类的类加载器 System.out.println(String.class.getClassLoader()); System.out.println(sun.net.spi.nameservice.dns.NDSNameService.class.getClassLoader().getClass().getName()); System.out.println(MyTest.class.getClassLoader().getClass().getName());
运行结果:
null // 因为引导类加载器的底层是由 C++ 实现的,java 无法识别 sun.misc.Launcher$ExtClassLoader // ExtClassLoader 是 Launcher 的静态内部类 sun.misc.Launcher$AppClassLoader // AppClassLoader 是 Launcher 的静态内部类
在双亲委派机制中,扩展类加载器是应用类加载器的父加载器,同时,引导类加载器是扩展类加载器的父加载器。虽然有父子关系,但是三个类加载器中并没有继承关系。
引导类加载器是由 C++ 实现的,而扩展类加载器和应用类加载器则是由 Java 实现的。先记住这一点,下面会继续说明。
2. 从运行程序开始介绍类加载的关键流程。
(1) 在 Windows 平台下开始运行一个 Java 程序,首先 java.exe 会调用底层 vm.dll 文件创建一个 Java 虚拟机,Java 虚拟机的底层是由 C++ 来实现的;
(2) 接着创建引导类加载器,引导类加载器也是 C++ 实现的,引导类加载器会负责加载 jre/lib 目录下的类库;
(3)引导类加载器加载 sun.misc.Launcher 类,这是 JVM 启动类,在创建这个类的同时会创建其它的类加载器,例如扩展类加载器、应用类加载器、自定义的类加载器。
(4) 需要加载一个类时通过 launcher.getClassLoader() 方法获取类加载器,系统默认的类加载器是 应用类加载器(AppClassLoader) ;
(5) 调用类加载器的 loadClass(String name) 方法对 类进行加载,这其中就会实现双亲委派机制;
(6) 类加载完成后,调用 main 入口方法,执行程序;
(7) Java 程序执行结束后 销毁 JVM。
3. 双亲委派机制的流程。
例如有一个类 com.potuo.load.HelloWorld 需要加载,在双亲委派机制下流程是怎么样的呢?
(1) 首先 应用类加载器 会判断之前是否加载过这个类,如果加载过则返回,如果没有加载过,则会向上委托,也就是委托给扩展类加载器;
(2) 扩展类加载器同样会去判断之前是否加载过这个类,如果加载过则返回,如果没有加载过,则继续向上委托,也就是委托给 引导类加载器;
(3) 引导类加载器判断之前是否加载过这个类,如果加载过则返回,如果没有加载过则会去 jre/lib 目录下去查询是否有这个类,如果有这个类则加载,如果没有就向下回传给 扩展类加载器;
(4) 扩展类加载器去 jre/lib/ext 目录下去查询是否有这个类,如果有这个类则加载,如果没有这个类就向下会传给 应用类加载器;
(5) 应用类加载器会去自定义的 classPath 下去查询是否有这个类,如果有这个类则加载,如果没有这个类就会抛出 ClassNotFoundException。
Q1:为什么需要双亲委派机制进行类加载?
(1) 避免重复加载。当父加载器已经加载过该类,就没有必要再让子加载器进行加载,保证加载类的唯一性。(三个加载器都遍历判断是否加载过,如果都没有加载过才会对该类进行加载)
(2) 沙箱安全机制。自己写的 java.lang.String 类不会被加载,这样可以防止核心 API 库被随意篡改。
注意: java.lang 禁止使用自定义加载器加载,由于沙箱安全机制
Q2:是否可以省略前面从 AppClassLoader 向*问至 BootstrapClassLoader 的过程,而直接从 BootstrapClassLoader 直接向下加载,寻找适合的类加载器?
排除类被回收的可能,类都是只会被加载一次的。
当类第一次被加载的时候,由下往上进行委托的行为的确意义不大;
但是程序中需要运行的类大部分都是用户自己写的,也就是应该由应用类加载器进行加载的类,而且往往这些类创建不止一次。如果这时,每次都需要由上往下遍历完 jre/lib 目录,又遍历 jre/lib/ext 目录才发现都不在这两个目录下面,而是应该交给 应用类加载器进行加载,那么这是效率就比较低了。
Q3:源码分析,类加载器何时被创建?
(1) 引导类加载器基于 C++,在调用 Java 程序前就被创建。
(2) 扩展类加载器 和 应用类加载器在创建 启动器实例 sun.misc.Launcher 的时候就会被创建。下面从源码说明这一点:
首先,扩展类加载器 和 应用类加载器都时 Launcher 的静态内部类
它们都继承了 URLClassLoader,根据继承关系图可以看到,URLClassLoader 继承了 ClassLoader
public class Launcher { // 应用类加载器 static class AppClassLoader extends URLClassLoader { // ... } // 扩展类加载器 static class ExtClassLoader extends URLClassLoader { // ... }
}
查看 Launcher 的无参构造方法就可以发现,扩展类加载器和应用类加载器就是在 Launcher 创建时就会创建。
创建 扩展类加载器,接着创建 应用类加载器,将扩展类加载器传给应用类加载器作为应用类加载器的父加载器;
将应用类加载器设置为默认加载器,并设置当前运行线程的上下文 ClassLoader 为 应用类加载器。
public class Launcher { private static Launcher launcher = new Launcher(); private ClassLoader loader; public static Launcher getLauncher() { return launcher; } // C++ 调用 Java 代码创建 JVM 启动类实例 sun.misc.Launcher // 由引导类加载器负责加载 // 同时创建其它类加载器(扩展类加载器、应用类加载器) 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); } // 设置该线程的上下文 ClassLoader。 // 上下文 ClassLoader 可以在创建线程设置, // 并允许创建者在加载类和资源时向该线程中运行的代码提供适当的类加载器。 Thread.currentThread().setContextClassLoader(this.loader); String var2 = System.getProperty("java.security.manager"); if (var2 != null) { SecurityManager var3 = null; if (!"".equals(var2) && !"default".equals(var2)) { try { var3 = (SecurityManager)this.loader.loadClass(var2).newInstance(); } catch (IllegalAccessException var5) { } catch (InstantiationException var6) { } catch (ClassNotFoundException var7) { } catch (ClassCastException var8) { } } else { var3 = new SecurityManager(); } if (var3 == null) { throw new InternalError("Could not create SecurityManager: " + var2); } System.setSecurityManager(var3); } } public ClassLoader getClassLoader() { return this.loader; } }
Q4:源码分析,类加载器如何设置父加载器?
(1) AppClassLoader 设置 ExtClassLoader 为其父加载器的过程
最终设置 ClassLoader 中的 parent 属性的值。
public class Launcher { static class AppClassLoader extends URLClassLoader { public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { return new Launcher.AppClassLoader(var1x, var0); // step1 } } AppClassLoader(URL[] var1, ClassLoader var2) { super(var1, var2, Launcher.factory); //step2 this.ucp.initLookupCache(this); } } class URLClassLoader{ public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(parent);// step3 } } class SecureClassLoader{ protected SecureClassLoader(ClassLoader parent) { super(parent); // step4 } } class ClassLoader{ private final ClassLoader parent; protected ClassLoader(ClassLoader parent) { this(checkCreateClassLoader(), parent); // step5 } private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; //step6 } }
(2) ExtClassLoader 同理,不同的是,ExtClassLoader 设置的父加载器是 Null
在上面代码第二步中,var2 为 null
public ExtClassLoader(File[] var1) throws IOException { super(getExtURLs(var1), (ClassLoader)null, Launcher.factory); // step2 SharedSecrets.getJavaNetAccess().getURLClassPath(this).initLookupCache(this); }
Q5:源码分析,双亲委派机制的关键代码实现。
关键代码在 ClassLoader 类的 loadClass 方法中。
首先判断这个类是否被当前的类加载器所加载过,如果没有,则委托给父加载器;
如果父加载器加载返回结果为 Null,则说明父加载器无法加载,则
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // 首先 ,判断这个类是否已经被加载了 Class<?> c = findLoadedClass(name); if (c == null) { // 没有被加载 long t0 = System.nanoTime(); try { if (parent != null) { // 判断是否有父加载器 // 继续用父加载器加载,父加载器返回加载结果 // c = null 则父加载器无法加载 c = parent.loadClass(name, false); } else { c = findBootstrapClassOrNull(name); // 在引导类加载器中寻找 } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } 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; } }
Q6:面试题. 假设类路径下,ext*.jar、 rt.jar 三类中都有 A.class,那么 A.class 是否会被加载3次,如果不会,它的加载顺序是什么样的? 答案:不会被加载3次,并最终由 BootstrapClassLoader 来加载 A.class
结合源码分析: 1. AppClassLoader 类加载器调用 loadClass 方法加载类的时候,会判断曾经是否加载过 A.class ,加载过则直接返回,没有加载过则调用父加载器的 (ExtClassLoader) 的loadClass() 方法。 2. ExtClassLoader 加载器同样会判断是否曾经加载过 A.class,加载过的话则直接返回,没有加载过则调用父加载器(BootstrapClassLoader)加载器判断是否加载过, 3. BootstrapClassLoader 加载过则返回, 4. 如果没有加载过则调用 findClass 方法去指定的包中搜索 A.class 找到则返回, 5. 没找到则交由 ExtClassLoader调用 findClass 方法去指定的包中搜索 A.class ,找到则返回, 6. 没找到则交由 AppClassLoader 调用 findClass 方法进行查找, 7. 如果找不到则抛出 ClassNotFoundException 异常。
三、手写自定义类加载器打破双亲委派机制。
1. 自定义类加载器(继承 ClassLoader)
也就是说 自定义的类加载器其父加载器是 应用类加载器(AppClassLoader),因为在 ClassLoader 的无参构造器中为其定义的父加载器为系统默认加载器,即应用类加载器。
protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); } private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; // ... }
2. 应用场景。
在 Tomcat 中,可能会有两个 war 包,一个基于 spring4,一个基于 sprint5,可以预见到的是,这两个包中一定有大量的类拥有相同的包名和类名,却有不同的底层实现,这时就需要改写类加载器,以便到对应的目录下加载 spring4 或者 spring5 的类。
3. 自定义加载路径,重写 findClass(String) 方法;打破双亲委派机制,重写 loadClass() 方法,禁止调用父加载器
假设加载 com.potou.User类,war1 的字节码文件在 test1 目录下,war2 的字节码文件在 test2 下
public class User { public void say(){ System.out.println("===spring5==="); } }
首先我们可以先看一下源码是怎么写的,findClass(String) 方法在 URLClassLoader 类中有比较完整的实现
public class URLClassLoader extends SecureClassLoader implements Closeable { protected Class<?> findClass(final String name) throws ClassNotFoundException { final Class<?> result; try { result = AccessController.doPrivileged( new PrivilegedExceptionAction<Class<?>>() { public Class<?> run() throws ClassNotFoundException { String path = name.replace('.', '/').concat(".class"); Resource res = ucp.getResource(path, false); // 得到资源路径下的字节码文件 if (res != null) { try { return defineClass(name, res); } catch (IOException e) { throw new ClassNotFoundException(name, e); } } else { return null; } } }, acc); } catch (java.security.PrivilegedActionException pae) { throw (ClassNotFoundException) pae.getException(); } if (result == null) { throw new ClassNotFoundException(name); } return result; } }
我们可以从自定义目录下查找类,以字节流的方式读取,然后再将字节数组转化成 Class 对象,并禁止调用父加载器。
package com.potou; import java.io.FileInputStream; public class MyClassLoader extends ClassLoader { private String classPath; public MyClassLoader(String classPath) { this.classPath = classPath; } private byte[] getByte(String name) throws Exception { name = name.replaceAll("\\.", "/"); FileInputStream fileInputStream = new FileInputStream(classPath + "/" + name + ".class"); int len = fileInputStream.available(); byte[] data = new byte[len]; fileInputStream.read(data); fileInputStream.close(); return data; } protected Class<?> findClass(String name) throws ClassNotFoundException { try { byte[] data = getByte(name); // defineClass 将一个字节数组转化成 Class 对象,这个字节数组是 class 文件读取后最终的字节数组 return defineClass(name,data,0,data.length); }catch (Exception e){ e.printStackTrace(); throw new ClassNotFoundException(); } } 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) { long t0 = System.nanoTime(); // 禁止调用 父加载器 // try { // if (parent != null) { // c = parent.loadClass(name, false); // } else { // c = findBootstrapClassOrNull(name); // } // } catch (ClassNotFoundException e) { // // ClassNotFoundException thrown if class not found // // from the non-null parent class loader // } 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; } } }
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException { MyClassLoader myClassLoader1 = new MyClassLoader("d:/load/test1"); Class clazz1 = myClassLoader1.loadClass("com.potou.User"); Method method1 = clazz1.getDeclaredMethod("say",null); Object obj1 = clazz1.newInstance(); method1.invoke(obj1,null); MyClassLoader myClassLoader2 = new MyClassLoader("d:/load/test2"); Class clazz2 = myClassLoader2.loadClass("com.potou.User"); Method method2 = clazz2.getDeclaredMethod("say",null); Object obj2 = clazz2.newInstance(); method2.invoke(obj2,null); }
4. 重写 loadClass(String name,boolean resolve) 方法
重写完 上述方法之后还有一个问题,现在用同一个类加载器 MyClassLoader 去加载两个路径下的字节码文件,需要注意的是,User 类有父类 java.lang.Object ,而这个类并不在当前目录下
所以你有两个方法可以选择,一是将所有可能用到的类都放在 d:/load/test1 和 d:/load/test2 目录下;二是仍然使用引导类加载器和扩展类加载器加载这些类,也就是说仍然要使用 双亲委派机制。
这里当然选择第二种。
那么久就需要改写 loadClass 方法,当加载的类的包名不是 com.potou 时,使用父加载器进行加载
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) { long t0 = System.nanoTime(); // 调用 父加载器 if(!name.startsWith("com.potou")){ c = this.getParent().loadClass(name); } // try { // if (parent != null) { // c = parent.loadClass(name, false); // } else { // c = findBootstrapClassOrNull(name); // } // } catch (ClassNotFoundException e) { // // ClassNotFoundException thrown if class not found // // from the non-null parent class loader // } 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; } }注意: java.lang 禁止使用自定义加载器加载,由于沙箱安全机制 注意:自定义类会有 Object 类等,像这些类不需要打破双亲委派机制
本篇博客参考 B 站视频,链接如下:
https://www.bilibili.com/video/BV1ZA411J7Ec?p=1