类加载机制系列1——深入理解Java中的类加载器

0 类加载器介绍

Java类加载器是Java运行时环境(Java Runtime Environment)的一部分,它负责动态加载Java类到Java虚拟机的内存空间中。类通常是按需加载,即第一次使用该类时才加载。由于有了类加载器,Java运行时系统不需要知道文件与文件系统。

Java中的类加载器是java.lang.ClassLoader,它是一个抽象类。给定一个类名,ClassLoader就负责把这个类从特定的文件系统中加载到虚拟机中。

Class类有一个方法getClassLoader(),每一个类的Class对象都可以调用这个方法来获取把这个类加载到虚拟机中的ClassLoader。

对于数组来说,它们不是由ClassLoader来创建,而是由Java运行时创建,数组的ClassLoader就是加载该数组元素类的ClassLoader。如果元素类型是基本类型,那么数组就没有ClassLoader。

ClassLoader采用的是代理模式来加载类,每一个ClassLoader实例都有一个父ClassLoader(并不是继承关系),当一个类加载器需要加载一个类的时候,它会首先传递这个类的信息到parent 类加载器,请求parent来加载,然后依次传递,直到该类被成功加载或者失败。如果失败了,那么就由最开始的那个类加载器来进行加载。在Java虚拟机中有一个内置的类加载器是bootstrap class loader,它是没有parent的,但是可以作为所有ClassLoader实例的parent。这种加载方式也叫作双亲委派机制或者父委托机制。

通常来讲,类加载器都是加载本地的Class文件,但是它也可以加载其它来源的文件,比如从网络下载下来的。可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求而不需要完全了解Java虚拟机的类加载的细节。ClassLoader的一个方法defineClass可以把一个字节数组转为Class实例。然后可以根据Class.newInstance()方法来创建一个对象。被ClassLoader创建的类的方法或者构造方法可能还会引用其它的类,为了确定引用的类,虚拟机会调用最开始加载引用类的ClassLoader的loadClass方法。

protected final Class<?> defineClass(String name, byte[] b, int off, int len)

例如,想要自定义一个NetworkClassLoader,来加载从网络传来的Class类:

ClassLoader loader = new NetworkClassLoader(host, port);
Object main = loader.loadClass("Main", true).newInstance();
. . .

NetworkClassLoader必须重写findClass方法,,然后定义一个方法来返回Class类的字节数组。当下载完毕,需要调用defineClass方法,示例如下:

class NetworkClassLoader extends ClassLoader {
    String host;
    int port;

    public Class findClass(String name) {
        byte[] b = loadClassData(name);
        return defineClass(name, b, 0, b.length);
    }

    private byte[] loadClassData(String name) {
        // load the class data from the connection
          . . .
    }
}

1 JVM中的ClassLoader

JVM中有3个默认的类加载器:

  • 引导(Bootstrap)类加载器。用C/C++写的,在Java代码中无法获取到。主要是加载存储在<JAVA_HOME>/jre/lib目录下的核心Java库,对应的加载路径是sun.boot.class.path
  • 扩展(Extensions)类加载器.用来加载<JAVA_HOME>/jre/lib/e。t目录下或者对应的加载路径java.ext.dirs中指明的Java扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。该类由sun.misc.Launcher$ExtClassLoader实现。
  • Apps类加载器(也称系统类加载器)。根据 Java应用程序的类路径(java.class.path或CLASSPATH环境变量)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。该类由sun.misc.Launcher$AppClassLoader实现,它的parent类加载器是ExtClassLoader。

下面通过一个示例来看一下:

package classLoader;

public class ClassLoaderTest {
    public static void main(String[] args) {
        
        ClassLoaderTest clt=new ClassLoaderTest();
        ClassLoader cl=clt.getClass().getClassLoader();
        System.out.println(cl);
        System.out.println(cl.getParent());
        System.out.println(cl.getParent().getParent());
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(System.getProperty("sun.boot.class.path"));
        System.out.println(System.getProperty("java.ext.dirs"));
        System.out.println(System.getProperty("java.class.path"));
    }
}

