Java 类加载机制分析

在编写 Java 程序时,我们所编写的 .java 文件经编译后,生成能被 JVM 识别的 .class 文件,.class 文件以字节码格式存储类或接口的结构描述数据。JVM 将这些数据加载至内存指定区域后,依此来构造类实例。

1. 类加载过程

JVM 将来自 .class 文件或其他途径的类字节码数据加载至内存,并对数据进行验证、解析、初始化,使其最终转化为能够被 JVM 使用的 Class 对象,这个过程称为 JVM 的类加载机制。

2. ClassLoader

ClassLoader 是 Java 中的类加载器,负责将 Class 加载到 JVM 中,不同的 ClassLoader 具有不同的等级,这将在稍后解释。

2.1 ClassLoader的作用

ClassLoader 的作用有以下 3点:

  • 将 Class 字节码解析转换成 JVM 所要求的 java.lang.Class 对象
  • 判断 Class 应该由何种等级的 ClassLoader 负责加载
  • 加载 Class 到 JVM中

2.2 ClassLoader的主要方法

ClassLoader 中包含以下几个主要方法:

  • defineClass

    protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    

    作用:将 byte 字节流转换为 java.lang.Class 对象。
    说明:字节流可以来源于.class文件,也可来自网络或其他途径。调用 defineClass 方法时,会对字节流进行校验,校验不通过会抛出 ClassFormatError 异常。该方法返回的 Class 对象还没有 resolve(链接),可以显示调用 resolveClass 方法对 Class 进行 resolve,或者在 Class 真正实例化时,由 JVM 自动执行 resolve.

  • resolveClass

    protected final void resolveClass(Class<?> c)
    

    作用 :对 Class 进行链接,把单一的 Class 加入到有继承关系的类树中。

  • findClass

    Class<?> findClass(String name)
    

    作用:根据类的 binary name,查找对应的 java.lang.Class 对象。
    说明:binary name 是类的全名,如 String 类的 binary name 为 java.lang.String。findClass 通常和 defineClass 一起使用,下面将举例说明二者关系。
    举例:java.net.URLClassLoader 是 ClassLoader 的子类,它重写了 ClassLoader中的 findClass 和 defineClass 方法,我们看下 findClass 的主方法体。

    // 入参为 Class 的 binary name,如 java.lang.String
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        // 以上代码省略
      
        // 通过 binary name 生成包路径,如 java.lang.String -> java/lang/String.class
        String path = name.replace('.', '/').concat(".class");
        // 根据包路径,找到该 Class 的文件资源
        Resource res = ucp.getResource(path, false);
        if (res != null) {
            try {
               // 调用 defineClass 生成 java.lang.Class 对象
                return defineClass(name, res);
            } catch (IOException e) {
                throw new ClassNotFoundException(name, e);
            }
        } else {
            return null;
        }
      
        // 以下代码省略
    }
    
  • loadClass

     public Class<?> loadClass(String name)
    

    作用:加载 binary name 对应的类,返回 java.lang.Class 对象
    说明:loadClass 和 findClass 都是接受类的 binary name 作为入参,返回对应的 Class 对象,但是二者在内部实现上却是不同的。loadClass 方法实现了 ClassLoader 的等级加载机制。我们看下 loadClass 方法的具体实现:

    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;
      }
    }
    

    loadClass 方法的实现流程主要为:

    1. 调用 findLoadedClass 方法检查目标类是否被加载过,如果未加载过,则进行下面的加载步骤
    2. 如果存在父加载器,则调用父加载器的loadClass 方法加载类
    3. 父加载类不存在时,调用 JVM 内部的 ClassLoader 加载类
    4. 经过 2,3 步骤,若还未成功加载类,则使用该 ClassLoader 自身的 findClass 方法加载类
    5. 最后根据入参 resolve 判断是否需要 resolveClass,返回 Class 对象

    loadClas 默认是同步方法,在实现自定义 ClassLoader 时,通常的做法是继承 ClassLoader,重写 findClass 方法而非 loadClass 方法。这样既能保留类加载过程的等级加载机制和线程安全性,又可实现从不同数据来源加载类。

