第四章:再谈类的加载器

概述

ClassLoader的作用

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

image-20221024100422140

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

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

  • 显式加载指的是在代码中通过调用 ClassLoader 加载class对象, 如直接使用Class . forName(name)或this. getClass(). getClassLoader() . loadClass( )加载class对象。
  • 隐式加载则是不直接在代码中调用ClassLoader的方法加载class对象,而是通过虚拟机自动加载到内存中,如在加载某个类的class文件时,该类的class文件中引用了另外一个类的对象,此时额外引用的类将通过JVM自动加载到内存中。

在日常开发以上两种方式一般会混合使用。

public class UserTest {
    public static void main(String[] args) {
        User user = new User(); //隐式加载

        try {
            Class clazz = Class.forName("com.atguigu.java.User"); //显式加载
            ClassLoader.getSystemClassLoader().loadClass("com.atguigu.java.User");//显式加载
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

    }
}

类加载器的必要性

一般情况下,Java开发人员并不需要在程序中显式地使用类加载器,但是了解类加载器的加载机制却显得至关重要。从以下几个方面说:

  • 避免在开发中遇到 java. lang . ClassNotFoundException异常或 java . lang . NoClassDefFoundError异常时,手足无措。只有了解类加载器的加载机制才能够在出现异常的时候快速地根据错误异常日志定位问题和解决问题
  • 需要支持类的动态加载或需要对编译后的字节码文件进行加解密操作时,就需要与类加载器打交道了。
  • 开发人员可以在程序中编写自定义类加载器来重新定义类的加载规则,以便实现–些自定义的处理逻辑。

命名空间与类的唯一性

何为类的唯一性?

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确认其在Java虚拟机中的唯一一性。每一一个类加载器,都拥有一个独立的类名称空间:比较两个类是否相等,只有在这两个类是由同一个类加载器加载的前提下才有意义。否则,即使这两个类源自同一个Class文件,被同一个虚拟机加载,只要加载他们的类加载器不同,那这两个类就必定不相等

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

  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类

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

同一个类,被不同类加载器加载,这俩个类就不是相同的类

类加载器的分类

JVM支持两种类型的类加载器,分别为引导类加载器( Bootstrap ClassLoader) 和自定义类加载器(User-Defined ClassLoader)

从概念上来讲,自定义类加载器一般指的是程序中由开发人员自定 义的一类类加载器,但是Java虛拟机规范却没有这么定义,而是将所有派生于抽象类ClassLoader的类加载器都划分为自定义类加载器。无论类加载器的类型如何划分,在程序中我们最常见的类加载器结构主要是如下情况:

image-20221024103527477

除了顶层的启动类加载器外,其余的类加载器都应当有自己的“父类”加载器。
不同类加载器看似是继承(Inheritance) 关系,实际上是包含关系。在下层加载器中,包含着上层加载器的引用

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

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

扩展类加载器(Extension ClassLoader)

  • Java语言编写,由sun. misc.Launcher$ExtClassLoader实现。继承于ClassLoader类
  • 父类加载器为启动类加载器
  • 从java. ext . dirs系统属性所指定的目录中加载类库,或从JDK的安装目录的jre/1ib/ext子目录下加载类库。如果用户创建的JAR放在此目录下,也会自动由扩展类加载器加载。

应用程序类加载器(紧统类加载器,AppClassLoader)

  • java语言编写,由sun . misc. Launcher$AppClassLoader实现继承于ClassLoader类
  • 父类加载器为扩展类加载器
  • 它负责加载环境变量classpath或系统属性java. class. path指定路径下的类库应用程序中的类加载器默认是系统类加载器。它是用户自定义类加载器的默认父加载器
  • 通过ClassLoader的getSystemClassLoader( )方法可以获取到该类加载器

用户自定义类加载器

