详解Java类加载机制

一:ClassLoader

从JVM结构图中可以看到,类加载器的作用是将Java类文件加载到Java虚拟机。

HotSpot JVM结构,图片来自Java Garbage Collection Basics

只有当类被加载进虚拟机内存,才能使用对应的类。

在Java中,类加载过程大概分为以下几步:

  1. 通过全限类名获取类文件字节数组。可来自本地文件、jar包、网络等。
  2. 在方法区/元空间保存类的描述信息、静态属性。
  3. 在JVM堆中生成一个对应的java.lang.Class对象。

理解Java的类加载机制,对理解JVM有很大帮助。

二:Java默认的类加载器

Java默认提供三个类加载器,分别为:

  • Bootstrap ClassLoader
  • Extension ClassLoader
  • App ClassLoader

Bootstrap ClassLoader 负责加载Java基础类,主要是 %JRE_HOME%/lib/ 目录下的rt.jar、resources.jar、charsets.jar等。

Extension ClassLoader 负责加载Java扩展类,主要是 %JRE_HOME%/lib/ext 目录下的jar。

App ClassLoader 负责加载当前应用的ClassPath中的所有类。

三个ClassLoader所负责加载的类,可以通过以下方式进行查看。

public class ClassPath {
    public static void main(String[] args) {
        System.out.println("Bootstrap ClassLoader path: ");
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println("----------------------------");

        System.out.println("Extension ClassLoader path: ");
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println("----------------------------");

        System.out.println("App ClassLoader path: ");
        System.out.println(System.getProperty("java.class.path"));
        System.out.println("----------------------------");
    }
}

具体原因,在源码分析章节说明。

其中Bootstrap ClassLoader是JVM级别的,由C++撰写。

Extension ClassLoader和App ClassLoader都是Java类。

JVM启动Bootstrap ClassLoader,然后初始化sun.misc.Launcher。

接着,Launcher初始化Extension ClassLoader和App ClassLoader。

三:源码分析

sun.misc.Launcher类是Java程序的入口。

其构造器如下:

public Launcher() {
    Launcher.ExtClassLoader var1;
    try {
        var1 = Launcher.ExtClassLoader.getExtClassLoader();
    } catch (IOException var10) {
        throw new InternalError("Could not create extension class loader", var10);
    }

    try {
        this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
    } catch (IOException var9) {
        throw new InternalError("Could not create application class loader", var9);
    }

    Thread.currentThread().setContextClassLoader(this.loader);
    ……
}

其中有两行比较重要的代码:

Launcher.ExtClassLoader.getExtClassLoader();

this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);

第一行初始化了ExtClassLoader,但没有指定其parent。

一些文章表示ExtClassLoader的父加载器是Bootstrap ClassLoader,这个说法其实并不完全准确。

第二行初始化了AppClassLoader,指定ExtClassLoader作为其父加载器。并将AppClassLoader作为系统类加载器。

AppClassLoader将会成为自定义ClassLoader的默认父加载器。

具体逻辑可按照以下顺序查看源代码:

  1. Launcher类的getClassLoader()方法。
  2. ClassLoader类的initSystemClassLoader()方法。
  3. ClassLoader类的getSystemClassLoader()方法。
  4. ClassLoader类的ClassLoader()方法。

其中getSystemClassLoader()方法的注释为:

/**
* Returns the system class loader for delegation. This is the default
* delegation parent for new <tt>ClassLoader</tt> instances, and is
* typically the class loader used to start the application.
**/


ExtClassLoader和AppClassLoader都继承了URLClassLoader类。

URLClassLoader支持从文件目录和jar包加载class。

ExtClassLoader和AppClassLoader都调用了父类的构造函数。

public URLClassLoader(URL[] urls, ClassLoader parent,
                          URLStreamHandlerFactory factory)

URLClassLoader类中有个属性为ucp,表示该ClassLoader负责搜索的路径。

ExtClassLoader和AppClassLoader最大的不同,即它们负责的路径不同。

/* The search path for classes and resources */
private final URLClassPath ucp;

查看源码可得:

ExtClassLoader负责搜索的路径为:

String var0 = System.getProperty("java.ext.dirs");

AppClassLoader负责搜索的路径为:

String var1 = System.getProperty("java.class.path");

所以,上一节可以通过这两个方法获取不同ClassLoader所负责加载的目录。

此外,Bootstrap ClassLoader负责搜索的路径为:

String bootClassPath = System.getProperty("sun.boot.class.path");

