一篇文章读懂Java类加载器

Java类加载器算是一个老生常谈的问题,大多Java工程师也都对其中的知识点倒背如流,最近在看源码的时候发现有一些细节的地方理解还是比较模糊,正好写一篇文章梳理一下。

关于Java类加载器的知识,网上一搜一大片,我自己也看过很多文档,博客。资料虽然很多,但还是希望通过本文尽量写出一些自己的理解,自己的东西。如果只是重复别人写的内容那就失去写作的意义了。

类加载器结构

名称解释:

  1. 根类加载器,也叫引导类加载器、启动类加载器。由于它不属于Java类库,这里就不说它对应的类名了,很多人喜欢称BootstrapClassLoader。本文都称之为根类加载器。
    加载路径:<JAVA_HOME>\lib
  2. 扩展类加载器,对应Java类名为ExtClassLoader,该类是sun.misc.Launcher的一个内部类。
    加载路径:<JAVA_HOME>\lib\ext
  3. 应用类加载器,对应Java类名为AppClassLoader,该类是sun.misc.Launcher的一个内部类。
    加载路径:用户目录
//可以通过这种方式打印加载路径
System.out.println("boot:"+System.getProperty("sun.boot.class.path"));
System.out.println("ext:"+System.getProperty("java.ext.dirs"));
System.out.println("app:"+System.getProperty("java.class.path"));

重点说明:

  1. 根类加载器对于普通Java工程师来讲可以理解成一个概念上的东西,因为我们无法通过Java代码获取到根类加载器,它属于JVM层面。
  2. 除了根类加载器之外,其他两个扩展类加载器和应用类加载器都是通过类sun.misc.Launcher进行初始化,而Launcher类则由根类加载器进行加载。

看下Launcher初始化源码:

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);

       //删除了一些安全方面的代码
       //...
}

双亲委派模型

双亲委派模型是指当我们调用类加载器的loadClass方法进行类加载时,该类加载器会首先请求它的父类加载器进行加载,依次递归。如果所有父类加载器都加载失败,则当前类加载器自己进行加载操作。
逻辑很简单,通过ClassLoader类的源码来分析一下。

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) {
                        //如果当前类没有被加载且父类加载器不为null,则请求父类加载器进行加载操作
                        c = parent.loadClass(name, false);
                    } else {
                       //如果当前类没有被加载且父类加载器为null,则请求根类加载器进行加载操作
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //如果父类加载器加载失败,则由当前类加载器进行加载,
                    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;
        }
    }

双亲委派模型的实现逻辑总体看还是非常简单明了的。
这里有几个细节需要说明:

  1. ClassLoader类是一个抽象类,但却没有包含任何抽象方法。
  2. 如果要实现自己的类加载器且不破坏双亲委派模型,只需要继承ClassLoader类并重写findClass方法。
  3. 如果要实现自己的类加载器且破坏双亲委派模型,则需要继承ClassLoader类并重写loadClass,findClass方法。

令人疑惑的系统类加载器

当你把上面的知识都搞清楚以后,会发现ClassLoader类中有个方法是getSystemClassLoader,系统类加载器,这又是什么?
系统类加载器是个容易让人混淆的概念,我一度以为它就是应用类加载器的别名,就跟启动类加载器和根类加载器道理一样。事实上,默认情况下我们通过ClassLoader.getSystemClassLoader()获取到的系统类加载器也确实是应用类加载器
很多资料在说类加载器结构的时候会直接把应用类加载器说成是系统类加载器,其实我们通过类名就可以判断两个不是一回事。
系统类加载器可以通过System.setProperty("java.system.class.loader", xxx类名)进行自定义设置。
系统类加载器不是一个全新的加载器,它只是一个概念,本质上还是上述说的四大类加载器(把用户自定义类加载器算进去),至于提出这个概念的原因以及使用场景,还需要继续考究。

被人忽略的上下文类加载器