打印结果:

sun.misc.Launcher$AppClassLoader@2a139a55
sun.misc.Launcher$ExtClassLoader@7852e922
null
sun.misc.Launcher$AppClassLoader@2a139a55
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/resources.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/rt.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/sunrsasign.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jsse.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jce.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/charsets.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/jfr.jar:
/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/classes

/Users/jason/Library/Java/Extensions:/Library/Java/JavaVirtualMachines/jdk1.8.0_121.jdk/Contents/Home/jre/lib/ext:
/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java
/works/EclipseWorkSpace/classLoader/bin

java.lang.ClassLoader继承关系如下:

Java中ClassLoader继承关系.png

其中AppClassLoader和ExtClassLoader都是在Laucher中的内部类。而这个Laucher是JVM的入口。

注意: 并不是说子加载器继承自父加载器

2 ClassLoader源码分析

前面已经讲了,类的加载使用的是双亲委派机制。那我们启动一个Java应用程序,它的类加载顺序是从AppClassLoader委托ExtClassLoader,如果ExtClassLoader也找不到就会去委托Bootstrap类加载器加载。如果父加载器没有找到的话,再从子加载器中加载,加载到的类会被缓存起来,如果最终都没有找到这个类,就会报一个异常ClassNotFoundException

我们先看一下ClassLoader的构造方法,它有3个构造方法,但是其中有一个私有的:

//最终调用的还是这个私有的方法
private ClassLoader(Void unused, ClassLoader parent) {}

//有参构造,传递parent类加载器
protected ClassLoader(ClassLoader parent) {
   this(checkCreateClassLoader(), parent);
}

// 无参构造,默认采用getSystemClassLoader()方法获取的ClassLoader作为parent类加载器
protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

下面我们来看getSystemClassLoader()这个方法:

public static ClassLoader getSystemClassLoader() {
    // 初始化系统类加载器
    initSystemClassLoader();
    if (scl == null) {
        return null;
    }
    // 做一些安全方面的校验
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkClassLoaderPermission(scl, Reflection.getCallerClass());
    }
    return scl;
}

initSystemClassLoader()方法如下:

private static synchronized void initSystemClassLoader() {
    if (!sclSet) {
        if (scl != null)
            throw new IllegalStateException("recursive invocation");
        //获取Launcher对象
        sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
        if (l != null) {
            Throwable oops = null;
            //调用Launcher对象的getClassLoader()方法,这个获取的就是AppClassLoader,详细内容可以看下面对Launcher的分析
            scl = l.getClassLoader();
            try {
                scl = AccessController.doPrivileged(
                    new SystemClassLoaderAction(scl));
            } catch (PrivilegedActionException pae) {
                oops = pae.getCause();
                if (oops instanceof InvocationTargetException) {
                    oops = oops.getCause();
                }
            }
            if (oops != null) {
                if (oops instanceof Error) {
                    throw (Error) oops;
                } else {
                    // wrap the exception
                    throw new Error(oops);
                }
            }
        }
        sclSet = true;
    }
}

下面我们就ClassLoader加载一个类的过程来进行一下分析。ClassLoader加载一个类,调用的方法是loadClass()方法:

//通常外界是调用ClassLoader的这个loadClass方法
public Class<?> loadClass(String name) throws ClassNotFoundException {
    return loadClass(name, false);
}