ClassLoader源码

ClassLoader是一个抽象类,几个主要的方法如下:

  • defineClass(String name, byte[] b, int off, int len)把字节数组b中的内容转换成Java类,返回的结果是java.lang.Class类的实例。

  • findClass(String name)查找名称为name的类,返回的结果是java.lang.Class类的实例。

  • loadClass(String name)加载名称为name的类,返回的结果是java.lang.Class类的实例。

  • resolveClass(Class<?> c)链接指定的Java 类。

其中,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;
    }
}

该方法主要的步骤如下:

  1. 指定全限类名进行加载,首先调用findLoadedClass(name)判断当前类加载器是否已经加载该类。
  2. 如果没有被加载。则判断当前ClassLoader的父加载器是否为null。如果不为null,则委托其父加载器进行加载。如果为null,则使用Bootstrap ClassLoader进行加载。
  3. 如果父加载器或Bootstrap ClassLoader都无法加载,则调用findClass(name)方法寻找需要加载的类。

此外,loadClass方法还涉及加锁的过程,使用ConcurrentHashMap对不同的全限类名进行加锁。

具体可查看getClassLoadingLock方法。

四:双亲委托模式

Java类加载机制使用双亲委托模式。

一个ClassLoader加载一个类时,首先需要将任务委托给其父加载器,直到Bootstrap ClassLoader。

如果父加载器未加载该类,则逐层返回给委托发起者即当前ClassLoader进行加载。

在正常应用中,用户不自定义类加载器。

类加载工作首先由App ClassLoader发起,然后委托给Extension ClassLoader,最后委托给Bootstrap ClassLoader。


首先,通过一个例子了解三个ClassLoader所负责加载的类和双亲委托模式。

新建一个jar包,名为acai-cl.jar,包中有个简单的Person类。

写一个简单的程序输出person对象所对应的ClassLoader。

import com.acai.Person;

public class TestClassLoader {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
    }
}

测试一:将jar包引入项目

jar包引入项目

对应输出:

sun.misc.Launcher$AppClassLoader@18b4aac2

可以看到,位于ClassPath的类,是由App ClassLoader负责加载。

测试二:将jar包复制到%JRE_HOME%/lib/ext目录

复制到%JRE_HOME%/lib/ext

对应输出:

sun.misc.Launcher$ExtClassLoader@4cc77c2e

可以得出,Extension ClassLoader负责加载%JRE_HOME%/lib/ext目录下的类。

加载Person类时,会首先尝试使用App ClassLoader进行加载。

由于双亲委托模式,最终委托到Extension ClassLoader,而其负责的目录%JRE_HOME%/lib/ext下存在Person类,则进行了类加载操作。

测试三:将jar包追加到Bootstrap ClassLoader加载路径上

追加到Bootstrap ClassLoader加载路径

使用参数:-Xbootclasspath/a:d:\acai-cl.jar,将jar包追加到Bootstrap ClassLoader加载路径。

对应输出:

null

可以看出,Person类的加载工作,最终被委托到了Bootstrap ClassLoader。

注:Bootstrap ClassLoader由C++撰写。由Bootstrap ClassLoader负责加载的类,其getClassLoader()方法输出为null。

可以尝试输出String类的类加载器。

System.out.println(String.class.getClassLoader());

接下来,再通过debug来验证双亲委托模式。

还是原来那个简单的demo。

import com.acai.Person;

public class Test {
    public static void main(String[] args) {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
    }
}

在ClassLoader类的loadClass方法上打断点。

App ClassLoader尝试加载
Extension ClassLoader尝试加载
Bootstrap ClassLoader尝试加载

可以看出,类的加载过程符合从下到上委托,最终会被委托到Bootstrap ClassLoader。

同时符合从上到下加载,每一层ClassLoader都会尝试进行加载。最终由App ClassLoader加载了Person类。

接着,尝试加载一个特殊的类:Splash.class。

Splash类位于jfxrt.jar,这个jar包在%JRE_HOME%/lib/ext目录下。

import com.sun.javafx.applet.Splash;

public class ExtTest {
    public static void main(String[] args) {
        Splash splash = new Splash(null);
        System.out.println(splash.getClass().getClassLoader());
    }
}

对应输出:

sun.misc.Launcher$ExtClassLoader@330bedb4

毫无疑问,Splash类应该由Extension ClassLoader进行加载。

但其加载过程,仍然会从默认的系统类加载器App ClassLoader开始。

