类加载机制及双亲委派模型
虚拟机的类加载机制:
虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型(Java字节码);
一个类的完整的声明 周期包括以下几个阶段:
加载、连接(验证、准备、解析)、初始化、使用、卸载;
类的加载过程
有一篇博客专门介绍过类的加载过程,这里就不再详细展开了,加载大致是通过类的全限定名获取对应类的二进制字节流,然后将静态的存储结构转换为运行时的动态存储结构,生成一个class对象,作为访问这个类的方法的入口;连接过程包括验证、准备、解析;验证就是检查字节码文件是否符合虚拟机的要求,比如是否是cafe babe开头,检查数据流和控制流是否符合规则,是否会危害虚拟机的正常运行,符号引用对应的直接引用是否存在(是否能够正常解析引用关系等等),初始化过程(clinit<>,不是每个类都必须存在,只是当有类静态变量或者静态代码块static时才会有),会按照代码的顺序收集静态变量的赋值语句,进行赋值操作;使用过程不详细赘述;类的卸载,判断类是否可以卸载的条件比较苛刻,而且就算满足了也不一定会卸载,(1)虚拟机堆中不存在这个类的任何实例;(2)对应类的类加载器已经卸载;(3)不存在该类的clazz对象,保证无法在任何地方通过反射访问到该类;
本篇文章重点梳理以下双亲委派模型和如何才能够破坏双亲委派模型呢?
首先何为双亲委派模型呢?
从Java程序员角度来看类加载器,包括三种类型:启动类加载器(Bootstrap ClassLoader),负责加载JAVA_HOME/lib下的基础类;扩展类加载器,负责加载JAVA_HOME/lib/ext下的相关类,系统类加载器(应用程序类加载器),如果没有特殊指定类加载器,一般就是通过系统类加载器进行类的加载的;
双亲委派模型:
除了启动类加载器以外,任何类加载器都要有自己的父类加载器,当收到一个类的加载请求的时候,先将类加载请求委托给父类加载器,只有当父类加载器无法完成加载的时候,才会由子类加载器去尝试加载;
需要注意的是,这里类加载器的父子关系是通过组合的形式来实现的,而不是通过继承;
接下来通过源代码来看一下双亲委派模型的原理:
private final ClassLoader parent; public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { // 类加载需要先获取锁,防止多个线程并发加载类时会有线程不安全的问题 synchronized (getClassLoadingLock(name)) { // 看这个类是否已经完成了加载 Class<?> c = findLoadedClass(name); if (c == null) { try { if (parent != null) { // 如果父类加载器存在且不为空,就先调用父类的LoadClass尝试加载 c = parent.loadClass(name, false); } else { // 父类为空证明是启动类加载器,调用启动类加载器进行加载 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // not found exception catch } } if (c == null) { // 如果c仍然是空,说明父类都没有办法完成加载,则自己尝试去加载类 c = findClass(name); } } if (resolve) { resolveClass(c); } }
整个逻辑就类似于注释中写的那样,父类加载器不为空的情况下不断向上传递,交给父类加载器去完成类的加载,只有当父类无法加载的情况下,才会去交给子类尝试加载,而子类尝试加载这个类使用的是
findClass()方法,所以通常情况下,子类加载器只需要继承ClassLoader,然后重写findClass方法就可以按照双亲委派模型的机制来完成类的加载了,父类加载不了自然会调用自己重写的findClass方法来进行类的加载;
我们以一个文件系统类加载器为例,看下如何实现自定义的类加载器;
public class FileSystemClassLoader extends ClassLoader{ private String rootDir; public FileSystemClassLoader(String rootDir) { this.rootDir = rootDir; // 保存类的系统路径初始化 } protected Class<?> findClass(String name) throws ClassNotFoundException { // 通过重写findclass方法由子类类加载器进行类的加载 byte[] classDate = getClassData(name); // 获取类加载的二进制字节类数据 if (classDate == null) { throw new ClassNotFoundException(); } else { return defineClass(name, classDate, 0, classDate.length); // 调用defineclass进行类的加载 } }
// 从文件中进行类的加载过程 public byte[] getClassData(String className) { String path = classNameToPath(className); try { InputStream in = new FileInputStream(path); ByteArrayOutputStream baos = new ByteArrayOutputStream(); int bufferSize = 4096; byte[] buffer = new byte[bufferSize]; int bytesNumRead; while (bytesNumRead = in.read(buffer) != -1) { baos.write(buffer, 0, bytesNumRead); } return baos.toByteArray(); } catch (IOException e) { e.printStackTrace(); } return null; } }
最后,如果想要破坏双亲委派的类加载机制的话,需要怎么做呢?
通过分析源码我们发现,其实双亲委派的逻辑都是在loadClass()中实现的,如果我们想要破坏双亲委派模型,只需要让自定义类加载器实现ClassLoader类,然后重写loadClass()方法,就可以不按照双亲委派的机制实现类的加载了!