【小家Java】从原理层面理解Java中的类加载器:ClassLoader、双亲委派模型、线程上下文类加载器

每篇一句

必须从过去的错误学习教训而非依赖过去的成功

前言

java.lang.ClassLoader类的基本职责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个Java 类,即 java.lang.Class类的一个实例。

类加载器是平时开发中基本不会接触的问题,但是在高阶应用中必须要深入其原理才能予以自用。比如tomcat加载web-jar就是通过自己的ClassLoader去加载进来的。同时这个也是高级技术常常会问到的一个专题,因此本文针对于此做一些叙述,希望对大家能够有所帮助

普通的java开发者其实用到的不多,但对于某些框架开发者来说却非常常见。理解ClassLoader的加载机制,也有利于我们编写出更高效的代码。ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。

Java程序启动时,并不是一次把所有的类全部加载后再运行,它总是先把保证程序运行的基础类一次性加载到jvm中,其它类等到jvm用到的时候再加载,这样的好处是节省了内存的开销,因为java最早就是为嵌入式系统而设计的,内存宝贵,这是一种可以理解的机制,而用到时再加载这也是java动态性的一种体现

类加载器的三个机制(约束)
委托

委托机制是指将加载一个类的请求交给父类加载器,如果这个父类加载器不能够找到或者加载这个类,那么再加载它。(双亲委派模型
在这里插入图片描述

可见性

可见性的原理是子类的加载器可以看见所有的父类加载器加载的类,而父类加载器看不到子类加载器加载的类

单一性

单一性原理是指仅加载一个类一次,这是由委托机制确保子类加载器不会再次加载父类加载器加载过的类

类加载器的种类

在这里插入图片描述
JAVA类装载方式,有两种:

  1. 隐式装载, 程序在运行过程中当碰到通过 new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。
  2. 显式装载, 通过class.forname()等方法,显式加载需要的类

有三种默认使用的类加载器:Bootstrap类加载器Extension类加载器System类加载器(或者叫作Application类加载器)。每种类加载器都有设定好从哪里加载类。

  • Bootstrp加载器:是用C++语言写的(其余均为Java写的),它是在Java虚拟机启动后初始化的,它主要负责加载rt.jar中的类。(JDK的核心类,如String、Integer等等类) 它对rt.jar的加载全盘负责

  • ExtClassLoader:Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader.ExtClassLoader。是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader。xtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。
    在这里插入图片描述

  • AppClassLoader:Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader。 另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader。

System.out.println(System.getProperty(“java.class.path”));可以获得classpath的配置,也就是system classloader 加载的类
在这里插入图片描述

  • AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器
    在这里插入图片描述
    需要注意的是,如下例子:
    public static void main(String[] args) {
        System.out.println(String.class.getClassLoader()); //null
        System.out.println(Main.class.getClassLoader().getParent()); //sun.misc.Launcher$ExtClassLoader@23fc625e
    }

我们发现String类的类加载器为null,肿么回事呢?

其实前面有提到Bootstrap Loader是用C++语言写的,依java的观点来看,逻辑上并不存在Bootstrap Loader的类实体,所以在java程序代码里试图打印出其内容时,我们就会看到输出为null。

Class类没有public的构造方法,Class对象是在装载类时由JVM通过调用类装载器中的defineClass()方法自动构造的。

类加载过程

JVM将类加载过程分为三个步骤:装载(Load),链接(Link)和初始化(Initialize)
在这里插入图片描述
1) 装载:查找并加载类的二进制数据;
2)链接:
验证:确保被加载类信息符合JVM规范、没有安全方面的问题。
准备:为类的
静态变量
分配内存,并将其初始化为默认值。
解析:把虚拟机常量池中的符号引用转换为直接引用
3)初始化:
为类的静态变量赋予正确的初始值。

ps:解析部分需要说明一下,Java 中,虚拟机会为每个加载的类维护一个常量池【不同于字符串常量池,这个常量池只是该类的字面值(例如类名、方法名)和符号引用的有序集合。 而字符串常量池,是整个JVM共享的】这些符号(如int a = 5;中的a)就是符号引用,而解析过程就是把它转换成指向堆中的对象地址的相对地址。

为何需要双亲委派模型?

可能有人会问,为什么要双亲委派模型呢?自己直接加载不就完事了吗?那看看下面这个场景:

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行(但十分十分不建议这么去做,正所谓不作死就不会死)。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

比如如下:

ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实ClassLoader类默认的loadClass方法已经帮我们写好了,一般情况下我们无需去写。

双亲委派模式优势:
1、采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
2、其次是考虑到安全因素(也就是上面提到的),java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

思考:假如我们自己写了一个java.lang.String的类,我们是否可以替换调JDK本身的类?
答案是否定的。我们不能实现。为什么呢?我看很多网上解释是说双亲委托机制解决这个问题,其实不是非常的准确。因为双亲委托机制是可以打破的,你完全可以自己写一个classLoader来加载自己写的java.lang.String类,但是你会发现也不会加载成功,具体就是因为针对java.*开头的类,jvm的实现中已经保证了必须由bootstrp来加载。(全盘负责)

