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 方法的实现流程主要为:
- 调用 findLoadedClass 方法检查目标类是否被加载过,如果未加载过,则进行下面的加载步骤
- 如果存在父加载器,则调用父加载器的loadClass 方法加载类
- 父加载类不存在时,调用 JVM 内部的 ClassLoader 加载类
- 经过 2,3 步骤,若还未成功加载类,则使用该 ClassLoader 自身的 findClass 方法加载类
- 最后根据入参 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[] {};
}
}