//ClassLoader的默认加载方式,如果需要自定义ClassLoader最好不要重写这个方法。
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
{
    // 同步代码块
    synchronized (getClassLoadingLock(name)) {
        // findLoadedClass是一个native方法,如果已经加载过的类是会被缓存起来的,直接从缓存获取即可
        Class<?> c = findLoadedClass(name);
        if (c == null) {
        // c为null,说明没有缓存,就需要初次加载
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    // 如果parent不为null就委托parent去加载
                    c = parent.loadClass(name, false);
                } else {
                    // 如果parent为null就委托bootstrap class loader去加载
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // 如果非空的父加载器找不到类会抛出异常,在这里try-catch住了
            }
            
            if (c == null) {
                // 如果父加载器和bootstrap加载器都没有找到,就会调用ClassLoader实例自身的findClass()方法。
                // 其方法体是抛出一个ClassNotFoundException异常,
                // 所以继承ClassLoader的子类加载器需要重写这个findClass()方法
                long t1 = System.nanoTime();
                c = findClass(name);
                ...
            }
        }
        if (resolve) {
            // 使Classloader链接指定的类,如果这个类已经被链接过了,那么这个方法只做一个简单的返回。
            // 否则,这个类将被按照 Java™规范中的Execution描述进行链接
            resolveClass(c);
        }
        return c;
    }
}


需要注意的是第一个参数,name表示的是二进制名称(Binary name),例如:

"java.lang.String"
"javax.swing.JSpinner$DefaultEditor"
"java.security.KeyStore$Builder$FileBuilder$1"
"java.net.URLClassLoader$3$1"

需要指出:类加载过程是同步的

简单总结一下类加载器的工作过程:

  1. 如果当前加载的类已经加载过,直接从缓存获取。
  2. 之前没有加载过,如果该ClassLoader对象的parent不为null就委托父加载器加载,父加载器会重新开始走第1步。如果parent为null,那么就采用根加载器bootstrap class loader进行加载。
  3. 如果之前还是没有成功加载类,那么就会调用当前ClassLoader的findClass()方法去加载。

类加载器采用双亲委派机制的好处:

  1. 加载的类会被缓存起来,下次加载就快了。
  2. 安全,比如我们自定义一个与系统String包名类型一致的类,然后想要把这个String类加载进来干点坏事的话实际上是做不到的,由于父委托机制,真正的String类会被bootstrap class loader 加载(String类是存放在bootstrap class loader 负责加载的区域),就不会再调用我们这个假的String类。实际上,如果你自定义了一个类加载器并且重写了loadClass的逻辑,最终还是不能加载假的String类,因为ClassLoader有一个preDefineClass方法,该方法会检测类的包名,如果是'java'开头就会抛出一个SecurityException异常。

那么AppClassLoader和ExtClassLoader是什么时候初始化的呢?下面我们再去看一下Launcher的部分源码:

# sun.misc.Launcher
//构造方法
public Launcher() {
    ClassLoader extcl;
    try {
        // 创建ExtClassLoader对象
        extcl = ExtClassLoader.getExtClassLoader();
    } catch (IOException e) {
        throw new InternalError(
            "Could not create extension class loader", e);
    }
    //创建AppClassLoader对象loader,这个loader就是上面讲到的,Launcher的getClassLoader()方法返回的对象。 
    //AppClassLoader.getAppClassLoader()方法的参数为extcl,实际上就是把ExtClassLoader对象当作其父加载器
    try {
        loader = AppClassLoader.getAppClassLoader(extcl);
    } catch (IOException e) {
        throw new InternalError(
            "Could not create application class loader", e);
    }
    // 设置上下文的类加载器,也就是AppClassLoader对象。
    Thread.currentThread().setContextClassLoader(loader);
    // Finally, install a security manager if requested
    String s = System.getProperty("java.security.manager");
    if (s != null) {
        SecurityManager sm = null;
        if ("".equals(s) || "default".equals(s)) {
            sm = new java.lang.SecurityManager();
        } else {
            try {
                sm = (SecurityManager)loader.loadClass(s).newInstance();
            } catch (IllegalAccessException e) {
            } catch (InstantiationException e) {
            } catch (ClassNotFoundException e) {
            } catch (ClassCastException e) {
            }
        }
        if (sm != null) {
            System.setSecurityManager(sm);
        } else {
            throw new InternalError(
                "Could not create SecurityManager: " + s);
        }
    }
}