3. ClassLoader 的等级加载机制

上文已经提到 Java 中存在不同等级的 ClassLoader,且类加载过程中运用了等级加载机制,下面将进行详细解释。

3.1 Java 中的四层 ClassLoader

  • Bootstrap ClassLoader
    又称启动类加载器。Bootstrap ClassLoader 是 Java 中最顶层的 ClassLoader,它负责加载 JDK 中的核心类库,如 rt.jar,charset.jar,这些是 JVM 自身工作所需要的类。

    Bootstarp ClassLoader 由 JVM 控制,我们无法访问到这个类。虽然它位于类记载器的顶层,但它没有子加载器。需要通过 native 方法,来调用 Bootstap ClassLoader 来加载类,如下:

    private native Class<?> findBootstrapClass(String name);
    

    以下代码能够输出 Bootstrap ClassLoader 加载的类库路径:

    System.out.print(System.getProperty("sun.boot.class.path"));
    
    运行结果:
    C:\Software\Java8\jre\lib\resources.jar;
    C:\Software\Java8\jre\lib\rt.jar;
    C:\Software\Java8\jre\lib\jsse.jar;
    C:\Software\Java8\jre\lib\jce.jar;
    C:\Software\Java8\jre\lib\charsets.jar;
    C:\Software\Java8\jre\lib\jfr.jar;
    C:\Software\Java8\src.zip
    
  • Ext ClassLoader

    又称扩展类加载器。Ext ClassLoader 负责加载 JDK 中的扩展类库,这些类库位于 /JAVA_HOME/jre/lib/ext/ 目录下。如果我们将自己编写的类打包丢到该目录下,则该类将由 Ext ClassLoader 负责加载。

    以下代码能够输出 Ext ClassLoader 加载的类库路径:

    System.out.println(System.getProperty("java.ext.dirs"));
    
    运行结果:
    C:\Software\Java8\jre\lib\ext;
    C:\Windows\Sun\Java\lib\ext
    

    这里自定义了一个类加载器,全名为 com.eric.learning.java._classloader.FileClassLoader,我们想让它能够由 Ext ClassLoader加载,需要进行如下步骤:

    • 在 /JAVA_HOME/jre/lib/ext/ 目录下按照类的包结构新建目录
    • 将编译好的 FileClassLoader.class 丢到目录 /JAVA_HOME/jre/lib/ext/com/eric/learning/java/_classloader 下
    • 运行命令 jar cf test.jar com,生成 test.jar
    • 现在就可以用 ExtClassLoader 来加载类 FileClassLoader 了
      ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();
      Class<?> clazz = classLoader.loadClass("com.eric.learning.java._classloader.FileClassLoader");
      System.out.println(clazz.getName());
      

      ClassLoader.getSystemClassLoader() 获得的是 Ext ClassLoader 的子加载器, App ClassLoader

  • App ClassLoader

    继承关系图

    又称系统类加载器,App ClassLoader 负责加载项目 classpath 下的 jar 和 .class 文件,我们自己编写的类一般有它负责加载。App ClassLoader 的父加载器为 Ext ClassLoader。

    以下代码能够输出 App ClassLoader 加载的 .class 和 jar 文件路径:

     System.out.println(System.getProperty("java.class.path"));
    
    运行结果:
    C:\Coding\learning\target\classes;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-core\2.8.8\jackson-core-2.8.8.jar;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-databind\2.8.8\jackson-databind-2.8.8.jar;
    C:\Users\huizhuang\.m2\repository\com\fasterxml\jackson\core\jackson-annotations\2.8.8\jackson-annotations-2.8.8.jar
    

    笔者的项目通过 Maven 来管理,\target\class 是 Maven 工程里 .class 文件的默认存储路径,其余如 jackson-core-2.8.8.jar 是通过 Maven 引入的第三方依赖包。

  • Custom ClassLoader
    自定义类加载器,自定义类加载器需要继承抽象类 ClassLoader 或它的子类,并且所有 Custom ClassLoader 的父加载器都是 AppClassLoader,下面简单解释下这点。

    抽象类 ClassLoader 中有2种形式的构造方法:

    // 1
    protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
    // 2
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    

    构造器1 以 getSystemClassLoader() 作为父加载器,而这个方法返回的即是 AppClassLoader。
    构造器2 表面上看允许我们指定当前类加载器的parent,但是如果我们试图将 Custom ClassLoader 的构造方法写成如下形式:

    public class FileClassLoader extends ClassLoader {
        public FileClassLoader(ClassLoader parent) {
            super(parent);
        }
    }
    

    在构造 FileClassLoader 实例时,new FileClassLoader( ClassLoader ) 将抛出异常:

    Java 的 security manager 不允许自定义类构造器访问上述的 ClassLoader 的构造方法。

