Java基础-类加载器以及加载机制

我是在关于Java的面试题里了解到类加载器的,在这以前从未想过Java里类是如何被加载、解析的,一直觉得只要Import就行了。事实上Java类加载器是一块很是重要的内容,能够用在类层次划分、OSGi、热部署、代码加密等领域。即便业务上可能没有涉及到,了解相关知识对排除BUG也是有帮助的。java

类加载器基本概念

平时在编写代码时,想使用什么类就Import就行了,好像这些类一开始就在JVM里了同样,如今咱们知道这是由于JVM自动为咱们加载了这些类。顾名思义,类加载器的工做主要是加载Java字节码文件(也就是.class文件)到虚拟机里,并解析为java.lang.Class类的一个实例。到这里,被加载的类仍是不能像平时同样直接new一个对象出来的。由于一个类总共要经历加载、验证、解析、初始化等4个步骤后才是Java里的一个类型。后面几个步骤不是本文重点,你们能够自行学习。面试

类加载器的组成

类加载器一共有4种,分别是引导类加载器(bootstrap class loader)、扩展类加载器(extensions class loader)、系统类加载器(system class loader)、自定义加载器,它们之间的加载关系以下图所示:bootstrap

其中,除了引导类加载器是用原生代码实现,其他的加载器都是继承自抽象类java.lang.ClassLoader。并且系统自带的3个加载器都有本身的特殊之处。安全

引导类加载器

引导类加载器是用来加载Java的核心库,像是java.lang包等这些Java应用必备的类都是引导类加载器加载的。加载路径是<JAVA_HOME>\lib目录中的或者是-Xbootclasspath参数所指定的目录中,被JVM所识别的文件(经过名字识别,名字必须是rt.jar)。由于引导类加载器是用原生代码实现的,因此不能在Java代码中直接引用到引导类加载器。bash

扩展类加载器

顾名思义,扩展类加载器是用来加载Java的扩展类库。加载路径是<JAVA_HOME>\lib\ext目录中的或者是java.ext.dirs系统变量所指定的路径中的全部类库。markdown

系统类加载器

系统类加载器的加载路径是Java应用的类路径(CLASSPATH),也就是说在没有自定义加载器的状况下,Java应用的类都是由系统类加载器加载的。并且该加载器能够用ClassLoader类的getSystemClassLoader()方法直接获取到。架构

除了引导类加载器,每一个加载器都有一个父加载器。好比加载器A加载了加载器B,那么加载器A就是加载器B的父加载器,能够经过java.lang.ClassLoadergetParent()方法获取父加载器,并且Java中每一个Class对象都维护着一个加载器引用,能够经过getClassLoader()方法获取加载该类的加载器。oop

例以下面这段代码:学习

public class Main {

    public static void main(String[] args) {
        ClassLoader loader = Main.class.getClassLoader();
        while (loader != null) {
            System.out.println(loader.toString());
            loader = loader.getParent();
        }
    }

}
复制代码

这里输出了Main类的加载器与其全部的父加载器,运行结果:this

sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@1540e19d

Process finished with exit code 0
复制代码

咱们看到Main类的加载器是系统类加载器,它的父加载器是扩展类加载器。扩展类加载器的父加载器应该是引导类加载器才对,这里没有输出是由于有些JDK的实现里在父加载器为引导类加载器的状况下是返回null的。

双亲委托模式

第一次看到双亲委托模式这个词的时候就感受意义不明,彻底不知道是什么意思。在了解了加载器的加载过程以后,才发现是一种代理模式。

以上文中的Main类的加载过程为例,它的加载器为系统类加载器。可是系统类加载器不会直接去加载这个类,而是先委托给它的父加载器,也就是扩展类加载器。一样,扩展类加载器也会先委托给它的父加载器,一直委托到引导类加载器才开始真正的尝试加载,若是加载失败就返回由发出委托的加载器尝试加载。

这样作的目的是为了保护Java核心库和保持类型安全。由于在JVM中判断两个类是否相同,不只仅是看它们的全名是否相同,还要判断它们的加载器是否相同。经过双亲委托模式就能保证每次加载核心库的加载器都是引导类加载器,从而防止出现相似于多个java.lang.Object类型这种状况。

自定义加载器

编写自定义加载器并不困难,只要继承抽象类java.lang.ClassLoader并覆盖findClass(String name)方法就好了。不建议覆盖 loadClass(String name)方法,由于这个方法里面封装了前面提到的双亲委托模式,覆盖可能会致使该模式失效。

// 源码来自 https://www.ibm.com/developerworks/cn/java/j-lo-classloader
public class FileSystemClassLoader extends ClassLoader { 
 
   private String rootDir; 
 
   public FileSystemClassLoader(String rootDir) { 
       this.rootDir = rootDir; 
   } 
 
   protected Class<?> findClass(String name) throws ClassNotFoundException { 
       byte[] classData = getClassData(name); 
       if (classData == null) { 
           throw new ClassNotFoundException(); 
       } 
       else { 
           return defineClass(name, classData, 0, classData.length); 
       } 
   } 
 
   private byte[] getClassData(String className) { 
       String path = classNameToPath(className); 
       try { 
           InputStream ins = new FileInputStream(path); 
           ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
           int bufferSize = 4096; 
           byte[] buffer = new byte[bufferSize]; 
           int bytesNumRead = 0; 
           while ((bytesNumRead = ins.read(buffer)) != -1) { 
               baos.write(buffer, 0, bytesNumRead); 
           } 
           return baos.toByteArray(); 
       } catch (IOException e) { 
           e.printStackTrace(); 
       } 
       return null; 
   } 
 
   private String classNameToPath(String className) { 
       return rootDir + File.separatorChar 
               + className.replace('.', File.separatorChar) + ".class"; 
   } 
}
复制代码

参考资料

Java类加载机制与Tomcat类加载器架构

深刻探讨 Java 类加载器