JVM | 基于类加载的一次完全实践

这篇具有很好参考价值的文章主要介绍了JVM | 基于类加载的一次完全实践。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

引言

我在上篇文章:JVM | 类加载是怎么工作的 中为你介绍了Java的类加载器及其工作原理。我们简单回顾下:我用一个易于理解的类比带你逐步理解了类加载的流程和主要角色:引导类加载器扩展类加载器应用类加载器。并带你深入了解了这些“建筑工人”如何从底层工作,搬运原材料(类)并将其完整地构建在Java虚拟机(JVM)的“建筑工地”上。然后,我们跟随一个具体的Building类,亲眼目睹了其在JVM中的生命周期。我在文章末尾留了几个问题,你还记得吗?

本篇文章,我将带你了解自定义类加载器的创建和使用。我们还将探索Java的SPI机制,了解它如何利用类加载器实现服务的动态发现和加载。接着,我们再来看下Tomcat的类加载机制,尤其是它的热部署多版本共存的实现,了解类加载机制在现实世界中的高级应用。


自定义类加载器的创建和使用

当我们的类涉及到一些安全的操作,或者我们想从网络或者其它地方加载类。这种情况,我们就会创建自定义的类加载器,重写findClass方法来完成这个特殊的加载逻辑。

沿用上篇文章的例子:假如工地来活了,要求建造一个复杂的建筑物,这个建筑物不仅包括了普通的房间(普通的类),还包括了一些特殊设计的房间(特殊的类)。
在这个情况下,你可能会需要一位专门的工人来处理这些特殊的房间。这位工人需要有特殊的技能和工具,才能按照设计图纸(类的字节码)正确地建造出房间。
接下来,我们来看下类加载器怎么创建与使用的。


创建类加载器

我们来实现一个类加载器,代码如下:


public class CustomClassLoader extends ClassLoader {
    private String classPath;
    public CustomClassLoader(String classPath) {
        this.classPath = classPath;
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 先检查类是否已经被加载
        Class<?> cls = findLoadedClass(name);
        if (cls != null) {
            return cls;
        }

        try {
            // 如果类还未被加载,尝试使用父类加载器加载(不破坏双亲委派机制)
            cls = getParent().loadClass(name);
        } catch (ClassNotFoundException e) {
            // 父类加载器无法加载该类,那么就调用 findClass 尝试自己加载
            cls = findClass(name);
        }

        return cls;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 你的类加载逻辑...
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }

