Java 类加载机制

何为类加载

类加载指的是JVM将class二进制文件读取到内存方法区,在堆内存中生成Class对象。

类加载过程

类加载的过程包含如下步骤:

  • 加载
  • 验证
  • 准备
  • 解析
  • 初始化

加载(Loading)

在这个阶段,类加载器(ClassLoader)负责查找和导入二进制字节码到 JVM 内存中。它通过类的全限定名(包名+类名)找到对应的 .class 文件或其他包含类定义的数据源(比如 Jar 文件、网络数据流等)。
加载完成后,在内存中创建一个 java.lang.Class 类型的对象,该对象将作为方法区内的类元数据的入口,包含了与类有关的各种信息。

验证(Verification)

验证阶段是对加载的字节码数据进行合法性校验,确保符合 JVM 规范。验证内容包括文件格式验证、元数据验证、字节码验证和符号引用验证等。
可以使用-Xverify:none或者-noverifyJVM参数,禁用验证步骤,同时也会加快Java应用的启动速度。(JDK13及其之后已废弃此参数)

准备(Preparation)

准备阶段主要是为类变量(static fields)分配内存并设置初始值。这里的初始值通常是指数据类型的零值(例如 int 类型为 0,对象引用为 null),而不是程序员在 Java 源代码中为它们赋予的初始值。对于 static final 常量,如果其值在编译期就可以确定,则会在此阶段直接赋值为常量池中的值。

解析(Resolution)

解析阶段主要是将常量池中的符号引用替换为直接引用的过程。符号引用包括类和接口的全限定名、字段名和方法名等,直接引用则更为具体,可以是内存偏移量或句柄等。此阶段主要针对类或者接口、字段和方法的符号引用进行解析。

初始化(Initialization)

初始化是类加载过程的最后一步,真正执行类初始化语句(即类构造器 <clinit> 方法),为类变量赋初始值或者执行其他初始化逻辑。初始化只会执行一次,且在类首次主动使用时触发,例如创建类的实例、调用类的静态方法或访问类的静态字段时。

Java 类加载器

Java 类加载器是 Java 虚拟机 (JVM) 中负责动态加载 Java 类到 JVM 运行时数据区中的关键组件。在 Java 中,类加载过程是 JVM 实现动态性的重要手段,它通过不同的类加载器协作完成对类的查找、加载、链接(验证、准备、解析)和初始化。

Java 类加载器可分为以下几种类型:

  • Bootstrap ClassLoader 是最顶层的类加载器,C语言实现,是 JVM 的一部分,不继承自 java.lang.ClassLoader 类。它负责加载 Java 核心库,即 JDK 的 lib 目录下的 rt.jar、resources.jar 等核心类库,或被-Xbootclasspath参数指定路径中的class文件。如java.*开头的类。无法使用loader.getParent()方法获取。
  • ExtClassLoader 由 Java 编写,继承自 ClassLoader 类。负责加载$JAVA_HOME/lib/ext目录中的class,以及被java.ext.dirs系统变量指定路径中的class。如javax.*开头的类。
  • AppClassLoader 负责加载用户类路径(classpath,java.class.path系统变量)中的类。如果没有自定义类加载器,应用程序的类默认会被AppClassLoader加载。
  • 其他ClassLoader 需要用户在特定的使用场景显式使用。例如从URL加载class,或者说是热部署、模块化框架、加密的类资源等。这些classloader需要继承ClassLoader类。

Java的classLoader具有层级(父子)关系,classLoader按照双亲委派模型加载class。双亲委派模型后面说明。上面所属的四种classLoader,前面的classLoader是后面的父classLoader。

Java中所有的Class都有一个classLoader属性,用来标明该类是由哪个类加载器加载的。在程序中获取一个类由哪个类加载器加载,可使用如下方式:

SomeKlass.class.getClassLoader();

下面举一个例子:

public class ClassLoaderDemo {
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader());
        System.out.println(ClassLoaderDemo.class.getClassLoader());
    }
}

执行的结果为:

null
sun.misc.Launcher$AppClassLoader@18b4aac2

如果一个类由BootstrapClassLoader加载,那么该类的classLoader属性值为null。如果一个classloader的父classLoader为BootstrapClassLoader,这个classloader的parent属性为null。(反过来也成立,null会被视为BootstrapClassLoader)

类加载器行为

  • 加载class时默认使用调用者的classLoader加载。
  • classloader自己不加载这个class,将加载工作转交给父加载器去加载(如果父加载器还有父加载器,一直向上传递),如果父加载器找不到这个类无法加载,子加载器才会尝试加载。
  • 父加载器加载的class是会共享给所有的子加载器的(都算作已加载,子加载器的loadClass方法能够获取到父加载器加载的class)。但是多个平行关系的子加载器加载的class,互相之间不共享。
  • 决定两个class是否相同的不仅是包名和class名,还有这个class的类加载器。也就是说如果包名和类名相同的class被两个不存在父子关系的类加载器加载,那么加载后的这两个class是完全不同的。

类加载机制

  • 全盘负责:当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入。除非显式使用另外一个类加载器来载入。
  • 父类委托:任何类加载器先让父类加载器试图加载该类,只有在父类加载器无法加载该类时自己才尝试从自身的类路径中加载该类。
  • 缓存机制:缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这种机制导致了class文件的修改无法实时在JVM中体现出来。如果需要实时体现出修改(称之为热加载),需要放弃Class缓存然后使用类加载器重新加载(例如再次新创建一个自定义加载器,重新加载该类)。