上面讨论了各个类加载器的加载路径。鉴于双亲委派模型的设计,子类加载器都保留了父类加载器的引用,也就是说当由子类加载器加载的类需要访问由父类加载器加载的类时,毫无疑问是可以访问到的。但考虑一种场景,会不会有父类加载器加载的类需要访问子类加载器加载的类这种情况?如果有,怎么解决(父类加载器并没有子类加载器的引用)?
这就是我们要讨论的常常被人们忽略的上下文类加载器。
经典案例:
JDBC是Java制定的一套访问数据库的标准接口,它包含在Java基础类库中,也就是说它是由根类加载器加载的。与此同时,各个数据库厂商会各自实现这套接口来让Java工程师可以访问自己的数据库,而这部分实现类库是需要Java工程师在工程中作为一个第三方依赖引入使用的,也就是说这部分实现类库是由应用类加载器进行加载的。
先上一段Java获取Mysql连接的代码:

//加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//连接数据库
Connection conn = DriverManager.getConnection(url, user, password);

这里DriverManager类就属于Java基础类库,由根类加载器加载。我们可以通过它获取到数据库的连接,显然是它通过com.mysql.jdbc.Driver驱动成功连接到了数据库,上面也说了数据库驱动(作为第三方类库引入)是由应用类加载器加载的。这个场景就是典型的由父类加载器加载的类需要访问由子类加载器加载的类。
Java是怎么实现这种逆向访问的呢?直接看DriverManager类的源码:

//建立数据库连接各个不同参数的方法最终都会走到这里
private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //获取调用者的类加载器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        synchronized(DriverManager.class) {
            //如果为null,则使用上下文类加载器
            //这里是重点,什么时候类加载器才会为null? 当然就是由根类加载器加载的类了
            if (callerCL == null) {
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        //...省略

        for(DriverInfo aDriver : registeredDrivers) {
            //使用上下文类加载器去加载驱动
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    //如果加载成功,则进行连接
                    Connection con = aDriver.driver.connect(url, info);
                    //...
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
            }
            //...
        }
    }

重点说明:
为什么上下文类加载器就可以加载到数据库驱动呢?回到上面一开始Launcher初始化类加载器的源码,我们发现原来所谓的上下文类加载器本质上就是应用类加载器,有没有豁然开朗的感觉?上下文类加载器只是为了解决类的逆向访问提出来的一个概念,并不是一个全新的类加载器,它本质上就是应用类加载器



基本上我理解的Java类加载器就这么多知识,如果有没提到的或者是错误的地方,欢迎交流。

Java学习交流QQ群:523047986  禁止闲聊,非喜勿进!

时间: 2024-12-18 09:34:26

一篇文章读懂Java类加载器的相关文章

一文读懂Java类加载机制

Java 类加载机制 Java 类加载机制详解. @pdai Java 类加载机制 类的生命周期 类的加载:查找并加载类的二进制数据 连接 验证:确保被加载的类的正确性 准备:为类的静态变量分配内存,并将其初始化为默认值 解析:把类中的符号引用转换为直接引用 初始化 使用 卸载 类加载器, JVM类加载机制 类加载器的层次 寻找类加载器 类的加载 JVM类加载机制 自定义类加载器 参考文章 类的生命周期 其中类加载的过程包括了加载.验证.准备.解析.初始化五个阶段.在这五个阶段中,加载.验证.准

一篇文章读懂volatile

前提 计算机在执行程序代码的时候,实际上执行的是一条条指令,而这些指令,肯定会涉及到数据的读取和写入操作. 在我们的程序中,所定义的变量等临时数据,计算机会放在内存中,也称为主存. 那么问题来了,CPU执行指令的速度是很快的,但是从内存中读取数据和写入数据的过程,相比CPU执行指令的速度来说是比较慢的.如果每个程序都是直接从内存中读取数据,那么由于CPU执行指令的速度和数据的读取写入操作的速度不一致,那么肯定会大大降低了执行的效率,所以在CPU里面引入了高速缓存. 当程序在运行过程中,会将运算需

一篇文章看懂Java并发和线程安全