    private byte[] loadClassData(String className) {
        String fileName = getFileName(className);
        try {
            InputStream is = new FileInputStream(fileName);
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            int bufferSize = 4096;
            byte[] buffer = new byte[bufferSize];
            int bytesNumRead;
            while ((bytesNumRead = is.read(buffer)) != -1) {
                baos.write(buffer, 0, bytesNumRead);
            }
            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private String getFileName(String name) {
        int index = name.lastIndexOf('.');
        // 如果没有找到'.'则直接在classPath中查找
        if (index == -1) { 
            return classPath + name + ".class";
        } else {
            return classPath + name.substring(index + 1) + ".class";
        }
    }
}

上面的类加载器CustomClassLoader 通过构造的方式传入文件路径。当我们要加载类时,它会调用loadClass方法从我们定义的类路径下读取字节流。好,
接下来,我们来使用我们自己定义的类加载器。

使用类加载器

代码如下:

// 把上篇文章的Building类放在桌面
CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\xxx\\Desktop\\");

try {
    // 使用自定义类加载器加载 Building 类,若你的包名不叫这个,请更换。
    Class<?> cls = Class.forName("org.kfaino.jvm.Building", true, customClassLoader);

    // 创建类的实例
    cls.newInstance();

    System.out.println("实例名:" + cls.getName() + " 被加载器:" + cls.getClassLoader() + "创建");

} catch (Exception e) {
    e.printStackTrace();
}

执行!

Connected to the target VM, address: '127.0.0.1:4706', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:sun.misc.Launcher$AppClassLoader@3f3fdda9创建

Disconnected from the target VM, address: '127.0.0.1:4706', transport: 'socket'
Process finished with exit code 0

类被AppClassLoader加载了,为啥? 原因是我的项目中有Building类, 这个类可以被应用类加载器加载,因此就轮不到·CustomClassLoader 加载了。我们把项目内的Building先改名一下。然后执行!

Connected to the target VM, address: '127.0.0.1:5032', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:org.kfaino.jvm.CustomClassLoader@6379b5ed创建
Disconnected from the target VM, address: '127.0.0.1:5032', transport: 'socket'

Process finished with exit code 0

这次,我们的类成功被CustomClassLoader加载,并且加载的是我们桌面上的字节码文件。


使用Java自带的类加载器工具类

当然,如果你想要从外部加载字节码文件,可以不必这么繁琐。JDK提供了一个功能更强大的URLClassLoader。我们一起来看下它怎么用:

        // 把Building放在桌面
        URL[] urls = new URL[] {new URL("file:C:\\Users\\xxx\\Desktop\\")};
        URLClassLoader customClassLoader = new URLClassLoader(urls);

        try {
            // 使用自定义类加载器加载 Building 类
            Class<?> cls = Class.forName("org.kfaino.jvm.Building", true, customClassLoader);

            // 创建类的实例
            cls.newInstance();

            System.out.println("实例名:" + cls.getName() + " 被加载器:" + cls.getClassLoader() + "创建");

        } catch (Exception e) {
            e.printStackTrace();
        }

我们执行看下:

Connected to the target VM, address: '127.0.0.1:9734', transport: 'socket'
建筑蓝图已被创建!
实例名:org.kfaino.jvm.Building 被加载器:java.net.URLClassLoader@28634811创建
Disconnected from the target VM, address: '127.0.0.1:9734', transport: 'socket'

Process finished with exit code 0

没有问题,本地的字节码文件Building 被成功读取。因此,当你有从外部读取字节码文件的需求,可以试试用JDK自带的·URLClassLoader类加载器。同时,它还提供了其它更强大的功能。

从网络URL加载类和资源

若你想从网络加载字节码文件,你可以这么做:

URL url = new URL("http://www.github.com/xxx/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

更多的URL加载类和资源

细心的你肯定发现URLClassLoader的构造入参是数组类型,也就意味着可以传入多个URL,具体用法如下:

URL url1 = new URL("http://www.github.com/xxx1/");
URL url2 = new URL("http://www.github.com/xxx2/");
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url1, url2});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

从JAR文件加载类和资源

它可以从完整的jar包中读取字节码文件,代码如下:

File file = new File("/xxx/jarfile.jar");
URL url = file.toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
Class<?> clazz = urlClassLoader.loadClass("包名.类名");

加载外部配置文件

它可以从外部读取配置文件,代码如下:

File file = new File("/xxx/resources/");
URL url = file.toURI().toURL();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
URL resourceUrl = urlClassLoader.getResource("xxx.properties");
InputStream stream = resourceUrl.openStream();
Properties properties = new Properties();
properties.load(stream);

自定义类加载器的注意事项

类加载器在类的加载过程中起着至关重要的作用。因此,在使用时,我们必须倍加警惕。接下来,让我们来看下哪些需要注意的问题:

内存泄漏

长期存活的类加载器持有类的引用就会导致内存泄露。为了避免这个问题,我建议你在关键代码处适当使用如下代码,让旧的类加载器和类实例进行解绑:

// 释放对 ClassLoader 的引用,使其有可能被垃圾回收
classLoader = null;
System.gc();

当然,每个问题都需要我们针对性地分析。我这里只是提供可能导致内存泄漏的一个说法。实际上,引发内存泄漏的原因有很多,如果你在工作中遇到了这个问题,可以使用一些可视化分析工具来综合性的分析。

不要轻易破坏双亲委派机制

双亲委派模型是为了保证Java核心类库的安全性。当然,我们也可以选择破坏双亲委派模型,前提是,你已考虑好这些风险并规避。

在上述代码中,我们没有违背双亲委派模型的原则。回顾一下我们在之前文章中提到的双亲委派模型的概念:在类加载的过程中,我们首先会让父类加载器进行加载,只有在父类加载器无法加载的情况下,我们才会使用自定义的类加载器进行加载。顺便我把上篇缺少的自定义类加载器也补充进去,你可以看下:
JVM | 基于类加载的一次完全实践,JVM,jvm,开发语言,java,安全,后端

线程安全问题

如果我们在多线程中使用类加载器,可能会导致类被重复加载多次。除了会浪费资源外,还会导致我们一些静态初始化代码被执行多次,造成一些诡异的问题。我在上篇专栏中说到,解决线程安全的方式有多种。为了保险起见,你可以采用同步方案来解决它。


自定义类加载器使用场景

在上面的例子中,我为你展示如何从外部加载字节码文件。接下来,我们来看下还有哪些使用场景:

安全检查

安全,是软件工程中永恒的话题。为了防止第三方的潜在干扰,我们通常在获取外部文件的同时,做一些过滤的机制。你看代码:

public class SecurityCheckingClassLoader extends ClassLoader {
    private static final String CLASS_NAME_PREFIX = "Safe";

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
    	// 加上你想要的安全校验逻辑
        if (!name.startsWith(CLASS_NAME_PREFIX)) {
            throw new ClassNotFoundException("不安全的类: " + name);
        }