类加载的方式

类加载有三种方式:

1 启动应用时候由JVM初始化加载
2 通过Class.forName()方法动态加载
3 通过ClassLoader.loadClass()方法动态加载

这三种方式的区别为:

  • 使用ClassLoader.loadClass()加载类,不会执行初始化块
  • 使用Class.forName()加载类,默认会执行初始化块
  • 使用Class.forName(),指定类加载器来加载类,不会执行初始化块

双亲委派模型

双亲委派模型的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的类时,即无法完成该加载,子加载器才会尝试自己去加载该类。

双亲委派模型目的是确保类的全局唯一性。

ClassLoader类的loadClass方法源代码完整实现了双亲委派模型。代码如下所示:

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    // 加锁,每个class对应着不同的lock object
    // 确保同一个类不会被同时加载
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        // 首先检查这个class是否已经加载过了
        // 如果已经被加载过,直接返回加载过的类
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                // 如该加载器的父加载器存在,则调用父类加载器的loadClass方法。
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    // 如果parent为null,说明父加载器为bootstrap类加载器,查找并返回使用bootstrap类加载器加载的类
                    // 如果bootstrap没有加载该类,返回null
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }
            // 如果类仍未加载(父类加载器没有加载,bootstrap类加载器也没有加载)
            // 则调用findClass方法,亲自加载该类
            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                // findClass方法是详细的根据类名查找类二进制文件,读取并解析的过程
                // findClass方法需要ClassLoader的子类来重写
                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;
    }
}

上面代码的主要逻辑为:

  1. 检查类是否已经被加载。如果已被加载,返回加载过的类。
  2. 如果类没有被加载,交给该类加载器的父加载器去加载。如果父加载器为BootStrapClassLoader,查找并返回它加载的类。
  3. 如果父类加载器无法加载该类。自己再亲自去加载这个类。

Thread的contextClassLoader

用于使用父加载器加载的类去显式加载子加载器范围内的类的情况,打破双亲委派模型。例如Java SPI。SPI的ServiceLoader::load代码如下所示:

public final class ServiceLoader<S> implements Iterable<S> {
  
    public static <S> ServiceLoader<S> load(Class<S> service) {
        // 在Launcher类的构造器中被赋值为AppClassLoader
        // 可以读取classpath
        // ServiceLoader自身默认是BootStrap ClassLoader加载的,如果用这个class loader是无法读取到用户classpath中的类的
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        // 这个方法中最终由指定classLoader的Class.forName方法去加载实现类
        return ServiceLoader.load(service, cl);
    } 
}

从上面分析可知获取线程context classLoader的方法为:

ClassLoader loader = Thread.currentThread().getContextClassLoader();

线程的contextClassLoader默认为父线程的contextClassLoader。可以使用setContextClassLoader方法修改。

主要注意的是,contextClassLoader除非有意使用,否则永远不会被调用。加载class默认使用的classLoader永远是调用者的classLoader

除此之外Context ClassLoader还可以用于线程间classLoader的共享和不同线程间classLoader的隔离。

自定义classLoader使用场景

自定义classLoader的几个典型的使用场景:

  • 解决依赖冲突:使用不同的classLoader加载package和name相同的class,可以实现不同版本的class共存。
  • 热加载:检测二进制文件变更(最后修改时间),检测到变更之后创建新的classLoader然后加载这个class(不创建新的classLoader无法再次加载该class)。
  • class加密:对编译之后的class文件二进制内容加密。使用时借助自定义classLoader读取加密的二进制内容,解密后再交给JVM解析class。

编写自定义classLoader需要继承ClassLoader类,重写findClass方法。自定义加载逻辑位于findClass方法中。获取到Class内容byte数组之后,将其传递给defineClass方法,解析为Java的Class。比如说上面的class加密,可以在findClass的时候对原始class文件内容解密之后再交给defineClass

一个最简单的例子,使用自定义classLoader读取项目编译输出目录(使用maven在IDE里执行对应的是target/classes目录)中指定名字的class二进制文件,然后解析为Java的Class。

class MyClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) {
        String className = name.replace(".", "/").concat(".class");
        byte[] bytes;
        try (InputStream stream = getClass().getClassLoader().getResourceAsStream(className);
             ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
            int i;
            while ((i = stream.read()) != -1) {
                outputStream.write(i);
            }
            bytes = outputStream.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        if (bytes == null) {
            throw new RuntimeException("Class not found");
        }

        return defineClass(name, bytes, 0, bytes.length);
    }
}

使用方式:

Class<?> aClass = myClassLoader.loadClass("org.example.ClassLoaderDemo");
Class<?> bClass = myClassLoader.findClass("org.example.ClassLoaderDemo");
System.out.println(aClass.getClassLoader());
System.out.println(bClass.getClassLoader());

输出为:

sun.misc.Launcher$AppClassLoader@18b4aac2
org.example.MyClassLoader@6bc7c054

解释:
loadClass方法是用双亲委派模型加载,因为ClassLoaderDemo类位于classpath中,在启动的时候已经被AppClassLoader加载过了。所以aClass的classLoader为AppClassLoaderloadClass方法间接调用了findClass方法。实际开发中建议使用loadClass方法。
findClass方法是用户自己实现的,如果直接调用的话没有考虑双亲委派模型。这里为了演示直接调用。不建议在项目中直接使用。

参考文献

https://zhuanlan.zhihu.com/p/51374915

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容