深入理解Java类加载器(一):Java类加载原理解析

一、引子

 大家对java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,其实,这个异常背后涉及到的是Java技术体系中的类加载。Java类加载机制是技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对于理解Java虚拟机的连接模型和Java语言的动态性都有很大的帮助。

二、Java虚拟机类加载器结构描述

1、JVM三种预定义类加载器

 我们首先看一下JVM预定义的三种类加载器,当JVM启动的时候,Java缺省开始使用如下三种类加载器:
启动(Bootstrap)类加载器
 引导类加载器是用本地代码实现的类加载器,它将负责<JAVA_HOME>/lib下面的核心类库或-Xbootclasspath选项指定的jar包等虚拟机识别的类库加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

扩展(extension)类加载器
 扩展类加载器是由sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的,它负责将**<JAVA_HOME>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中**。开发者可以直接使用标准扩展类加载器。

系统(System)类加载器
 系统类加载器是由Sun的AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的,它负责将用户类路径Java-classpath或者-Djava.class.path变量所指的目录,即当前类所在路径及其引用的第三方类库的路径,如问题6所述)下的类库加载到内存中。开发者可以直接使用系统类加载器。

 Ps: 除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在《深入理解Java类加载器(二):线程上下文类加载器》一文中进行单独介绍。

2、类加载双亲委派机制介绍与分析

 在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归 (本质上就是loadClass函数的递归调用)。因此,所有的加载请求最终都应该传送到顶层的启动类加载器中。如果父类加载器可以完成这个类加载请求,就成功返回;只有当父类加载器无法完成此加载请求时,子加载器才会尝试自己去加载。事实上,大多数情况下,越基础的类由越上层的加载器进行加载,因为这些基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API(当然,也存在基础类回调用户用户代码的情形)。 关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。
在这里插入图片描述
在这里插入图片描述
 上面两张图分别是标准扩展类加载器继承层次图和系统类加载器继承层次图。通过这两张图我们可以看出,扩展类加载器和系统类加载器均是继承自 java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下 抽象类 java.lang.ClassLoader 中几个最重要的方法:

//加载指定名称(包括包名)的二进制类型,供用户调用的接口  
public Class<?> loadClass(String name) throws ClassNotFoundException{ … }  

//加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用  
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }  

//findClass方法一般被loadClass方法调用去加载指定名称类,供继承用  
protected Class<?> findClass(String name) throws ClassNotFoundException { … }  

//定义类型,一般在findClass方法中读取到对应字节码后调用,final的,不能被继承  
//这也从侧面说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)  
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … } 

 通过进一步分析标准扩展类加载器和系统类加载器的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的 加载委派规则 — loadClass(…)方法。既然这样,我们就可以从java.lang.ClassLoader中的loadClass(String name)方法的代码中分析出虚拟机默认采用的双亲委派机制到底是什么模样:

public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            //首先判断该类型是否已经被加载
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                //如果没有加载,就委托给父类加载器或者委派给启动类加载器加载
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        //如果存在父类加载器,就委派给父类加载器加载
                        c = parent.loadClass(name, false);
                    } else {//递归终止条件
                        // 由于启动类加载器无法被Java程序直接引用,因此默认用 null 替代
                        // parent == null就意味着由启动类加载器尝试加载该类,

                        // 即通过调用 native方法 findBootstrapClass0(String name)加载
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // 如果父类加载器不能完成加载请求时,再调用自身的findClass方法进行类加载,
                    // 若加载成功,findClass方法返回的是defineClass方法的返回值

                    // 注意,若自身也加载不了,会产生ClassNotFoundException异常并向上抛出
                    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;
        }
    }

 通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:
在这里插入图片描述
 上面图片给人的直观印象是:系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

public class Loadertest
{
    public static void main(String[] args)
    {
        System.out.println(ClassLoader.getSystemClassLoader());
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
    }
}

输出的结果是:
在这里插入图片描述
 通过以上的代码输出,我们知道:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器 ,并且可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时却得到了null。事实上,由于启动类加载器无法被Java程序直接引用,因此JVM默认直接使用 null 代表启动类加载器。我们还是借助于代码分析一下,首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
 private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        if (ParallelLoaders.isRegistered(this.getClass())) {
            parallelLockMap = new ConcurrentHashMap<>();
            package2certs = new ConcurrentHashMap<>();
            domains =
                Collections.synchronizedSet(new HashSet<ProtectionDomain>());
            assertionLock = new Object();
        } else {
            // no finer-grained lock; lock on the classloader instance
            parallelLockMap = null;
            package2certs = new Hashtable<>();
            domains = new HashSet<>();
            assertionLock = this;
        }
    }

紧接着,我们再看一下ClassLoader抽象类中parent成员的声明:

// The parent class loader for delegation  
private ClassLoader parent; 

 声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:

1.系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

2.扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null(null 本身就代表着引导类加载器)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)

 事实上,这就是启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系。

3、类加载双亲委派示例

 以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子,首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

package com.huawei.classload001;

public class TestBean {
    public TestBean() {

    }
}