        return super.loadClass(name);
    }
}

上面,我为你举了一个简单的例子。我在加载类方法loadClass前校验类名的前缀,如果你不是Safe开头的类,我们就不予放行。

解密加密的类文件

网络环境充满不确定性,如果你选择从网络获取字节码文件,我建议你首先做好加密工作。既然是从外部获取文件,我们可以通过继承URLClassLoader来实现。代码如下:

import java.net.URL;
import java.net.URLClassLoader;

public class DecryptingURLClassLoader extends URLClassLoader {
    public DecryptingURLClassLoader(URL[] urls) {
        super(urls);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
    	// 获取字节码文件
        byte[] classData = loadClassData(name);
        if (classData == null) {
            throw new ClassNotFoundException();
        }
        byte[] decryptedClassData = decrypt(classData);
        return defineClass(name, decryptedClassData, 0, decryptedClassData.length);
    }

    private byte[] loadClassData(String className) {
    	// 从网络中获取字节码文件
    }

    private byte[] decrypt(byte[] classData) {
		// 解密字节码文件
    }
}

上面两个是工作中相对比较容易用到的两种场景,还有没有其它类加载器的优秀案例呢?


基于自定义类加载器的其它实现

我们从官网文档中得知,Tomcat会为每个web应用创建类加载器。图片如下:
JVM | 基于类加载的一次完全实践,JVM,jvm,开发语言,java,安全,后端
现在,我们来看下Tomcat用自定义加载器做了哪些事情。

Tomcat中的热部署

先来解释下什么是热部署? 热部署是指我们的应用在运行过程中,可以在不关闭应用的前提下更新应用。
假如你想开启热部署,你可以在context.xml里面设置reloadable="true”。限于篇幅有限,我在这里只是为你说明Tomcat热部署到底是怎么实现的,如果你感兴趣,建议您亲自动手实操。

热部署实现原理

Tomcat通过一个BackgroundProcessor 后台线程周期性的检查web 应用的 WEB-INF/classes 和 WEB-INF/lib 目录下的 class 文件或 jar 文件是否有变化。具体做法是比较文件的最后修改时间和上次记录的最后修改时间是否一致。如果有变化,就触发 web 应用的重载。
Tomcat重新加载一个web应用时,会创建一个新的WebappClassLoader实例,并使用这个新的类加载器来加载web应用的类。这样,新的类加载器就会加载最新版本的类,而旧的类加载器加载的旧版本的类会在它们不再被引用时被垃圾回收。这就是Tomcat的热部署。

Tomcat中的多版本共存

那什么是多版本共存? 我们在上面说到,每一个web应用都有自己独立的类加载器,这就意味着每一个web应用都有自己的类和库的命名空间。即使同一Tomcat实例中运行的多个web应用使用了同名的类和库,它们也不会相互干扰。
也就是说Tomcat的多版本共存关键也在于每个应用都有不同的类加载器。
限于篇幅有限,更多细节,建议你移步到官方文档,我在文末参考文献中为你贴出官方地址。


ServiceLoader和SPI

我们经常会听到许多Java框架包括Dubbo、Spring等都使用了SPI这个机制,SPI究竟是什么东西?ServiceLoader和我们今天讲的类加载器又有什么关系?

SPI(Service Provider Interface)是什么?

我从服务提供方和服务调用方两个视角来为你讲解:

服务提供方