可以通过debug进行查看。

App ClassLoader尝试加载

Splash类加载的过程会被委托到Bootstrap ClassLoader,但Bootstrap ClassLoader并不负责加载%JRE_HOME%/lib/ext目录下的类。最终由Extension ClassLoader进行加载。

Bootstrap ClassLoader尝试加载未成功
最终由Extension ClassLoader加载

很多文章在阐述三个ClassLoader之间的关系时候,会给出一个getParent操作的demo。

并且认为Bootstrap ClassLoader是Extension ClassLoader的父加载器。

Extension ClassLoader是App ClassLoader的父加载器。

App ClassLoader是自定义类加载器的父加载器。

这样的解释基本正确,但Bootstrap ClassLoader和Extension ClassLoader之间的关系需要额外解释。

双亲委托机制,图片来自参考7

由于Bootstrap ClassLoader并不是使用Java编写,故无法指定Extension ClassLoader的parent为Bootstrap ClassLoader。

这一层关系在ClassLoader的loadClass方法中做了弥补。

在加载类时,会判断当前ClassLoader的父加载器是否为null,为null则使用Bootstrap ClassLoader进行加载。

在Java提供的三个默认类加载器中,父加载器为null的只有Extension ClassLoader。

该过程可参考ClassLoader的loadClass方法。


为什么使用双亲委托模式?

网上很多例子是关于String类。假设自己写一个java.lang.String类,使用双亲委托模式可以防止这个问题。

但其实双亲委托模式可以被打破,而真正阻止自定义java.lang.String的是“安全机制”。

这里尝试自定义java.lang.String类,并使用自定义ClassLoader进行加载。

package java.lang;

public class String {
}
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;

public class StringClassLoader extends ClassLoader {
    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        if ("java.lang.String".equals(name)) {
            return findClass(name);
        } else {
            return super.loadClass(name);
        }
    }

    @Override
    public Class<?> findClass(String s) throws ClassNotFoundException {
        try {
            byte[] classBytes = Files.readAllBytes(Paths.get("d:/String.class"));
            return defineClass(s, classBytes, 0, classBytes.length);
        } catch (IOException e) {
            throw new ClassNotFoundException(s);
        }
    }

    public static void main(String[] args) throws ClassNotFoundException {
        StringClassLoader stringClassLoader = new StringClassLoader();
        Class clazz = stringClassLoader.loadClass("java.lang.String", false);
        System.out.println(clazz.getClassLoader());
    }
}

该自定义类加载器破坏了双亲委托机制,具体方式将在下个章节说明。

输出结果为:

Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang

可以看到,在被findClass方法调用的defineClass中有这么一段:

if ((name != null) && name.startsWith("java.")) {
            throw new SecurityException
                ("Prohibited package name: " +
                 name.substring(0, name.lastIndexOf('.')));
        }

它会检查当前加载类的全限类名是否以java.开头,这也是一种安全机制。

如果按照网上的说法,java.lang.String被Bootstrap ClassLoader加载,demo中自定义的类加载器会被略过,不会输出异常。

所以说,双亲委托模式的作用只是防止类重复加载。

五:自定义ClassLoader

多数情况下,Java默认的三个类加载器已经可以满足需求。

自定义类加载器则可以实现额外的需求,例如:

  1. 从网络文件加载类。
  2. 从任意目录加载类。
  3. 对字节码文件做加密处理,由自定义类加载器做解密。

实现自定义类加载器的主要步骤为:

  1. 继承ClassLoader类。如果只是从目录或者jar包加载类,也可以选择继承URLClassLoader类。
  2. 重写findClass方法。
  3. 在重写的findClass方法中,无论用何种方法,获取类文件对应的字节数组,然后调用defineClass方法转换成类实例。

自定义类加载器真正好玩的是打破双亲委托机制,也是很多面试官会问到的问题。

上文提到类加载双亲委托模式实现位于ClassLoader的loadClass方法,想要破坏这个机制,则需要重写该方法。

打破双亲委托模式的确有一定的实用价值。

比如有两个class文件,或者两个jar包。

其中两个类的全限类名都一样,如果需要同时使用这两个类,则需要打破双亲委托模式。

有两个Person类,它们的全限类名均为com.acai.Person,唯一的区别是sayHello()方法输出的内容略有不同。

package com.acai;

import lombok.Data;

@Data
public class Person {

    private String name;

    private Integer age;

