Java Class 加载过程| Java Debug 笔记

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: Java Class 加载过程| Java Debug 笔记

我们知道,直接编写好的类( 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
  1. verification 校验 class 文件格式,最基本的是校验魔数(开头是否为 cafe babe)、检验元数据(对字节码描述的信息进行语义分析,确保符合 JVM 规范)、验证字节码(确定程序语义是合法的)和验证符号引用。 如果校验不通过的话,类加载过程中断。在我们可以确保 class 文件正确,可以使用 -Xverfity:none 来关闭验证,加速整个类加载的过程。
  2. preparation 为静态变量分配内存并赋值(默认值)。注意是默认值而不是初始化值,也就是到了这一步静态变量是处于一个半初始化状态。假如有 static int i = 100; ,到了这一步,i = 0,而不是 i = 100 。
  3. 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 进行类加载,整体流程为:


  1. 先从当前类加载器中查找该类是否已经被加载过,有则直接返回加载结果;
  2. 没有则调用 parent.loadClass(name, false); 开始往上逐层委托,直到 parent 为空,代表传给了 Bootstrap ClassLoader;
  3. 从 Bootstrap ClassLoader 开始尝试加载,加载成功则返回;
  4. 没有则将按照原本的从下往上的委托层级原路返回,从上往下的尝试加载;
  5. 如果到最后还是加载不到对应类,则抛出 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 仍然遵循“双亲委派”模式,需要完成的操作为:
  1. 继承 ClassLoader 类
  2. 重写 findClass 方法
  1. 将对应的 class 文件内容读出来放到字节数组中
  2. 调用 defineClass 方法将字节数组转换为 Class 对象并返回
  1. class CustomClassLoader extends ClassLoader { @Override protected Class<?> findClass(String name) throws ClassNotFoundException { // 自定义 ClassLoader 的查找类逻辑 ... return super.findClass(name); } }
  • 自定义的 ClassLoader 需要打破“双亲委派”模式,需要完成的操作为:
  1. 继承 ClassLoader 类
  2. 重写 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 的作用


  1. 对 class 文件进行加密,一定程度上能防止反编译 具体操作就是将编译所得的 class 文件加载成二进制字节流,并字节流内容进行加密,再重新存成加密文件。 在自定义 ClassLoader 的 findClass 方法中读取加密文件,对内容进行解密,得到原 class 文件的字节流内容之后再调用 defineClass 方法转换为 Class 对象。
  2. 打破“双亲委派”模式 直接选择重写 loadClass 方法,完全自定义 ClassLoader 的加载类逻辑。可以绕过只重写 findClass 方法无法避开的 findLoadedClass(name) 方法和 parent.loadClass(name, false) ,打破“双亲委派”模式。
  3. 实现多个具有相同类名的类能够处于同一空间


SPI 机制


SPI 全称为 Service Provider Interface,是 JDK 内置的一种服务提供发现机制。主要是解决接口和具体实现的硬编码问题,彻底实现“面向接口编程”:JDK 提供接口,供应商提供具体的实现,用户面向接口编程。


以 JDBC 为例,Java 定义了接口 java.sql.Driver 作为数据库链接规范。但并没有具体的实现,具体的实现都是由不同厂商提供,即由具体的数据库厂商(Mysql、PostgreSQL)提供实现。


那么问题是位于内层的 JDBC 是如何发现位于外层的具体实现的?使用的正是 SPI 机制。


SPI 的开发流程:


  1. 定义一个接口作为标准
  2. 编写这个接口的实现类,并在打包文件(jar 包)中的 META-INF/services/ 目录中,以接口的全类名称作为文件名,实际实现类的全类名作为内容,建立配置文件
  3. 通过 java.util.ServiceLoader 类的 load 方法,传入接口名称,获得实现类的实例对象


具体例子:


  1. 定义一个接口作为标准
    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;
复制代码


  1. }


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%
复制代码


  1. 实际使用
    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;
         }
     });
    ...
 }
复制代码


  1. }


我们知道 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 加载。

相关文章
|
21天前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
本系列教程笔记详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。若需快速学习Kotlin,建议查看“简洁”系列教程。本期重点介绍了Kotlin与Java的共存方式,包括属性、单例对象、默认参数方法、包方法、扩展方法以及内部类和成员的互操作性。通过这些内容,帮助你在项目中更好地结合使用这两种语言。
38 1
|
23天前
|
Java 开发工具 Android开发
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
Kotlin语法笔记(26) -Kotlin 与 Java 共存(1)
30 2
|
6天前
|
Java Maven Spring
Java Web 应用中,资源文件的位置和加载方式
在Java Web应用中,资源文件如配置文件、静态文件等通常放置在特定目录下,如WEB-INF或classes。通过类加载器或Servlet上下文路径可实现资源的加载与访问。正确管理资源位置与加载方式对应用的稳定性和可维护性至关重要。
|
12天前
|
安全 Java 编译器
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
Kotlin教程笔记(27) -Kotlin 与 Java 共存(二)
|
12天前
|
Java 开发工具 Android开发
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
Kotlin教程笔记(26) -Kotlin 与 Java 共存(一)
|
12天前
|
Java 编译器 Android开发
Kotlin教程笔记(28) -Kotlin 与 Java 混编
Kotlin教程笔记(28) -Kotlin 与 Java 混编
|
14天前
|
Java 编译器 Maven
Java“class file contains wrong class”解决
当Java程序运行时出现“class file contains wrong class”错误,通常是因为类文件与预期的类名不匹配。解决方法包括:1. 确保类名和文件名一致;2. 清理并重新编译项目;3. 检查包声明是否正确。
|
21天前
|
Java 编译器 Android开发
Kotlin语法笔记(28) -Kotlin 与 Java 混编
本系列教程详细讲解了Kotlin语法,适合需要深入了解Kotlin的开发者。对于希望快速学习Kotlin的用户,推荐查看“简洁”系列教程。本文档重点介绍了Kotlin与Java混编的技巧,包括代码转换、类调用、ProGuard问题、Android library开发建议以及在Kotlin和Java之间互相调用的方法。
18 1
|
21天前
|
安全 Java 编译器
Kotlin语法笔记(27) -Kotlin 与 Java 共存(二)
本教程详细讲解Kotlin语法,适合希望深入了解Kotlin的开发者。若需快速入门,建议查阅“简洁”系列教程。本文重点探讨Kotlin与Java共存的高级话题,包括属性访问、空安全、泛型处理、同步机制及SAM转换等,助你在项目中逐步引入Kotlin。
18 1
|
23天前
|
Java 编译器 Android开发
Kotlin语法笔记(28) -Kotlin 与 Java 混编
Kotlin语法笔记(28) -Kotlin 与 Java 混编
24 2