类的加载器

类的加载器,第1张

类的加载器 概述

类加载器是JVM执行类加载机制的前提。
ClassLoader的作用:
ClassLoader是Java的核心组件,所有的Class都是由ClassLoader进行加载的,ClassLoader负责通过各种方式将Class信息的二进制数据流读入JVM内部,转换为一个与目标类对应的java.lang.Class对象实例。然后交给Java虚拟机进行链接、初始化等 *** 作。因此,ClassLoader在整个装载阶段,只能影响到类的加载,而无法通过ClassLoader去改变类的链接和初始化行为。至于它是否可以运行,则由Execution Engine决定。


类加载器最早出现在Java1.0版本中,那个时候只是单纯地为了满足Java Applet应用而被研发出来。但如今类加载器却在OSGi、字节码加解密领域大放异彩。这个主要归功于Java虚拟机的设计者们当初在设计类加载器时候,并没有考虑将它绑定在JVM内部,这样做的好处就是能够更加灵活和动态地执行类加载 *** 作。

类的加载分类:显式加载 和 隐式加载

class文件的显式加载与隐式加载的方式是指JVM加载class文件到内存的方式。

  • 显式加载指的是在代码中通过调用ClassLoader加载class对象,如直接使用Class.forName(name)或者this.getClass().getClassLoader().loadClass()加载class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。
命名空间
  1. 何为类的唯一性?
    对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一性。每一个类加载器,都拥有一个独立的类命名空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即时这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

  2. 命名空间

    • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有的父加载器所加载的类组成。
    • 在同一个命名空间中,不会出现类的完整名字(包括类的报名)相同的两个类。
    • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。

    在大型应用中,我们往往借助这一特性,来运行同一个类的不同版本。

类加载的基本特征

通常类加载机制有三个基本特征:

  • 双亲委派模型。
  • 可见性,子类加载器可以访问父类加载器加载的类型,但是反过来是不允许的。不然,因为缺少必要的隔离,我们就没办法利用类加载器去实现容器的逻辑。
  • 单一性,由于父类加载器的类型对于子类加载器是可见的,所以父类加载器中加载的类型,就不会在子类加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为相互不可见。
类加载器分类

JVM支持两种类型的类加载器,分别为引导类加载器(Bootstrap ClassLoader)和自定义类加载器(User-Defined ClassLoader)。
从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定义的一类类加载器,但是Java虚拟机规范却没有这么定义,而是讲所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器的结构主要是如下情况:

引导类加载器

启动类加载器(引导类加载器,Bootstrap ClassLoader)

  • 这个类加载使用C/C++语言实现的,嵌套在JVM内部。
  • 它用来加载Java的核心库(JAVA_HOME/jre/lib/rt.jar或者sun.boot.class.path路径下的内容)。用于提供JVM自身需要的类。
  • 并不继承自java.lang.ClassLoader,没有父加载器。
  • 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类。
  • 是扩展类加载器和应用程序类加载器的父类加载器

参数:-XX:+TraceClassLoading 打印 引导类加载器加载的类

扩展类加载器(Extension ClassLoader)
  • Java语言编写,由sun.misc.Launcher$ExtClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java.ext.dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/lib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。
应用程序类加载器(系统类加载器,AppClassLoader)
  • java语言编写,由sun.misc.Launcher$AppClassLoader实现
  • 继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java.lang.path指定路径下的类库
  • 应用程序中的类加载器默认是系统类加载器
  • 它是用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader()方法可以获取到该类加载器
用户自定义类加载器
  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是Java开发者可以自定义类加载器来实现类库的动态加载,加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制,这方法的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。
  • 自定义类加载器通常需要继承于ClassLoader。
  • 自定义类加载器能够实现应用隔离。通过定义不同的类加载器,实现加载不同的组件模块,从而实现应用的隔离。
ClassLoader源码解析 ClassLoader类的主要方法