  • 在Java的日常应用程序开发中,类的加载几乎是由上述3种类加载器相互配合执行的。在必要时,我们还可以自定义类加载器,来定制类的加载方式。
  • 体现Java语言强大生命力和巨大魅力的关键因素之一便是,Java开发者可以自定义类加载器来实现类库的动态加载加载源可以是本地的JAR包,也可以是网络上的远程资源。
  • 通过类加载器可以实现非常绝妙的插件机制,这方面的实际应用案例举不胜举。例如,著名的OSGI组件框架,再如Eclipse的插件机制。类加载器为应用程序提供了一种动态增加新功能的机制,这种机制无须重新打包发布应用程序就能实现。

同时,自定义加载器能够实现应用隔离,例如Tomcat, Spring等中 间件和组件框架都在内部实现了自定义的加载器,并通过自定义加载器隔离不同的组件模块。这种机制比C/C++程序要好太多,想不修改C/C++程序就能为其新增功能,几乎是不可能的,仅仅一个兼容性便能阻挡住所有美好的设想。自定义类加载器通常需要继承于ClassLoader.

测试不同的类加载器

每个Class对象都会包含一个定义它的ClassLoader的一个引用。

获取ClassLoader的途径

//获得当前类的ClassLoader
clazz . getClassLoader()
//获得当前线程上下文的ClassLoader
Thread . current Thread( ) . getContextClassLoader()
//获得系统的ClassLoader
ClassLoader . getSystemClassLoader( )

说明:
站在程序的角度看,引导类加载器与另外两种类加载器(系统类加载器和扩展类加载器)并不是同一个层次意义上的加
载器,引导类加载器是使用C++语言编写而成的,而另外两种类加载器则是使用Java语言编写而成的。由于引导类加载
器压根儿就不是一一个Java类,因此在Java程序中只能打印出空值

数组类的Class对象,不是由类加载器去创建的,而是在Java运行期JVM根据需要自动创建的。对于数组类的类加载器来说,是通过Class . getClassLoader( )返回的,与数组当中元素类型的类加载器是一样的;如果数组当中的元素类是基本数据类型,数组类是没有类加载器的。

代码测试:

public class ClassLoaderTest1 {
    public static void main(String[] args) {
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader(); // 系统类加载器/应用程序加载器
        System.out.println(systemClassLoader);

        ClassLoader extClassLoader = systemClassLoader.getParent(); // 扩展类加载器
        System.out.println(extClassLoader);

        ClassLoader bootStrapClassLoader = extClassLoader.getParent(); // null 引导类加载器
        System.out.println(bootStrapClassLoader);

        ClassLoader classLoader = ClassLoaderTest1.class.getClassLoader(); // 自定义的类使用系统类加载器
        System.out.println(classLoader);

        // 数组类的加载器和数组元素类型使用的加载器保持一致。
        // String类使用引导类加载器,因此String数组也使用引导类加载器
        String[] arr =  new String[10];
        ClassLoader arrClassLoader = arr.getClass().getClassLoader();
        System.out.println(arrClassLoader); // null
    }
}

ClassLoader 源码剖析

image-20221024182502618

ClassLoader 中 主要的方法

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

  • public final ClassLoader getParent()
    • 返回该类加载器的超类加载器
  • public Class<?> loadClass(String name) throws ClassNotFoundException
    • 加载名称为name的类,返回结果为java. lang. Class类的实例。如果找不到类,则返回ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。
  • protected Class<?> f indClass(String name) throws ClassNotFoundException
    • 查找二进制名称为name的类,返回结果为java . lang. Class类的实例。这是一个受保护的方法,JVM鼓 励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用
  • protected final Class<?> defineClass(String name, byte[] b, int off, int len)
    • 根据给定的字节数组b转换为Class的实例,off和1en参数表示实际Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
  • protected final void resolveClass(Class<?> c)
    • 链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
  • protected final Class<?> findLoadedClass(String name )
    • 查找名称为name的已经被加载过的类,返回结果为java . lang. Class类的实例。这个方法是final方法,无法被修改。
  • private final ClassLoader parent;
    • 它也是一个ClassLoader的实例,这个字段所表示的ClassLoader也称为这个ClassLoader的双亲。在类加载的过程中,ClassLoader可 能会将某些请求交予自己的双亲 处理。

CLass.forName 和 ClassLoader.loadClass 的区别

  • Class. forName(): 是一个静态方法,最常用的是Class . forName(String className);根据传入的类的全限定名返回一个Class对象。该方法在将Class 文件加载到内存的同时,会执行类的初始化。如:Class . forName(" com. atguigu. java. HelloWorld");

  • ClassLoader. loadClass(): 这是一个实例方法,需要一个ClassLoader 对象来调用该方法。该方法将Class文件加载到内存时,并不会执行类的初始化,直到这个类第一次使用时才进行初始化。该方法因为需要得到一个ClassLoader对象,所以可以根据需要指定使用哪个类加载器.如: ClassLoader c1=… ; cl. loadClass(“com. atguigu. java. HelloWorld”);

双亲委派机制

定义
如果一个类加载器在接到加载类的请求时,它首先不会自己尝试去加载这个类,而是把这个请求任务委托给父类加载器去完成,依次递归,如果父类加载器可以完成类加载任务,就成功返回。只有父类加载器无法完成此加载任务时,才自己去加载。

本质
规定了类加载的顺序是:引导类加载器先加载,若加载不到,由扩展类加载器加载,若还加载不到,才会由系统类加载器或自定义的类加载器进行加载。

双亲委派机制优势