对于服务提供方而言,它只需要根据接口实现对应方法。并且把配置文件放在META-INF/services/ 告知服务调用方。

服务调用方

服务调用方根据约定,使用ServiceLoader来遍历实现该约定的实现类。加载进内存中供服务提供方调用。

为了加深理解,我为你画了一张图:
JVM | 基于类加载的一次完全实践,JVM,jvm,开发语言,java,安全,后端

ServiceLoader和类加载器的关系

它和类加载器又有什么关系?我通过代码为你分析,你看:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

如果你不指定类加载器,load方法默认获取当前线程的类加载器去加载该类。它通过扫描META-INF/services/目录下的配置文件,找到服务接口的实现类的全限定名。有了全限定名就等于掌握了花名册,当我们遍历ServiceLoader时,服务加载器会通过类加载器将这些服务提供者实例化,这样我们就可以使用这些服务了。


文中重要部分解析

命名空间

我在上面Tomcat多版本共存中提到命名空间,什么是命名空间?每个类加载器实例其实就是一个命名空间。也就是说,在一个应用程序中允许一个类被多个类加载器实例加载,并且共存于应用程序中。暂停30秒,思考一下,这样会出现什么问题。
好,在回答这个问题之前,我为你展示一个代码:

try {
    URL[] urls = new URL[] {new URL("file:C:\\Users\\xxx\\Desktop\\")};
    URLClassLoader customClassLoader1 = new URLClassLoader(urls);
    URLClassLoader customClassLoader2 = new URLClassLoader(urls);

    Class cls1 = customClassLoader1.loadClass("org.kfaino.jvm.Building");
    Class cls2 = customClassLoader2.loadClass("org.kfaino.jvm.Building");

    Object obj1 = cls1.newInstance();
    Object obj2 = cls2.newInstance();

    System.out.println("obj1 class: " + obj1.getClass());
    System.out.println("obj2 class: " + obj2.getClass());

    System.out.println("obj1 class loader: " + obj1.getClass().getClassLoader());
    System.out.println("obj2 class loader: " + obj2.getClass().getClassLoader());
	// Building对象已经重写hashCode和equals方法
    System.out.println("obj1 equals obj2: " + obj1.equals(obj2));

} catch (Exception e) {
    e.printStackTrace();
}

在Building已经重写hashCode和equals方法的前提下,obj1 equals obj2: 会是true吗?我们看下结果:

建筑蓝图已被创建!
建筑蓝图已被创建!
obj1 class: class org.kfaino.webTemplate.jvm.Building
obj2 class: class org.kfaino.webTemplate.jvm.Building
obj1 class loader: java.net.URLClassLoader@da236ecf
obj2 class loader: java.net.URLClassLoader@8e02f9da
obj1 equals obj2: false

Process finished with exit code 0

结果是否定的,和我之前说的吻合。也就是说使用不同的类加载器,不同类加载器的对象(命名空间不同),在JVM中就是类型不一致的。

生产环境中的热部署

BackgroundProcessor 后台线程,需要周期性地检查(checkResources())文件的状态。处于对性能方面的考虑,在生产环境中,通常会关闭 Tomcat 的热部署功能。

SPI配置文件存放位置META-INF/services/可以更改吗?

查阅官方文档,我们可以知道SPI是JDK内置的一种服务提供发现机制。在SPI机制中,服务提供者的配置文件默认放在META-INF/services/目录下。这是Java SPI规范的一部分,无法更改。


总结

至此,本篇完结。我们来回顾下:首先,我带你创建并使用了类加载器完成从本地文件夹下加载自己的类。这些工作我们可以通过Java自带的类加载器来简化,我也为你演示其用法。当然,我们在使用自定义类加载器要格外注意,因为涉及到类初始化往往你会碰到一些不可预见的诡异BUG。然后,我为你介绍自定义类加载器场景的使用场景。顺便看一下Tomcat和Java是怎么用自定义类加载器的特性实现高级功能的。文章来源地址https://www.toymoban.com/news/detail-619147.html


常见面试题

如何自定义类加载器?
在什么情况下会需要自定义类加载器?
Tomcat的类加载器有什么特点?如何实现热部署和多版本共存?#### 什么是ServiceLoader和SPI,它们如何利用类加载器?
类加载器可能存在的问题有哪些?