抽象类ClassLoader的主要方法:(内部没有抽象方法)

  • public final ClassLoader getParent() 返回该类加载器的超类加载器

  • public Class loadClass(String name) throws ClassNotFoundException 加载名称为name的累,返回结果为java.lang.Class类的实例。如果找不到类,则返回ClassNotFoundException异常。该方法种的逻辑就是双亲委派模式的实现。

  • protected Class findClass(String name) throws ClassNotFoundException 查找二进制名称name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委派机制,该方法会在检查完父类加载器之后被loadClass()方法调用。

    在JDK1.2之前,在自定义类加载时,总会去继承ClassLoader类并重写loadClass方法,从而实现自定义的类加载。但是在JDK1.2之后已不再建议用户去覆盖loadClass()方法,而是建议把自定义的类加载逻辑卸载findClass()方法中,从前面的分析可知,findClass()方法是在loadClass()方法中被调用的,当loadClass()方法中父类加载器加载类失败后,则会调用自己的findClass()方法来完成类的加载,这样就可以保证自定义的类加载器也符合双亲委派模式。需要注意的是ClassLoader类中并没有实现findClass()方法的具体代码逻辑,取而代之的是抛出ClassNotFoundException异常,同时应该知道的是findClass方法通常是和defineClass方法一起使用的。一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象

  • protected final Class defineClass(String name, byte[] b, int off, int len) 根据给定的字节数组b转换为Class的实例,off和len参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。

    defineClass()方法是用来将byte字节流解析成JVM能够识别的Class对象(ClassLoader中已实现该方法逻辑),通过这个方法不仅能够将class文件实例化成class对象,也可以通过其他方法实例化class对象,如通过网络接收一个类的字节码,然后转换成byte字节流创建对应的Class对象 。defineClass()方法通常与findClass()方法一起使用,一般情况下,在自定义类加载器时,会直接覆盖ClassLoader的findClass()方法并编写加载规则,取得要加载类的字节码后转换成流,然后调用defineClass()方法生成类的Class对象

    • protected find void resolveClass(Class c) 链接指定的一个Java类。使用该方法可以使类的Class对象创建完成的同时也被解析。前面我们说过链接阶段主要对字节码进行验证(验证阶段),为类变量分配内存并设置初始值(准备阶段)同时将字节码文件中的符号引用转换为直接引用(解析阶段)。
    • protected find Class findLoadedClass(String name) 查找名称为name的已经加载过的类,返回结果为java.lang.Class类的实例。这个方法是final的方法,无法被修改。
    • private final ClassLoader parent; 它也是一个ClassLoader实例,这个字段所表示的ClassLoader也成为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可能会将某些请求交予自己的双亲处理。
SecureClassLoader 与 URLClassLoader

SecureClassLoader扩展了ClassLoader,新增了几个与使用相关的代码源(对代码源的位置及其证书的验证)和权限定义类验证(主要指对class源码的访问权限)的方法,一般我们不会直接跟这个类打交道,更多的是与它的子类URLClassLoader有所关联。

ClassLoader是一个抽象类,很多方法是空的没有实现,比如findClass(),findResource()等。而URLClassLoader这个实现类为这些方法提供了具体的实现。并新增了URLClassPath类协助取得Class字节码流等功能。在编写自定义类加载器时,如果没有太过于复杂的需求,可以直接继承URLClassLoader类,这样就可以避免自己去编写findClass()方法及其获取字节码流的方式,使自定义类加载器编写更加简洁。

ExtClassLoader 与 AppClassLoader

扩展类加载器ExtClassLoader 和 系统类加载器AppClassLoader,这两个类都继承自URLClassLoader,是sun.misc.Launcher的静态内部类。sun.misc.Launcher主要被系统用于启动主应用程序,ExtClassLoader和AppClassLoader都是由sun.misc.Launcher创建的。

Class.forName() 与 ClassLoader.loadClass()
  • Class.forName() 是一个静态方法,最常用的是Class.forName(String className);根据传入的类的全限定名返回一个Class对象。该方法将Class文件加载到内存的同时,会执行类的初始化。
  • ClassLoader.loadClass() 是一个实例方法,需要一个ClassLoader对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器。