  • 避免类的重复加载,确保一-个类的全局唯一 性
  • Java类随着它的类加载器一起具备 了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 保护程序安全,防止核心API被随意篡改

代码支持
双亲委派机制在java. lang. ClassLoader. loadClass(String, boolean)接口中体现。该接口的逻辑如下:

  1. 先在当前加载器的缓存中查找有无目标类,如果有,直接返回。
  2. 判断当前加载器的父加载器是否为空,如果不为空,则调用parent . loadClass(name, false)接 口进行加载。
  3. 反之,如果当前加载器的父类加载器为空,则调用findBootstrapClassOrNu1l(name)接口,让引导类加载器进行加载。
  4. 如果通过以上3条路径都没能成功加载,则调用findClass(name)接口进行加载。 该接口最终会调用java. lang. ClassLoader接口的defineClass系列的native接口加载目标Java类。

双亲委派的模型就隐藏在这第2和第3步中。

image-20221024190410917

双亲委托模式的弊端

检查类是否加载的委托过程是单向的,这个方式虽然从结构上说比较清晰,使各个ClassLoader的职责非常明确,但是同时会带来一个问题,即顶层的ClassLoader 无法访问底层的ClassLoader所加载的类 。通常情况下,启动类加载器中的类为系统核心类,包括一些重要的系统接口,而在应用类加载器中,为应用类。按照这种模式,应用类访问系统类自然是没有问题,但是系统类访问应用类就会出现问题。比如在系统类中提供了一个接口,该接口需要在应用类中得以实现,该接口还绑定一个工厂方法,用于创建该接口的实例,而接口和工厂方法都在启动类加载器中。这时,就会出现该工厂方法无法创建由应用类加载器加载的应用实例的问题。

结论

由于Java虚拟机规范并没有明确要求类加载器的加载机制一定要使用双亲委派模型,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有-一定区别,当缺省的类加载器接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。

沙箱安全机制

沙箱安全机制

