浅析ClassLoader--jdk1.8

本文部分内容参照以下
https://cloud.tencent.com/developer/article/1442353
https://blog.csdn.net/zezezuiaiya/article/details/84763695
https://blog.csdn.net/u014634338/article/details/81434327

什么是ClassLoader

负责将 Class 的字节码形式转换成内存形式的 Class 对象.可以是本地硬盘里的*.class文件,也可以是jar包里的*.class文件,也可以是来自远程的字节流.本质就是byte[].

每个Class对象里都有private final ClassLoader classLoader;来表示这个类属于哪个类加载器.

不同的类加载器加载的类一定不同,即使是加载的同一份*.class文件.

ClassLoader类型

JVM 中内置了三个重要的 ClassLoader,分别是 BootstrapClassLoader、ExtensionClassLoader 和 AppClassLoader

BootstrapClassLoader

俗称[根加载器],负责加载jdk的核心类库.这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.* 等等.是C语言实现.

ExtensionClassLoader

负责加载 JVM 扩展类,比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包

AppClassLoader

直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录.我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的.
AppClassLoader 可以由 ClassLoader 类提供的静态方法 getSystemClassLoader() 得到,它就是我们所说的「系统类加载器」,我们用户平时编写的类代码通常都是由它加载的。当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader.

URLClassLoader

URLClassLoader 不但可以加载远程类库,还可以加载本地路径的类库,取决于构造器中不同的地址形式.
ExtensionClassLoader 和 AppClassLoader 都是 URLClassLoader 的子类,它们都是从本地文件系统里加载类库.

ClassLoader 传递性

要加载一个未知的类,默认就是使用调用者Class对象的ClassLoader.
所以main方法的ClassLoader会负责所有的延迟加载,即AppClassLoader.

延迟加载:JVM并不会在程序运行就加载所有的类,程序运行过程中可能会遇到一些不认识的类,这时候ClassLoader就会加载它,并保存到ClassLoader中,下次就不需要重新加载.
在调用某个类的静态变量(不包括常量,因为常量在常量池中)或者静态方法时,就会加载类,然后初始化,但此时并不会实例化

双亲委派模式

在这里插入图片描述

这并不是ClassLoader的继承关系,而是双亲委派模型

当当一个ClassLoader去加载一个类时,它不会直接就去加载,会先去它的上一级ClassLoader中取寻找,以此类推.

例如:当用户自定义ClassLoader想要去加载一个类的时候,是先看BootstrapClassLoader是否加载了这个类,如果没有,再去ExtClassLoader中看是否加载了,如果没有,再去AppClassLoader中寻找,都没有的话,才会通过用户自定义ClassLoader去加载类.

自定义ClassLoader

ClassLoader 里面有三个重要的方法 loadClass()、findClass() 和 defineClass()

loadClass() 方法是加载目标类的入口,它首先会查找当前 ClassLoader 以及它的双亲里面是否已经加载了目标类,如果没有找到就会让双亲尝试加载,如果双亲都加载不了,就会调用 findClass() 让自定义加载器自己来加载目标类。ClassLoader 的 findClass() 方法是需要子类来覆盖的,不同的加载器将使用不同的逻辑来获取目标类的字节码。拿到这个字节码之后再调用 defineClass() 方法将字节码转换成 Class 对象。

所以我们自定义ClassLoader时,不能轻易复写loadClass(),否则可能破坏双亲委托模式

双亲委派模式的优势

可以避免重复加载,当父加载器已经加载了该类的时候,就没有必要 ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

类的生命周期