参考文献

  1. Java Guide SPI机制详解
  2. class-loader-howto
  3. baeldung-java-classloaders
  4. Inside the Java Virtual Machine
  5. 老大难的 Java ClassLoader 再不理解就老了

到了这里,关于JVM | 基于类加载的一次完全实践的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • JVM类加载&双亲委派-JVM(二)

    上篇文章说了java类的加载,验证、准备、解析、初始化。类的初始化必须是类加载完才执行,所以类的构造方法初始化是在静态方法之后执行。 JVM类加载机制-JVM(一) 一、类加载和双亲委派机制 前面类加载主要通过类加载器实现,类加载器有这几种: 引导类加载器:负责

    2024年02月11日
    浏览(40)
  • JVM类加载过程

    类加载Class类型的文件主要三步: 加载-链接-初始化 。链接过程又可以分为三步: 验证-准备-解析 。 将类的字节码载入方法区中,内部采用 C++ 的 instanceKlass 描述 java 类,它的重要 field 有: _java_mirror 即 java 的类镜像,例如对 String 来说,就是 String.class,作用是把 klass 暴 露

    2023年04月23日
    浏览(39)
  • JVM类加载机制(六)

    类加载器(ClassLoader)是 Java 虚拟机(JVM)的重要组成部分,负责将类的字节码加载到内存中并转换为可执行的 Java 类。类加载器的主要任务是根据类的名称查找字节码文件并加载到内存中,然后进行验证、准备和解析等操作,最终生成可执行的类。 类加载器的分类: 启动类

    2024年02月06日
    浏览(51)
  • JVM类加载

    执行 javac -parameters -d . HellowWorld.java编译为 HelloWorld.class文件,根据 JVM 规范,类文件结构如下 原始代码如下: 字节码文件自己分析嫌慢,可以 执行指令javap -v filepath反编译命令 ,直接获取字节码指令更直观 问题:方法如何被执行的呢? ANS:原始代码编译成字节码文件-常量池载入运行

    2024年01月23日
    浏览(29)
  • 【JVM】类加载

    类加载是 运行时环境 的一个重要核心功能。 类加载的主要功能:把 .class 文件加载到内存中构建成为类对象。 什么时候会进行类加载呢 ? 不是说 java 程序一运行,就把所有的类都加载了,而是用到的时候再加载。(懒汉模式) 那什么时候算是用到了呢 ? 创建类的实例 调用

    2024年02月07日
    浏览(31)
  • 【JVM】类加载器

    类加载器(ClassLoader)是Java虚拟机提供给应用程序去实现获取类和接口字节码数据的技术。 类加载器分为两类: Java代码中实现的类加载器 JVM底层源码实现的类加载器 jdk8和8之后版本的类加载器的设计差别较大,jdk8及之前的版本中默认的类加载器有如下几种: JVM底层实现

    2024年02月07日
    浏览(35)
  • JVM:类加载机制

    Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。 一个类型从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期将会经历 加载、验证

    2024年01月17日
    浏览(87)
  • JVM类加载机制

    自己编写的Java代码,是如何在各种各样的操作系统上运行起来的? Java文件通过javac编译成class文件,这种中间码被称为字节码,然后由jvm加载字节码,运行时解释器将字节码解释为一行行机器码来执行,在程序运行期间,即时编译器能会针对热点代码将该部分字节码编译成机

    2024年02月10日
    浏览(37)
  • JVM 的类加载机制

    类加载就是把 .class 文件加载到内存中,得到 类对象 的过程。 类加载的 5 个过程 1 加载 找到 .class 文件,将文件内容读取到内存中。 2 验证 验证加载的这个文件是否合法的 .class文件,.class 文件有明确的数据格式。 3 准备 给类对象分配内存空间,定义成员变量。 未初始化的

    2024年02月15日
    浏览(42)
  • JVM类加载器

    类加载器虽然只用于实现类的加载动作 ,但它在Java程序中起到的作用却远超类加载阶段。 对于 任意一个类,都必须由加载它的类加载器和这个类本身一起共同确立其在Java虚拟机中的唯一性 ,每一个类加载器,都拥有一个独立的类名称空间。这句话可以表达得更通俗一些:

    2024年02月09日
    浏览(34)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包