  • 保证程序安全
  • 保护Java原生的JDK代码

Java安全模型的核心就是Java沙箱(sandbox)。什么是沙箱?

​ 沙箱是一个限制程序运行的环境。

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

沙箱主要限制系统资源访问,那系统资源包括什么?

​ CPU、内存、文件系统、网络。不同级别的沙箱对这些资源访问的限制也可以不一一样。

所有的Java程序运行都可以指定沙箱,可以定制安全策略。

JDK1.0时期

在Java中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的Java实现中,安全依赖于沙箱(Sandbox)机制。如下图所示JDK1.0安全模型

image-20221024193252053

JDK1.1时期

JDK1.0中如此严格的安全机制也给程序的功能扩展带来障碍,比如当用户希望远程代码访问本地系统的文件时候,就无法实现。
因此在后续的Java1.1版本中,针对安全机制做了改进,增加了安全策略。允许用户指定代码对本地资源的访问权限。
如下图所示JDK1.1安全模型

image-20221024193318058

JDK1.2时期

在Java1.2版本中,再次改进了安全机制,增加了代码签名。不论本地代码或是远程代码,都会按照用户的安全策略设定,由类加载器加载到虚拟机中权限不同的运行空间,来实现差异化的代码执行权限控制。如下图所示JDK1.2安全模型

image-20221024193356718

JDK1.6时期

当前最新的安全机制实现,则引入了域(Domain)的概念。
虚拟机会把所有代码加载到不同的系统域和应用域。系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域(Protected Domain) ,对应不一样的权限(Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示,最新的安全模型(jdk1.6)

image-20221024193437685

自定义类加载器

为什么要自定义类加载器?

  • 隔离加载类

    • 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。比如:阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。再比如: Tomcat这 类Web应用服务器,内部自定义了好几种类加载器,用于隔离同一个Web应用服务器上的不同应用程序。( 类的仲裁–>类冲突)
  • 修改类加载的方式

    • 类的加载模型并非强制,除Bootstrap外, 其他的加载并非一定要引入, 或者根据实际情况在某个时间点进行按需进行动态加载
  • 扩展加载源

    • 比如从数据库、网络、甚至是电视机机顶盒进行加载
  • 防止源码泄漏

    • Java代码容易被编译和篡改,可以进行编译加密。那么类加载也需要自定义,还原加密的字节码。

常见的场景

  • 实现类似进程内隔离,类加载器实际上用作不同的命名空间,以提供类似容器、模块化的效果。例如,两个模块依赖于某个类库的不同版本,如果分别被不同的容器加载,就可以互不干扰。这个方面的集大成者是Java EE和OSGI、JPMS等框架。
  • 应用需要从不同的数据源获取类定义信息,例如网络数据源,而不是本地文件系统。或者是需要自己操纵字节码,动态修改或者生成类型。

实现方式

java提供了抽象类java. lang. ClassLoader, 所有用户自定义的类加载器都应该继承ClassLoader类
在自定义ClassLoader 的子类时候,我们常见的会有两种做法:

  • 方式一:重写1oadClass()方法
  • 方式二:重写findClass()方法 ——> 推荐

对比
这两种方法本质上差不多,毕竟 loadClass()也 会调用findClass(),但是从逻辑上讲我们最好不要直接修改loadClass()的内部逻辑。建议的做法是只在findClass()里重写自定义类的加载方法,根据参数指定类的名字,返回对应的Class对象的引用。

loadClass( )这个方法是实现双亲委派模型逻辑的地方,擅自修改这个方法会导致模型被破坏,容易造成问题。因此我们最好是在双亲委派模型框架内进行小范围的改动,不破坏原有的稳定结构。同时,也避免了自己重写loadClass( )方法的过程中必须写双亲委托的重复代码,从代码的复用性来看,不直接修改这个方法始终是比较好的选择。
当编写好自定义类加载器后,便可以在程序中调用loadClass() 方法来实现类加载操作。

说明

  • 其父类加载器是系统类加载器
  • JVM中的所有类加载都会使用java. lang.ClassLoader . loadClass(String)接口(自定义类加载器并重写

代码实现:

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 className) throws ClassNotFoundException {
        BufferedInputStream bis = null;
        ByteArrayOutputStream baos = null;
        try {
            //获取字节码文件的完整路径
            String fileName = byteCodePath + className + ".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();
            //调用defineClass(),将字节数组的数据转换为Class的实例。
            Class clazz = defineClass(null, byteCodes, 0, byteCodes.length);
            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;


    }
}

测试:

public class MyClassLoaderTest {
    public static void main(String[] args) throws ClassNotFoundException {
        MyClassLoader loader = new MyClassLoader("C:\\19_JVM\\code\\JvmDemo2\\out\\production\\JvmDemo2\\chapter04\\");
        Class<?> aClass = loader.findClass("ClassLoaderTest1");
        
        System.out.println(aClass.getClassLoader().getClass().getName()); // chapter04.MyClassLoader
        
        System.out.println(aClass.getClassLoader().getParent().getClass().getName()); // sun.misc.Launcher$AppClassLoader
    }
}

自定义类加载器的父类加载器为 系统类加载器/应用程序类加载器

JDK9 关于类加载器的改动

为了保证兼容性,JDK 9没有从根本上改变三层类加载器架构和双亲委派模型,但为了模块化系统的顺利运行,仍然发生了一些值得被注意的变动。

  1. 扩展机制被移除,扩展类加载器由于向后兼容性的原因被保留,不过被重命名为平台类加载器(platform class loader)。可以通过ClassLoader的新方法==getPlatformClassLoader( )==来获取。

JDK 9时基于模块化进行构建(原来的rt.jar和tools.jar 被拆分成数十个JMOD文件),其中的Java类库就已天然地满足了可扩展的需求,那自然无须再保留<JAVA_ HOME>\lib\ext 目录,此前使用这个目录或者java.ext.dirs 系统变量来扩展JDK功能的机制已经没有继续存在的价值了。

  1. 平台类加载器和应用程序类加载器都不再继承自 java . net . URLClassLoader. 现在启动类加载器、平台类加载器、应用程序类加载器全都继承于 jdk . internal . loader . BuiltinClassLoader.

image-20221024230847875

如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader 类的特定方法,那代码很可能会在JDK 9及更
高版本的JDK中崩溃。

  1. 在Java 9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。

  2. 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null, 而不会得到BootClassLoader实例。

  3. 类加载的委派关系也发生了变动。

当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某–个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

inClassLoader==.

[外链图片转存中…(img-TxFt9ZOZ-1669127533599)]

如果有程序直接依赖了这种继承关系,或者依赖了URLClassLoader 类的特定方法,那代码很可能会在JDK 9及更
高版本的JDK中崩溃。

  1. 在Java 9中,类加载器有了名称。该名称在构造方法中指定,可以通过getName()方法来获取。平台类加载器的名称是platform,应用类加载器的名称是app。类加载器的名称在调试与类加载器相关的问题时会非常有用。

  2. 启动类加载器现在是在jvm内部和java类库共同协作实现的类加载器(以前是C++实现),但为了与之前代码兼容,在获取启动类加载器的场景中仍然会返回null, 而不会得到BootClassLoader实例。

  3. 类加载的委派关系也发生了变动。

当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某–个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责那个模块的加载器完成加载。

image-20221024230924007



各位彭于晏,如有收获点个赞不过分吧…✌✌✌

Alt

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鲨瓜2号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值