在这里插入图片描述

  1. 加载:加载过程就是把class字节码文件载入到虚拟机中,至于从哪儿加载,虚拟机设计者并没有限定,你可以从文件、压缩包、网络、数据库等等地方加载class字节码。
    通过类的全限定名来获取定义此类的二进制字节流
    将此二进制字节流所代表的静态存储结构转化成方法区的运行时数据结构
    在内存中生成代表此类的java.lang.Class对象,作为该类访问入口.
  2. 验证:验证的目的是确保class文件的字节流中信息符合虚拟机的要求,不会危害虚拟机安全,使得虚拟机免受恶意代码的攻击,这一步至关重要。
    文件格式验证
    源数据验证
    字节码验证
    符号引用验证
  3. 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。(仅包含类变量,不包含实例变量).
  4. 解析:虚拟机将常量池中的符号引用替换为直接引用,解析动作主要针对类或接口,字段,类方法,方法类型等等。
  5. 初始化:在该阶段,才真正意义上的开始执行类中定义的java程序代码,该阶段会执行类构造器,并且在Java虚拟机规范中有明确的规定,在下面5种情况下必须对类进行初始化:
    遇到new、getstatic、putstatic、invokestatic这4条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
    使用java.long.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    当初始化一个类的时候,如果发现其父类没有进行过初始化,则需要先触发其父类的初始化。
    当虚拟机启动时,需要制定一个执行的主类(即main方法的类),虚拟机必须先初始化这个类。
    使用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。
  6. 使用:使用该类所提供的功能,其中包括主动引用和被动引用。
    主动引用:
    通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
    通过反射方式执行以上三种行为。
    初始化子类的时候,会触发父类的初始化。
    作为程序入口直接运行时(也就是直接调用main方法)。
    被动引用:
    引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
    定义类数组,不会引起类的初始化。
    引用类的常量,不会引起类的初始化。
  7. 卸载:从内存中释放,在我之前写的垃圾回收机制(GC)总结一文中有介绍到方法区内存回收中对类的回收条件,这里再贴出来一下:
    该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
    加载该类的ClassLoader已经被回收;
    该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

分工与合作

这里我们重新理解一下 ClassLoader 的意义,它相当于类的命名空间,起到了类隔离的作用。位于同一个 ClassLoader 里面的类名是唯一的,不同的 ClassLoader 可以持有同名的类。ClassLoader 是类名称的容器,是类的沙箱。

不同的 ClassLoader 之间也会有合作,它们之间的合作是通过 parent 属性和双亲委派机制来完成的。parent 具有更高的加载优先级。除此之外,parent 还表达了一种共享关系,当多个子 ClassLoader 共享同一个 parent 时,那么这个 parent 里面包含的类可以认为是所有子 ClassLoader 共享的。这也是为什么 BootstrapClassLoader 被所有的类加载器视为祖先加载器,JVM 核心类库自然应该被共享。

Thread.contextClassLoader

Thread类中有一个public ClassLoader getContextClassLoader()
首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它。你可以使用下面这种方式来显示使用它

Thread.currentThread().getContextClassLoader().loadClass(name);

这意味着如果你使用 forName(string name) 方法加载目标类,它不会自动使用 contextClassLoader。那些因为代码上的依赖关系而懒惰加载的类也不会自动使用 contextClassLoader来加载。

其次线程的 contextClassLoader 是从父线程那里继承过来的,所谓父线程就是创建了当前线程的线程。程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader。

那这个 contextClassLoader 究竟是做什么用的?我们要使用前面提到了类加载器分工与合作的原理来解释它的用途。

它可以做到跨线程共享类,只要它们共享同一个 contextClassLoader。父子线程之间会自动传递 contextClassLoader,所以共享起来将是自动化的。

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来。

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

线程的 contextClassLoader 使用场合比较罕见,如果上面的逻辑晦涩难懂也不必过于计较。

JDK9 增加了模块功能之后对类加载器的结构设计做了一定程度的修改,不过类加载器的原理还是类似的,作为类的容器,它起到类隔离的作用,同时还需要依靠双亲委派机制来建立不同的类加载器之间的合作关系。

发布了37 篇原创文章 · 获赞 1 · 访问量 1057

猜你喜欢

转载自blog.csdn.net/zhuchencn/article/details/103627039