我们知道,直接编写好的类( java 文件)是不能被直接运行的,必须先编译成 class 文件,才能被 JVM 所运行。
今天要聊的就是 JVM 加载 class 文件进内存的过程,也就是 Java Class 的加载过程。
类加载流程
关于类加载的流程,可以简单理解为:将某个 class 文件的内容(例如 String.class 文件)以二进制字节流的形式加载进内存,同时创建一个 Class 类的对象(单例)指向这一部分内容。
随后我们通过类(例如 String)的 .class 属性或者对象的 getClass() 方法访问到该对象。
class 文件记录的是类信息(类权限、类名、父类)、常量内容、字段、方法等内容,加载到内存的二进制字节流也都是这些内容。
通常我们很难直接去访问二进制字节流里的内容,所以 JVM 提供了一个 Class 对象作为入口,帮助我们访问加载到内存的二进制字节流。
类加载阶段
将类加载流程细分的话,主要分为三个阶段:
- loading 将 class 文件以二进制字节流的形式装载进内存,在堆中生成一个代表这个类的 Class 对象。 JVM 并不会在一启动就将所有类加载进内容,而是采用 Lazy Loading 的方式,只有用到该类的时候才会触发 Loading 。同时访问 final 的变量不会触发 Loading ,因为 final 变量本身就不可变。
- linking
- verification 校验 class 文件格式,最基本的是校验魔数(开头是否为 cafe babe)、检验元数据(对字节码描述的信息进行语义分析,确保符合 JVM 规范)、验证字节码(确定程序语义是合法的)和验证符号引用。 如果校验不通过的话,类加载过程中断。在我们可以确保 class 文件正确,可以使用 -Xverfity:none 来关闭验证,加速整个类加载的过程。
- preparation 为静态变量分配内存并赋值(默认值)。注意是默认值而不是初始化值,也就是到了这一步静态变量是处于一个半初始化状态。假如有 static int i = 100; ,到了这一步,i = 0,而不是 i = 100 。
- resolution JVM 将常量池中的符号引用转化为直接引用。
- 符号引用:以一组符号来描述所引用的目标,可以是任何形式的字面量,只要是能无歧义的定位到目标就好。符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。
- 直接引用:直接指向目标的指针、相对偏移量(实例变量、实例方法的直接引用都是偏移量)和间接定位到目标的句柄。能使用直接引用的目标必定已经被加载入内存中了。 例如: public class ProxyClass { public void doSomething() { TargetClass.doSomething(); } } 在 ProxyClass 类的二进制数据中,包含了一个对 TargetClass 类的 doSomething() 方法的符号引用,它由 doSomething() 方法的全名和相关描述符组成。 在 resolution 阶段,JVM 会把这个符号引用替换为一个指针,该指针指向 TargetClass 类的 doSomething() 方法在方法区的内存位置,这个指针就是直接引用。
- initializing 这一步将按编写顺序执行静态内容(包括初始化静态变量、执行静态代码块)。 也就是将静态变量赋值为初始值。假如有 static int i = 100; ,到了这一步,i = 100 。
类加载器
所有的类都是被类加载器加载进内存的,而类加载器本身也是一个 class 。
不同的类会被不同的类加载器所加载,JVM 自带了三种类加载器:
- Bootstrap
最顶层的 ClassLoader ,主要用作加载核心类(如 java.lang 包下的内容)和 C++ 实现。可以在启动 JVM 时指定 -Xbootclasspath 和路径来改变 Bootstrap ClassLoader 的加载目录。
当调用一个类的 getClassLoader() 方法时,如果返回结果为 null ,则说明该类是由
Bootstrap ClassLoader 加载。
例如:
// 以下类均位于 java.lang 包下 System.out.println(System.class.getClassLoader()); System.out.println(Thread.class.getClassLoader()); System.out.println(String.class.getClassLoader()); System.out.println(Integer.class.getClassLoader()); System.out.println("============"); // 所有由 Bootstrap ClassLoader 加载的类的包名 String property = System.getProperty("sun.boot.class.path"); System.out.println(property.replaceAll(":", System.getProperty("line.separator"))); 复制代码
输出结果为:
null null null null ============ %JRE_HOME%/lib/resources.jar %JRE_HOME%/lib/rt.jar %JRE_HOME%/lib/sunrsasign.jar %JRE_HOME%/lib/jsse.jar %JRE_HOME%/lib/jce.jar %JRE_HOME%/lib/charsets.jar %JRE_HOME%/lib/jfr.jar %JRE_HOME%/classes 复制代码
可见核心类库均由 Bootstrap ClassLoader 负责加载,这些核心类库被记录在 sun.boot.class.path 系统属性中。
至于为什么要将 Bootstrap ClassLoader 加载的类的 getClassLoader() 返回 null 。是因为 Bootstrap 类加载器是由 C++ 实现,在 Java 层没有对应 class 与之对应。
- Extension
加载拓展 jar 包(如 jre/lib/ext/*jar 包下内容)中的 class 类的类加载器。
Extension ClassLoader 本身是被 Bootstrap ClassLoader 加载进来的,而 Extension ClassLoader 的 parent ClassLoader 也指向 Bootstrap ClassLoader。
System.out.println(sun.net.spi.nameservice.dns.DNSNameService.class.getClassLoader()); System.out.println("============"); // 所有由 Extension ClassLoader 加载的类的包名 String extProperty = System.getProperty("java.ext.dirs"); System.out.println(extProperty.replaceAll(":", System.getProperty("line.separator"))); 复制代码
输出结果为:
sun.misc.Launcher$ExtClassLoader@3339ad8e ============ %USER_HOME%/Library/Java/Extensions %JRE_HOME%/lib/ext /Library/Java/Extensions /Network/Library/Java/Extensions /System/Library/Java/Extensions /usr/lib/java 复制代码
所有由 Extension ClassLoader 负责加载的包,被记录在 java.ext.dirs 系统属性中,可以使用 -D java.ext.dirs 选项指定的目录。
- App
加载当前应用的 CLASSPATH 的所有类。
App ClassLoader 是被 Bootstrap ClassLoader 加载进来,而 App ClassLoader 的 parent ClassLoader 指向 Extension ClassLoader
System.out.println(com.peterxx.Person.class.getClassLoader()); System.out.println("============"); String extProperty = System.getProperty("java.class.path"); System.out.println(extProperty.replaceAll(":", System.getProperty("line.separator"))); 复制代码
输出结果为:
sun.misc.Launcher$AppClassLoader@18b4aac2 ============ ... /Your/Project/Path/target/test-classes /Your/Project/Path/target/classes .. 复制代码
基本上除了 JDK 的核心类库和拓展包以外的类,都由 AppClassLoader 负责加载。包括我们由 Maven 导入的第三方类。
双亲委派
先不解释何为“双亲委派”,从我们如何使用一个 ClassLoader 加载一个类进行入手,理解类加载的整个过程。
当我们要使用一个 ClassLoader 对象去加载类的时候,是调用该 ClassLoader 对象的 loadClass() 方法,深入对应源码:
public abstract class ClassLoader { private final ClassLoader parent; private ClassLoader(Void var1, ClassLoader var2) { this.parent = var2; // 第二个参数作为 parent // 准备其他数据结构 ... } protected ClassLoader(ClassLoader var1) { // 初始化传入的 var1 作为 parent this(checkCreateClassLoader(), var1); } public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { /** * 防止同一个类被重复加载,加载的类是单例。 * 使用 synchronized 上锁,同时这里使用的是每一个类使用一个 Lock(以类名作为 key,锁对象作为 value 。存放在一个 ConcurrentHashMap 中),实现一个类只会被加载一次的同时多个类可以被同时加载。 */ synchronized (getClassLoadingLock(name)) { // 先在当前的类加载器中检查该类是否已经被加载过,底层使用的是 findLoadedClass0 方法,如果有被加载过直接返回 Class<?> c = findLoadedClass(name); if (c == null) { long t0 = System.nanoTime(); try { if (parent != null) { // 如果在当前类加载器中没有找到,先委托 parent 对应的类加载器尝试进行类加载 c = parent.loadClass(name, false); } else { // 如果当前类没有 parent ,代表需要委托给 Bootstrap 加载器进行加载。 // 通过 parent 为空来代表应该往上委托给 Bootstrap ,而不使用 parent 指向 Bootstrap 加载器,是因为 Bootstrap 并不是在 Java 层实现的,在 Java 层没有对应的类 c = findBootstrapClassOrNull(name); } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } // 如果还是没有则开始尝试真正的加载类 if (c == null) { long t1 = System.nanoTime(); // 默认实现为抛出 ClassNotFoundException 异常,该方法由具体的 ClassLoader 子类实现 c = findClass(name); // 记录统计数据 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c); } return c; } } } 复制代码
使用 ClassLoader 进行类加载,整体流程为:
- 先从当前类加载器中查找该类是否已经被加载过,有则直接返回加载结果;
- 没有则调用 parent.loadClass(name, false); 开始往上逐层委托,直到 parent 为空,代表传给了 Bootstrap ClassLoader;
- 从 Bootstrap ClassLoader 开始尝试加载,加载成功则返回;
- 没有则将按照原本的从下往上的委托层级原路返回,从上往下的尝试加载;
- 如果到最后还是加载不到对应类,则抛出 ClassNotFoundException 异常。
在不自定义 ClassLoader 的前提下,默认的委托关系是:AppClassLoader -> ExtClassLoader -> Bootstrap ClassLoader
这个委托关系是在 sum/misc/Launcher 中建立的,同时 AppClassLoader 和 ExtClassLoader 都是 Launcher 的内部类:
public class Launcher { ... // 默认的 ClassLoader private ClassLoader loader; public static Launcher getLauncher() { return launcher; } public Launcher() { // 声明一个 ExtClassLoader Launcher.ExtClassLoader var1; try { // 实例化 ExtClassLader,并不传入 parent ,则当 ExtClassLoader 往上委托的时候会给到 Bootstrap ClassLoader var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { // 设定 AppClassLoader 为默认 ClassLoader ,并设置 ExtClassLoader 为其的 parent this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } // 准备 SecurityManager ... } ... static class AppClassLoader extends URLClassLoader { // 限定了 AppClassLoader 的查找路径 ... } static class ExtClassLoader extends URLClassLoader { // 限定了 ExtClassLoader 的查找路径 ... } } 复制代码
注意:AppClassLoader 的 parent 为 ExtClassLoader,而 ExtClassLoader 的 parent 为 Bootstrap ClassLoader。但 AppClassLoader 和 ExtClassLoader 本身都是由 Bootstrap ClassLoader 加载的。
public static void main(String[] args) throws Exception { Class dnsClazz = sun.net.spi.nameservice.dns.DNSNameService.class; Class customClazz = com.peterxx.Person.class; // 打印 ExtClassLoader System.out.println(dnsClazz.getClassLoader()); // 打印 AppClassLoader System.out.println(customClazz.getClassLoader()); // 打印 null,代表会往上委托给 Bootstrap ClassLoader System.out.println(dnsClazz.getClassLoader().getParent()); // 打印 ExtClassLoader System.out.println(customClazz.getClassLoader().getParent()); // 打印 null,代表 ExtClassLoader 本身是由 Bootstrap ClassLoader 负责加载 System.out.println(dnsClazz.getClassLoader().getClass().getClassLoader()); // 打印 null,代表 AppClassLoader 本身是由 Bootstrap ClassLoader 负责加载 System.out.println(customClazz.getClassLoader().getClass().getClassLoader()); } 复制代码
在翻读了源码之后,再来看看关于“双亲委派”的定义:如果一个类加载器收到了类加载请求,它首先不会自己尝试去加载这个类,而是把这个请求委派给 parent 类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器(Bootstrap ClassLoader),只有 parent 类加载器反馈无法完成这个加载请求,子加载器才会尝试自己去加载。
使用“双亲委派”模式进行类加载的目的是为了安全,保证同一个类总是被同一个 ClassLoader 所加载。保证核心类库不能够被篡改或者相同的类被重复加载。
自定义 ClassLoader
如何自定义 ClassLoader
自定义 ClassLoader 分两种情况讨论:
- 自定义的 ClassLoader 仍然遵循“双亲委派”模式,需要完成的操作为:
- 继承 ClassLoader 类
- 重写 findClass 方法
- 将对应的 class 文件内容读出来放到字节数组中
- 调用 defineClass 方法将字节数组转换为 Class 对象并返回
- class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 自定义 ClassLoader 的查找类逻辑 ... return super.findClass(name); } }
- 自定义的 ClassLoader 需要打破“双亲委派”模式,需要完成的操作为:
- 继承 ClassLoader 类
- 重写 loadClass 方法
class CustomClassLoader extends ClassLoader { @Override protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { /** * 自定义 ClassLoader 的加载类逻辑 * 可以不调用 findLoadedClass 去检查是否已经被加载,选择每次都重新加载(实现热部署) * 可以不调用 parent 的 findClass 方法,打破“双亲委派”模式 */ return super.loadClass(name, resolve); } }
我们知道如果使用自定义的 ClassLoader 的话,委托关系为:自定义的 ClassLoader -> AppClassLoader -> ExtClassLoader -> Bootstrap ClassLoader
到这里其实会有一个疑问,自定义的 ClassLoader 是如何和 AppClassLoader 联系起来的 ?我们并没有指定自定义 ClassLoader 的 parent 。其实可以从源码中可以得到答案:
public abstract class ClassLoader { ... protected ClassLoader() { // 只要继承了 ClassLoader,默认构造方法会使用 getSystemClassLoader() 作为 parent this(checkCreateClassLoader(), getSystemClassLoader()); } @CallerSensitive public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } private static synchronized void initSystemClassLoader() { ... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { // 在 initSystemClassLoader 方法里面,会使用 sun.misc.Launcher 里的 classLoader ,而这个 classLoader 则为 AppClassLoader scl = l.getClassLoader(); } ... } ... } 复制代码
自定义 ClassLoader 的作用
- 对 class 文件进行加密,一定程度上能防止反编译 具体操作就是将编译所得的 class 文件加载成二进制字节流,并字节流内容进行加密,再重新存成加密文件。 在自定义 ClassLoader 的 findClass 方法中读取加密文件,对内容进行解密,得到原 class 文件的字节流内容之后再调用 defineClass 方法转换为 Class 对象。
- 打破“双亲委派”模式 直接选择重写 loadClass 方法,完全自定义 ClassLoader 的加载类逻辑。可以绕过只重写 findClass 方法无法避开的 findLoadedClass(name) 方法和 parent.loadClass(name, false) ,打破“双亲委派”模式。
- 实现多个具有相同类名的类能够处于同一空间
SPI 机制
SPI 全称为 Service Provider Interface,是 JDK 内置的一种服务提供发现机制。主要是解决接口和具体实现的硬编码问题,彻底实现“面向接口编程”:JDK 提供接口,供应商提供具体的实现,用户面向接口编程。
以 JDBC 为例,Java 定义了接口 java.sql.Driver 作为数据库链接规范。但并没有具体的实现,具体的实现都是由不同厂商提供,即由具体的数据库厂商(Mysql、PostgreSQL)提供实现。
那么问题是位于内层的 JDBC 是如何发现位于外层的具体实现的?使用的正是 SPI 机制。
SPI 的开发流程:
- 定义一个接口作为标准
- 编写这个接口的实现类,并在打包文件(jar 包)中的 META-INF/services/ 目录中,以接口的全类名称作为文件名,实际实现类的全类名作为内容,建立配置文件
- 通过 java.util.ServiceLoader 类的 load 方法,传入接口名称,获得实现类的实例对象
具体例子:
- 定义一个接口作为标准
package java.sql;
public interface Driver { Connection connect(String url, java.util.Properties info) throws SQLException;
boolean acceptsURL(String url) throws SQLException; DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info) throws SQLException; int getMajorVersion(); int getMinorVersion(); boolean jdbcCompliant(); public Logger getParentLogger() throws SQLFeatureNotSupportedException; 复制代码
- }
2.1 编写实现类
package com.mysql.cj.jdbc; import java.sql.DriverManager; import java.sql.SQLException; public class Driver extends NonRegisteringDriver implements java.sql.Driver { public Driver() throws SQLException { } static { try { DriverManager.registerDriver(new Driver()); } catch (SQLException var1) { throw new RuntimeException("Can't register driver!"); } } } 复制代码
2.2 将实现类打包,并编写配置文件
// mysql 的 jar 包里面有 META-INF/services/java.sql.Driver 配置文件 ➜ mysql-connector-java-7.0.19 tree META-INF/services META-INF/services └── java.sql.Driver 0 directories, 1 file // 文件内容是具体的实现类类名 ➜ mysql-connector-java-7.0.19 cat META-INF/services/java.sql.Driver com.mysql.cj.jdbc.Driver% 复制代码
- 实际使用
package java.sql;
public class DriverManager { private static void loadInitialDrivers() { ... AccessController.doPrivileged(new PrivilegedAction() { public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); ... } 复制代码
- }
我们知道 java.sql.Driver 是在 JDK 包内,由 Bootstrap ClassLoader 负责加载,而由 MySQL 编写的 com.mysql.cj.jdbc.Driver 实现类是由属于第三方实现,应当由 AppClassLoader 加载。
网上不少资料说 SPI 打破了双亲委派机制,但是事实上 com.mysql.cj.jdbc.Driver 仍然由 AppClassLoader 加载,只有 java.sql.Driver 才是由 Bootstrap ClassLoader 加载。