Java虚拟机--ClassLoader(十九)

来源:互联网 发布:英国大陆均势政策 知乎 编辑:程序博客网 时间:2024/05/16 11:21

目录:ClassLoader工作在Class装载的加载阶段,主要作用是从系统外部获得Class二进制数据流

  

知识点的梳理:

  • 当系统需要加载一个类时,会先从顶层的启动类加载器开始加载,逐层往下,直到找到该类;
    • 判断一个类是否需要被加载,是从底层的应用类加载器开始判断的,如果已经在应用类加载器中的类,就不会请求上层类加载器了;
    • 判断一个类是否被加载时,顶层类加载器不会询问底层类加载器;
  • 由不同ClassLoader加载的同名类属于不同的类型,不能相互转化和兼容;

      

  • 通过ClassLoader了解类的加载
    • 所有的Class都是由ClassLoader进行加载的;
      • ClassLoader负责通过各种方式将Class信息的二进制数据流读入系统,然后交给Java虚拟机进行连接,初始化等操作;
      • ClassLoader在这个转载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的连接和初始化行为;
    • 抽象类ClassLoader提供的接口用于自定义Class的加载流程和加载方式:
      • public Class<?> loadClass(String name) throws ClassNotFoundException给定一个类名,加载一个类,返回代表这个类的Class实例,如果找不到类,则返回ClassNotFoundException异常;
      • protected final Class<?> defineClass(byte[] b,int off,int len):根据给定的字节码流b定义一个类,offlen参数表示实际Class信息在byte数组中的位置和长度,其中byte数组bClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用;
      • protected Class<?> findClass(String name) throws ClassNotFoundException:查找一个类,这是一个受保护的方法,也是重载ClassLoader时,重要的系统扩展点。这个方法会在loadClass()时被调用,用于自定义查找类的逻辑。如果不需要修改类加载默认机制,只是想改变类加载的形式,就可以重载该方法;
      • protected final Class<?> findLoadedClass(String name):受保护的方法,它会去寻找已经加载的类。
      • 字段parent:它是一个ClassLoader实例,该字段所表示的ClassLoader也被称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交与自己的双亲处理;
  • ClassLoader的分类
    • 标准的Java程序,虚拟机会创建3ClassLoader为整个应用程序服务
      • Bootstrap ClassLoader(启动类加载器)
      • Extension ClassLoader(扩展类加载器);
      • AppClassLoader(应用类加载器);
      • 每个应用程序还可以拥有自定义的ClassLoader,扩展Java虚拟机获取Class数据的能力

ClassLoader的层次自顶往下为启动类加载器,扩展类加载器,应用类加载器和自定义类加载器。
其中,应用类加载器的双亲为扩展类加载器,扩展类加载器的双亲为启动类加载器。当系统需要使用一个类时,在判断类是否已经被加载时,会先从当前底层类加载器进行判断。当系统需要加载一个类时,会从顶层类开始加载,依次向下尝试,直到成功;

在这些
ClassLoader中,启动类加载器完全由C代码实现,且在Java中没有对象与之对应。系统的核心类就是由启动类加载器进行加载的。它也是虚拟机核心组件。

扩展类加载器和应用类加载器都有对应的
Java对象可供使用

  • 示例:验证各个加载器之间的关系

 

代码中先取得装载当前类PrintClassLoaderTree的ClassLoader,然后打印当前ClassLoader并获得其双亲,直到类加载器树被遍历完成:
结果
:

PrintClassLoaderTree用户类加载于AppClassLoader(应用类加载器)中,而AppClassLoader的双亲为ExtClassLoader(扩展类加载器)。而从ExtClassLoader无法再取得启用类加载器,因为这是一个系统级的纯C实现。因此,任何加载在启动类加载器中的类是无法获得其ClassLoader实例的,比如:

String属于Java核心类,因此会被启动类加载器加载,故以上代码返回null

  • 分散ClassLoader装载类的好处
    • 不同层次的类可以由不同的ClassLoader加载,从而进行划分,这有助于系统的模块化设计。
    • 启动类加载负责加载系统核心类,比如rt.jar中的java类;
    • 扩展类加载器用于加载%JAVA_HOME%/lib/ext/*.jar中的java类;
    • 应用类加载器用于加载用户类,也就是用户程序的类;
    • 自定义类加载器用于加载一些特殊途径的类,一般也是用户程序类;
  • ClassLoader的双亲委托模式
    • ClassLoader在协同工作时,默认使用双亲委托模式;
      • 原理:在类加载的时候,系统会判断当前类是否已经被加载,如果已经被加载,会直接返回可用的类,否则就会尝试加载;在尝试加载时,会先请求双亲处理,如果双亲请求失败,则会自己加载;
    • 示例:ClassLoader加载类的详细过程

分析:
在代码第
6行,ClassLoader试图查找该类是否已经被加载,如果已经被加载则直接返回。
如果没有被加载,则会在第
11行请求其双亲加载(不是自己加载),如果双亲为null时,则使用启动类加载器加载。如果双亲加载不成功,则会在24行由当前ClassLoader尝试加载;

说明:
双亲为
null有两种情况:
第一,其双亲就是启动类加载器;
第二,当前加载器就是启动类加载器;

  • 总结:判断类是否被加载,应用类加载器会顺着双亲路径往上判断,直到启动类加载器,但是启动类加载器不会往下询问,这个委托路线是单向的。这点非常重要!
  • 双亲委托模式的弊端
    • 检查类是否被加载的委托过程是单向的。这种方式从结构上说比较清晰,使各个ClassLoader的职责非常明确,但会带来一个问题:
      • 顶层的ClassLoader无法访问底层的ClassLoader所加载的类;
    • 通常,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。
      • 这种情况下,应用类访问系统类没有问题。但系统类访问应用类就会出现问题;
      • 比如:在系统类中,提供了一个接口,该接口需要在应用中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中;这时,会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。
      • 拥有这种问题的组件包括:JDBCXml Parser等;
  • 双亲委托模式的补充
    • 在java中,核心类(rt.jar)中提供外部服务,可由应用层自行实现的接口,可称为Service Provider Interface,即SPI
    • 示例:以javax.xml.parsers中实现XML文件解析功能模块为例,说明如何在启动类加载器中,访问由应用类加载器实现的SPI接口实例;
      • javax.xml.parsers.DocumentBuilderFactory中有如下实现,用来构造一个DocumentBuilderFactory实例,注意DocumentBuilderFactory是一个抽象类(加载在启动类加载器中),可以由应用程序自行实现;

FactoryFind.find()函数试图加载并返回一个DocumentBuilderFactory实例。当这个实例在应用层jar包里时,它会使用如下方法进行查找:

  

形参factoryId就是字符串"javax.xml.parsers.DocumentBuilderFactory"

  • findJarServiceProvider的部分源码如下:

分析:
系统通过读取
jar包中META-INF/services目录下的类名文件,读取工厂类类名,然后根据类名生成对应的实例。
加粗部分为关键代码,这里获取一个上下文加载器的
ClassLoader(也就是加粗的cl),并将此ClassLoader传入newInstance()方法,由这个ClassLoader去完成实例的加载和创建,而不是由这段代码所在的启动类加载器去加载。从而解决了启动类加载器无法访问factoryClassName指定类的问题;

  • ss.getContextClassLoader的源码分析,它如何获得这个上下文加载器
    • Thread.getContextClassLoader()中得到
    • Thread类的两个方法:

      • 这两个方法分别取得设置在线程中的上下文加载器和设置一个线程的上下文加载器,通过这两个方法,可以把一个ClassLoader置于一个线程实例之中,使该ClassLoader成为一个相对共享的实例。默认情况下,上下文加载器就是应用类加载器,这样即使是在启动类加载器中的代码也可以通过这种方式访问应用类加载器中的类了;
      • 下图显示了以上下文加载器作为中介,使得启动类加载器得以访问应用类加载器的类
  • 突破双亲模式
    • 双亲模式的加载是虚拟机的默认行为,可以通过重载ClassLoader来修改此行为;
    • 示例:下面的代码通过重载loadClass()方法,改变类的加载次序

以上代码通过自定义ClassLoader,重载loadClass()改变了默认的委托双亲加载方法。第10行通过findClass()读取class文件,并将二进制流定义为Class对象。
如果加载不到,则委托双亲加载,这种方式颠倒了默认的加载顺序

  • 热替换的实现
    • 说明:是指在程序运行过程中,不停止服务,只通过替换程序文件来修改程序的行为;
      • 作用:热替换的关键的需求在于服务不能停止,修改必须立即表现在正在运行的系统之中;
    • Java中的一个类加载到系统中,通过修改类文件,是无法让系统再来加载并重定义这个类的。Java实现热替换的一个可行方法是灵活运用ClassLoader;
    • 示例:在路径"D:/tmp/clz"下存放一个类"geym.zbase.ch10.findorder.HelloLoader",同时在当前ClassPath下也存放同名类"geym.zbase.ch10.findorder.HelloLoader",后者会打印字符串"I am in apploader";
      • 使用参数"-Xbootclasspath/a:D:/tmp/clz"运行以下代码:

运行结果:
null
I am in Boot ClassLoader

证明HelloLoader由启动类加载器加载

  • 修改代码:

运行这段代码回抛出如下异常:


分析:
从该错误看出,两个同名类居然无法相互转换,这是因为它们由不同的
ClassLoader加载的。

注意:两个不同
ClassLoader加载同一个类,在虚拟机内部,会认为这两个类是完全不同的。

  • 利用上例的特点,来模拟热替换的实现,基本思路如下图:

    • 首先需要自定义ClassLoader,它可以在给定目录下查找目标类,主要的实现思路是重载findClass()方法

分析:
findClass()的实现中,第9行,查找已经加载的类,如果类已经加载,则不作重复加载;
11~29行,通过文件查找,读取Class的二进制数据流;
31行,将此二进制数据流定义为Class,并返回该Class对象

  • 准备一个需要被热替换的类,命名为DemoA,代码如下,调用其hot()方法,将打印字符串"OldDemoA";
  • 将生成的DemoAclass文件放置于目录D:\tmp\clz下。建立热替换支持类DoopRum,它使用MyClassLoader在路径D:\tmp\clz下查找并且更新DemoA的实现;

分析:
该代码每次在调用
DemoA.hot()方法之前,都会重新加载DemoA,从而实现热替换。

这段代码会不停输出"Old
DemoA"字符串

  • 修改DemoA.hot()的输出字符为"NewDemoA",重新生成新的class文件,并将其覆盖到D:\tmp\clz下,在不停止DoopRun程序的情况下发现DemoA可以被更新:
原创粉丝点击