双亲委派机制 破坏双亲委派机制的三个常识
  1. 双亲委派机制是在jdk1.2被引入的,显然jdk1.1的时候类的加载是不满足双亲委派机制的。如果在jdk1.1时代自定义的类加载器,运行在jdk1.2以上的版本的jdk上,显然这种代码是破坏双亲委派机制的。

  2. 双亲委派机制的缺点是,父类加载器无法使用子类加载器实现的功能。为了解决该缺点,可以使用ContextClassLoader,由父类加载器,委托ContextClassLoader去调用子类加载器实现的功能。显然也是违背了双亲委派机制。

  3. 双亲委派模型第三次被破坏是由于用户对程序动态性的追求导致的。如:代码热替换,模块热部署等。在OSGi环境下,类加载器不再是双亲委派模型的树状结构,而是进一步发展为更加复杂的网状结构。当收到类加载请求时,OSGi将按照下面的顺序进行类搜索:

    1. 将以java.*开头的类,委派给父类加载器加载
    2. 否则,将委派列表名单的类,交给父类加载器加载。
    3. 否则,将import列表的类,委派给Export这个类的Bundle的累加载器加载
    4. 否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
    5. 否则,查找类是否在自己的Fragment Bundle中,如果在,委派给Fragment Bundle的类加载器加载。
    6. 否则,查找Dynamic import列表的Bundle,委派给对应的Bundle的累加载器加载。
    7. 否则,类查找失败。

    只有开头两点仍然符合双亲委派模型的原则,其余的类查找都是在 平级的类加载器中进行的。

沙箱安全机制
  • 保证程序安全
  • 保护Java原生的JDK代码

Java安全模型的核心就是Java沙箱。沙箱是一个限制程序运行的环境。沙箱机制就是将Java代码限定在虚拟机(JVM)特定的运行范围中,并且严格限制代码对本地系统资源访问。通过这样的措施来保证对代码的有限隔离,防止对本地系统造成破坏。

沙箱主要限制系统资源访问,那系统资源包括什么?CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也是不一样的。

自定义类的加载器
  1. 为什么要自定义类的加载器?
    • 隔离加载类:在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。
    • 修改类加载的方式:类的加载模型并非强制,除Bootstrap外,其他的加载并非一定要引入,或者根据实际情况在某个时间点按需进行动态加载。
    • 扩展加载源:比如从数据库,网络,甚至是电机机顶盒进行加载。
    • 防止源码泄漏:Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。
自定义类加载器的实现方式

用户通过定制自己的类加载器,这样可以重新定义类的加载规则,以便实现一些自定义的处理逻辑。

  1. 实现方式
    • Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
    • 在自定义ClassLoader的子类时候,我们常见的会有两种做法:重写loadClass()方法(不推荐)和重写findClass()方法
  2. 对比
    这两种方式本质上差不多,比较loadClass()也会调用findClass(),但是从逻辑上讲,我们最好不要直接修改loadClass的内部调用逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。
    • loadClass()这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadClass()方法的过程中必须写双亲委派的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
    • 当编写好自定义加载器后,便可以在程序中调用loadClass()方法来实现类加载的 *** 作。
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;


public class MyClassLoader extends ClassLoader {
    
    private String byteCodePath;
    
    public MyClassLoader(String byteCodePath) {
        this.byteCodePath = byteCodePath;
    }
    
    public MyClassLoader(ClassLoader parent, String byteCodePath) {
        super(parent);
        this.byteCodePath = byteCodePath;
    }

    @Override
    protected Class findClass(String name) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            String fileName = byteCodePath + name + ".class";
            bis = new BufferedInputStream(new FileInputStream(fileName));
            baos = new ByteArrayOutputStream();

            int len;
            byte[] data = new byte[1024];
            while ((len = bis.read(data)) != -1) {
                baos.write(data, 0, len);
            }
            byte[] byteCodes = baos.toByteArray();
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length); // 加载类,并生成java.lang.Class类的实例			
            return clazz;
        }catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if (bis != null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5660033.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存