关于ExtClassLoader和AppClassLoader的源码我们这里就不做多余的介绍了,感兴趣的可以去看一下Launcher这个类,ExtClassLoader和AppClassLoader都是其静态内部类。

3 自定义类加载器

我们完全可以通过自定义类加载器来加载我们想要加载的类,这个类可能来源于网络,也可能来源于文件系统。
从前面的分析我们知道,加载一个类的过程调用的是ClassLoader的loadClass()方法。自定义类加载器通常不要重写loadClass()方法的逻辑。在这个方法内部,如果所有的父加载器都没有成功加载,就会调用ClassLoader对象自身的findClass()方法,自定义类加载器可以实现这个findClass()方法即可。

还有一个关键的方法就是调用ClassLoader对象的defineClass()方法,这样就可以创建一个Class对象了。

public class CustomClassLoader extends ClassLoader {

    private String dirPath;

    public CustomClassLoader(String dirPath) {
        this.dirPath = dirPath;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = null;
        //注意这个name一定要是二进制名称,如'java.lang.String'
        //根据类的二进制名称,获得该class文件的字节码数组
        byte[] classData = getClassDataBytes(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        //调用define()方法将class的字节码数组转换成Class类的实例
        clazz = defineClass(name, classData, 0, classData.length);
        return clazz;
    }

    private byte[] getClassDataBytes(String name) {
        FileInputStream is = null;
        try {
            String path = classNameToPath(name);
            is = new FileInputStream(path);
            byte[] buff = new byte[1024];
            int len;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            while ((len = is.read(buff)) != -1) {
                baos.write(buff, 0, len);
            }
            return baos.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return null;
    }

    private String classNameToPath(String name) {
        return dirPath + "/" + name.replace(".", "/") + ".class";
    }
}

自定义的类加载器已经写好了,下面我们来演示一下如何加载一个类,首先我们编写一个java类:

package com.sososeen09;

class Test {
    public Test() {
    }

    public void print() {
        System.out.println("this is Test Class");
    }
}

根据javac命令把java文件编译为对应的class文件。这个Test.class文件的二进制名称就是com.sososeen09.Test

public class ClassLoaderTest {

    public static void main(String[] args) {
        String srcPath = "/test/bin";
        CustomClassLoader customClassLoader = new CustomClassLoader(srcPath);
        String classname = "com.sososeen09.Test";
        try {
            Class clazz = customClassLoader.loadClass(classname);
            System.out.println("loaded class: " + clazz);
            System.out.println("class loader: " + clazz.getClassLoader());
            System.out.println("class loader parent: " + clazz.getClassLoader().getParent());
            Constructor constructor = clazz.getConstructor();
            constructor.setAccessible(true);
            Object o = constructor.newInstance();
            Method print = clazz.getDeclaredMethod("print");
            print.setAccessible(true);
            print.invoke(o);
        } catch (Exception e) {
            e.printStackTrace();
        } 

运行一下可以查看打印结果:

loaded class: class com.sososeen09.Test
class loader: com.sososeen09.javamodule.classloaders.CustomClassLoader@14ae5a5
class loader parent: sun.misc.Launcher$AppClassLoader@18b4aac2
this is Test Class

可以看到我们自定义的类加载器已经成功的把一个文件系统中的class加载了。

需要注意:我们是把二进制文件前面的包名转为路径了,所以我们传递的srcPath是"/test/bin",那么实际上class文件存放路径应该是"/test/bin/com/sososeen09/"

4 扩展知识点

在Java中,类的加载时按需加载,也就是需要的时候才会把class文件加载到内存中。可以分为隐式加载和显示加载。

  • 隐式加载:由当new一个Java对象,或者调用类的静态方法或者使用静态成员变量的时候,会加载当前的Class。
  • 显示加载,显示的调用Class.forName()方法,或者调用ClassLoader的loadClass()方法。

参考文章

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

推荐阅读更多精彩内容