在现有当前工程中另外建立一个测试类(ClassLoaderTest.java)内容如下:
测试一:

package com.huawei.classload001;

public class ClassLoaderTest {
    public static void main(String[] args) {
        try {
            //查看当前系统类路径中包含的路径条目
            System.out.println(System.getProperty("java.class.path"));
            //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
            Class typeClassLoad = Class.forName("com.huawei.classload001.TestBean");
            //查看被加载的TestBean类型是被哪个类加载器加载的
            System.out.println(typeClassLoad.getClassLoader());
        }
        catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

打印结果:
“C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;
D:\masterSpring\jvm\target\classes;
D:\repo\sping\org\apache\commons\commons-lang\2.6\commons-lang-2.6.jar” com.huawei.classload001.ClassLoaderTest
C:\Program Files\Java\jdk1.8.0_91\jre\lib\charsets.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\deploy.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\access-bridge-64.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\cldrdata.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\dnsns.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jaccess.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\jfxrt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\localedata.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\nashorn.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunec.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunjce_provider.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunmscapi.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\sunpkcs11.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\ext\zipfs.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\javaws.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jce.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfr.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jfxswt.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\jsse.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\management-agent.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\plugin.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\resources.jar;
C:\Program Files\Java\jdk1.8.0_91\jre\lib\rt.jar;
D:\masterSpring\jvm\target\classes;
D:\repo\sping\org\apache\commons\commons-lang\2.6\commons-lang-2.6.jar;
D:\idea\IntelliJ IDEA 2018.3.1\lib\idea_rt.jar
sun.misc.Launcher$AppClassLoader@18b4aac2

测试二:
 将当前工程输出目录下的TestBean.class打包进test.jar剪贴到/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:
在这里插入图片描述
 对比测试一和测试二,我们明显可以验证前面说的双亲委派机制:系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。
测试三:
将test.jar拷贝一份到/lib下,运行测试代码,输出如下:

I:\AlgorithmPractice\TestClassLoader\bin
sun.misc.Launcher$ExtClassLoader@15db9742

 测试三和测试二输出结果一致。那就是说,放置到/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<JAVA_HOME>/lib目录下存在的陌生类,换句话说,虚拟机只加载<JAVA_HOME>/lib目录下它可以识别的类。因此,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<JAVA_HOME>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

三. Java 程序动态扩展方式

 Java的连接模型允许用户运行时扩展引用程序,既可以通过当前虚拟机中预定义的加载器加载编译时已知的类或者接口,又允许用户自行定义类装载器,在运行时动态扩展用户的程序。通过用户自定义的类装载器,你的程序可以加载在编译时并不知道或者尚未存在的类或者接口,并动态连接它们并进行有选择的解析。运行时动态扩展java应用程序有如下两个途径:
1、反射(调用java.lang.Class.forName(…)加载类)
这个方法其实在前面已经讨论过,在后面的问题2解答中说明了该方法调用会触发哪个类加载器开始加载任务。这里需要说明的是多参数版本的forName(…)方法:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader) throws ClassNotFoundException  

 这里的initialize参数是很重要的,它表示在加载同时是否完成初始化的工作(说明:单参数版本的forName方法默认是完成初始化的)。有些场景下需要将initialize设置为true来强制加载同时完成初始化,例如典型的就是利用DriverManager进行JDBC驱动程序类注册的问题。因为每一个JDBC驱动程序类的静态初始化方法都用DriverManager注册驱动程序,这样才能被应用程序使用。这就要求驱动程序类必须被初始化,而不单单被加载。Class.forName的一个很常见的用法就是在加载数据库驱动的时候。如 Class.forName(“org.apache.derby.jdbc.EmbeddedDriver”).newInstance()用来加载 Apache Derby 数据库的驱动。
2、用户自定义类加载器
 通过前面的分析,我们可以看出,除了和本地实现密切相关的启动类加载器之外,包括标准扩展类加载器和系统类加载器在内的所有其他类加载器我们都可以当做自定义类加载器来对待,唯一区别是是否被虚拟机默认使用。前面的内容中已经对java.lang.ClassLoader抽象类中的几个重要的方法做了介绍,这里就简要叙述一下一般 用户自定义类加载器的工作流程(可以结合后面问题解答一起看):
1、首先检查请求的类型是否已经被这个类装载器装载到命名空间中了,如果已经装载,直接返回;否则转入步骤2;
2、委派类加载请求给父类加载器(更准确的说应该是双亲类加载器,真实虚拟机中各种类加载器最终会呈现树状结构),如果父类加载器能够完成,则返回父类加载器加载的Class实例;否则转入步骤3;
3、调用本类加载器的findClass(…)方法,试图获取对应的字节码。如果获取的到,则调用defineClass(…)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败, 向上抛异常给loadClass(…), loadClass(…)转而调用findClass(…)方法处理异常,直至完成递归调用。

 必须指出的是,这里所说的自定义类加载器是指JDK1.2以后版本的写法,即不覆写改变
java.lang.loadClass(…)已有委派逻辑情况下。整个加载类的过程如下图:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值