3.2 等级加载机制

​ 如同我们在抽象类 ClassLoader 的 loadClass 方法所看到那样,当通过一个 ClassLoader 加载类时,会先自底向上检查父加载器是否已加载过该类,如果加载过则直接返回 java.lang.Class 对象。如果一直到顶层的 BootstrapClassLoader 都未加载过该类,则又会自顶向下尝试加载。如果所有层级的 ClassLoader 都未成功加载类,最终将抛出 ClassNotFoundException。如下图所示:


3.3 为何采用等级加载机制

​ 首先,采用等级加载机制,能够防止同一个类被重复加载,如果父加载器已经加载过某个类,再次加载时会直接返回 java.lang.Class 对象。

​ 其次,不同等级的类加载器的存在能保证类加载过程的安全性。如果只存在一个等级的 ClassLoader,那么我们可以用自定义的 String 类替换掉核心类库中的 String 类,这会造成安全隐患。而现在由于在 JVM 启动时就会加载 String 类,所以即便存在相同 binary name 的 String 类,它也不会再被加载。

4. 从 JVM 角度看类加载过程

​ 在 JVM 加载类时,会将读取 .class 文件中的类字节码数据,并解析拆分成 JVM 能识别的几个部分,这些不同的部分都将被存储在 JVM 的 方法区。然后 JVM 会在 堆区 创建一个 java.lang.Class 对象,用来封装该类在方法区的数据。 如下图所示:

​ 上文提到 .class 文件中的类字节码数据,会被 JVM 拆分成不同部分存储在方法区,而方法区实际就是用于存储类结构信息的地方。我们看看方法区都有哪些东西:

-   类及其父类的 binary name
-   类的类型 (class or interface)
-   访问修饰符 (public,abstract,final 等)
-   实现的接口的全名列表
-   常量池
-   字段信息
-   方法信息
-   静态变量
-   ClassLoader 引用
-   Class 引用

​ 方法区存储的这些类的各部分结构信息,能通过 java.lang.Class 类中的不同方法获得,可以说 Class 对象是对类结构数据的封装。

5. 一个简单的自定义类加载器例子

// 传入 .class 文件的绝对路径,加载 Class
public class FileClassLoader extends ClassLoader {

    // 重写了 findClass 方法
    @Override
    public Class<?> findClass(String path) throws ClassNotFoundException {
        File file = new File(path);
        if (!file.exists()) {
            throw new ClassNotFoundException();
        }
        
        byte[] classBytes = getClassData(file);
        if (classBytes == null || classBytes.length == 0) {
            throw new ClassNotFoundException();
        }
        return defineClass(classBytes, 0, classBytes.length);
    }

    private byte[] getClassData(File file) {
        try (InputStream ins = new FileInputStream(file); ByteArrayOutputStream baos = new  
             ByteArrayOutputStream()) {
            byte[] buffer = new byte[4096];
            int bytesNumRead = 0;
            while ((bytesNumRead = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new byte[] {};
    }
}

推荐阅读更多精彩内容