    public void sayHello() {
        System.out.println("Hello, this is Person in acai-cl");
    }
}
package com.acai;

import lombok.Data;

@Data
public class Person {

    private String name;

    private Integer age;

    public void sayHello() {
        System.out.println("Hello, this is Person in acai-cl2");
    }
}

将两个Person所在的项目打成jar包。

两个jar包

常规操作是,把两个jar包都引进项目。

写一个小小的demo。

import com.acai.Person;

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
        person.sayHello();
    }
}

对应输出为:

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

可以看到,demo中默认使用了acai-cl.jar中的Person类。

如果想要使用acai-cl2.jar中的Person类,则想到新建一个ClassLoader。

需要从jar包加载类,则优先想到URLClassLoader。

import com.acai.Person;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
        person.sayHello();

        URL url = new File("d:/acai-cl2.jar").toURI().toURL();
        URLClassLoader loader = new URLClassLoader(new URL[]{url});
        Thread.currentThread().setContextClassLoader(loader);
        Class<?> clazz = loader.loadClass("com.acai.Person");
        System.out.println(clazz.getClassLoader());
        Method method = clazz.getDeclaredMethod("sayHello");
        method.invoke(clazz.newInstance());
    }
}

对应输出:

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

可以看出,即使是指定了使用acai-cl2.jar,输出的仍然是acai-cl.jar中Person的sayHello。

原因是由于两个Person类拥有一样的全限类名。

加载第二个Person的时候,发现自定义类加载器的父类加载器App ClassLoader已经加载了com.acai.Person。

所以直接返回该类,即为acai-cl.jar中的Person类。

于是想到,新建ClassLoader,并且破坏双亲委托机制,重新loadClass方法。

import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLStreamHandlerFactory;

public class MyClassLoader extends URLClassLoader {


    public MyClassLoader(URL[] urls, ClassLoader parent) {
        super(urls, parent);
    }

    public MyClassLoader(URL[] urls) {
        super(urls);
    }

    public MyClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
        super(urls, parent, factory);
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        if (name.equals("com.acai.Person")) {
            return super.findClass(name);
        } else {
            return super.loadClass(name);
        }
    }
}

在MyClassLoader中重写了loadClass方法,当加载的类名等于com.acai.Person时,直接调用findClass方法,绕过双亲委托机制。

这里需要一个if判断,表示只有在加载com.acai.Person时才破坏双亲委托。

因为在加载一个类时,会同时加载它的父类。

Person的父类为java.lang.Object。

直接用自定义类加载器加载Object类,会抛出SecurityException异常。

于是,写一个demo。

import com.acai.Person;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class Main {

    public static void main(String[] args) throws Exception {
        Person person = new Person();
        System.out.println(person.getClass().getClassLoader());
        person.sayHello();

        URL url = new File("d:/acai-cl2.jar").toURI().toURL();
        URLClassLoader loader = new URLClassLoader(new URL[]{url});
        Thread.currentThread().setContextClassLoader(loader);
        Class<?> clazz = loader.loadClass("com.acai.Person");
        System.out.println(clazz.getClassLoader());
        Method method = clazz.getDeclaredMethod("sayHello");
        method.invoke(clazz.newInstance());

        URL url2 = new File("d:/acai-cl2.jar").toURI().toURL();
        MyClassLoader myClassLoader = new MyClassLoader(new URL[]{url2});
        Class<?> clazz2 = myClassLoader.loadClass("com.acai.Person");
        System.out.println(clazz2.getClassLoader());
        Method method2 = clazz2.getDeclaredMethod("sayHello");
        method2.invoke(clazz2.newInstance());
    }
}

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

sun.misc.Launcher$AppClassLoader@18b4aac2
Hello, this is Person in acai-cl

MyClassLoader@5e2de80c
Hello, this is Person in acai-cl2

可以看到,acai-cl2.jar中的Person类被正确加载。

得出,可以通过自定义ClassLoader,重写loadClass,破坏双亲委托机制。

六:参考资料

[1] Java Garbage Collection Basics
[2] java classloader是怎么加载自身到内存里面执行的?
[3] 详细深入分析 Java ClassLoader 工作机制
[4] 深入分析Java ClassLoader原理
[5] 深入探讨 Java 类加载器
[6] 深度分析Java的ClassLoader机制(源码级别)
[7] Java类加载原理与ClassLoader使用总结
[8] 实现java classloader 动态加载jar包
[9] ClassLoader的基础详解

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

推荐阅读更多精彩内容