自定义类加载器

既然JVM已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?

因为Java中提供的默认ClassLoader,只加载指定目录下的jar和class,如果我们想加载其它位置的类或jar时,比如:我要加载网络上的一个class文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的ClassLoader就不能满足我们的需求了,所以需要定义自己的ClassLoader。

定义自已的类加载器分为两步:

  1. 继承java.lang.ClassLoader
  2. 重写父类的findClass方法(应为父类此方法没有默认实现,子类必须实现)。其余方法都不用我们实现,因为JDK已经在loadClass方法中帮我们实现了ClassLoader搜索类的算法,当在loadClass方法中搜索不到类时,loadClass方法就会调用findClass方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写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) { //若存在父加载器,就调用父加载器的loadClass()方法,这样就形成了递归
                        c = parent.loadClass(name, false);
                    } else { //若不存在父加载器,那就找BootstrapClass即可
                        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); //调用者自己实现findClass逻辑即可

                    // 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;
        }
    }

调用者需要自己实现findClass方法

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }

自定义类加载器的方法:
1、如果不想打破双亲委派模型,那么只需要重写findClass方法即可
2、如果想打破双亲委派模型,那么就重写整个loadClass方法

加载一个类的过程如下:
在这里插入图片描述

线程上下文类加载器

该加载器十分的重要,也十分的优雅。在Tomcat和Spring中有大量的应用。作为补充,它可以补充JDK提供的三种加载器不能实现的功能,使之更为灵活。

双亲委派模型痛点场景:
Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC(Java官方并不提供具体实现,而是由各自的数据库厂商去实现)、JCE、JNDI、JAXP 和 JBI 等。

SPI接口均由Java核心库来提供,而实现代码都为其余厂商提供(一般都在我们引入的第三方jar包里面)。所以问题就来了:SPI接口中的代码经常需要加载具体的实现类,也就是说我再加载JDBC的时候就需要有实现类。 但是SPI接口是Bootstrap Classloader来加载的,而实现类在类路径由AppClassLoader来加载,所以SPI加载的时候铁定就加载不到实现类了。(因为违反了层级委托关系嘛)

解决方案:JDK1.2提供了上下文类加载器来解决此问题。它破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。看了很多博文,我一直都不理解它具体是如何打破“双亲委派模型”呢?知道我看到了JDBC驱动的加载过程,才彻底的了解了里面的原因~

写个案例:

    public static void main(String[] args) throws SQLException {
        //Class.forName("com.mysql.jdbc.Driver");

        Connection conn = java.sql.DriverManager.getConnection("jdbc:mysql://localhost:3306/jedi", "name", "password");
        System.out.println(conn); //com.mysql.jdbc.JDBC4Connection@15d0c81b
    }

细心的朋友会发现,我把平时我们认为必须要写的Class.forName("com.mysql.jdbc.Driver");这句代码去掉了,但程序还是能正常运行获取到链接。
这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类静态代码块,so玄机就在这个DriverManager的静态代码块里。

当然里面玄机还有很多,但核心原理就是利用到了上下文加载器来实现加载,具体各位可以下面博文,它比我说得好~
Java上线文加载器加载JDBC驱动
在这里插入图片描述

URLClassLoader

位于java.net包。从JDK源码上来看其实是URLClassLoader继承了ClassLoader,也就是说URLClassLoader把ClassLoader扩展了一下,所以可以理解成URLClassLoader功能要多点。

ClassLoader只能加载classpath下面的类,而URLClassLoader可以加载**任意路径**下的类。
们的继承关系如下:

public class URLClassLoader extends SecureClassLoader {}
public class SecureClassLoader extends ClassLoader {}

URLClassLoader提供了这个功能,它让我们可以通过以下几种方式进行加载:
* 文件: (从文件系统目录加载)
* jar包: (从Jar包进行加载)
* Http: (从远程的Http服务进行加载)

在Java7的Build 48版中,URLClassLoader提供了close()这个方法,可以将打开的资源全部释放掉,这个给开发者节省了大量的时间来精力来处理这方面的问题。

在这里插入图片描述
URLClassLoader 是AppClassLoader和ExtClassLoader的父类,它既可以从本地 文件系统获取二进制加载类,也可以从远程主机获取文件来加载类。

URLClassLoader 动态加载远程jar的代码实现:
借助URLClassLoader 来读取外部的jar包内的class文件,参考下面这个链接:
java中使用URLClassLoader访问外部jar包的java类

总结

以上是关于类加载器的一些介绍和工作原理。知道委托、可见性以及单一性原理,这些对于调试类加载器相关问题时至关重要。这些对于Java高级程序员和架构师来说都是必不可少的知识。


关注A哥

AuthorA哥(YourBatman)
个人站点www.yourbatman.cn
E-mailyourbatman@qq.com
微 信fsx641385712
活跃平台
公众号BAT的乌托邦(ID:BAT-utopia)
知识星球BAT的乌托邦
每日文章推荐每日文章推荐

BAT的乌托邦

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值