一.前言 长久以来,一直想剖析一下Java线程安全的本质,但是苦于有些微观的点想不明白,便搁置了下来,前段时间慢慢想明白了,便把所有的点串联起来,趁着思路清晰,整理成这样一篇文章. 二.导读 1.为什么有多线程? 2.线程安全描述的本质问题是什么? 3.Java内存模型(JMM)数据可见性问题.指令重排序.内存屏障 三.揭晓答案 1.为什么有多线程 谈到多线程,我们很容易与高性能画上等号,但是并非如此,举个简单的例子,从1加到100,用四个线程计算不一定比一个线程来得快.因为线程的创建和上下文切

【转】一篇文章读懂人力资源三支柱体系(COE?BP?SSC)

通过人力资源转型,提升效率和效能   作者:Sharon Li,翰威特大中华区咨询总监. 杰克韦尔奇曾说过“人力资源负责人在任何企业中都应该是第二号人物”,但在中国,99%的企业都做不到.原因很简单,人力资源部没创造这么大的价值——业务增长很快,但HR总在拖后腿.有些人说人力资源部是“秘书”,有人说人力资源是“警察”,在中国,真正认为人力资源部是“业务伙伴”的,真是凤毛麟角. 研究证明,人力资源部可以成为业务驱动力,关键是HR自身要转型. 1. 重新定位人力资源部门 人力资源部成为业务的驱动力,

趣味学习:一篇文章读懂三层交换机【新任帮主】

为什么我们说三层交换机的三层转发性能要比路由器的效率要高的多?有的时候在很多书上面会提及到现在路由器的软件的做的也非常强大,几乎也能够达到限速转发的能力: 软件能够和硬件比吗,不太可能:交换机之所以转发速度快是因为交换机使用的专门的ASIC硬件转发卡,而路由器是software-based 的转发: 我们习惯说,在二层网络环境中相同vlan之间可以通信,不同vlan之间不可以通信,如果想通信必须借助三层设备,所以说三层交换机必须要做的事情是路由转发,但是具体的工作原理是什么样的呢 ,接着看吧!

一篇文章读懂什么是串口通信及其工作原理

介绍 串行通信是在数据处理设备和外围设备之间传输信息的最广泛使用的方法.一般而言,沟通意味着通过书面文件,口头语言,音频和视频课程在个人之间交换信息. 每台设备都可能是您的个人计算机或移动设备在串行协议上运行.该协议是安全可靠的通信形式,具有由源主机(发送方)和目的地主机(接收方)寻址的一组规则.为了获得更好的洞察力,我已经解释了串行通信的概念. 在嵌入式系统中,串行通信是以串行数字二进制形式使用不同方法交换数据的方式.用于数据交换的一些众所周知的接口是RS-232,RS-485,I2C,SPI

JAVA 类加载器 第14节

JAVA 类加载器 第14节 今天我们将类加载机制5个阶段中的第一个阶段,加载,又叫做装载.为了阅读好区分,以下都叫做装载. 装载的第一步就是要获得二进制的字节流,它可以从读.class文件获得,也可以从网络中接收别人发送的字节流.反正只要符合虚拟机规定的字节流格式都可以进入这个阶段. 有了字节流了之后,要进行装载还需要一个工具,那就是加载器了.加载器既可以使用系统提供的引导类加载器,也可以用户自己定义加载器,只需要继承ClassLoader,再重写loadClass()方法就可以实现一个自己的

java类加载器-系统类加载器

系统类加载器 系统类加载器可能都耳详能熟,但是为了完整点,还是先简单的说说系统的类加载器吧. public class Test { public static void main(String[] args) { ClassLoader cl1 = Test.class.getClassLoader().getParent().getParent(); System.out.println(cl1); ClassLoader cl2 = Test.class.getClassLoader().

Java类加载器的工作原理

Java类加载器的作用就是在运行时加载类.Java类加载器基于三个机制:委托.可见性和单一性.委托机制是指将加载一个类的请求交给父类加载 器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它.可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类 加载器加载的类.单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类.正确理解类加载器能够帮你解决 NoClassDefFoundError和java.lang.ClassNo