深入理解Java ClassLoader及在 JavaAgent 中的应用

深入理解Java ClassLoader及在 JavaAgent 中的应用

转载自

背景

众所周知, Java 或者其他运行在 JVM(java 虚拟机)上面的程序都需要最终便以为字节码,然后被 JVM加载运行,那么这个加载到虚拟机的过程就是 classloader 类加载器所干的事情.直白一点,就是 通过一个类的全限定类名称来获取描述此类的二进制字节流 的过程.

双亲委派模型

说到 Java 的类加载器,必不可少的就是它的双亲委派模型,从 Java 虚拟机的角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader), 由 C++语言实现,是虚拟机自身的一部分.
  2. 其他的类加载器,都是由 Java 实现,在虚拟机的外部,并且全部继承自java.lang.ClassLoader

在 Java 内部,绝大部分的程序都会使用 Java 内部提供的默认加载器.

启动类加载器(Bootstrap ClassLoader)

负责将$JAVA_HOME/lib或者 -Xbootclasspath 参数指定路径下面的文件(按照文件名识别,如 rt.jar) 加载到虚拟机内存中.启动类加载器无法直接被 java 代码引用,如果需要把加载请求委派给启动类加载器,直接返回null即可.

扩展类加载器(Extension ClassLoader)

负责加载$JAVA_HOME/lib/ext 目录中的文件,或者java.ext.dirs 系统变量所指定的路径的类库.

应用程序类加载器(Application ClassLoader)

一般是系统的默认加载器,比如用 main 方法启动就是用此类加载器,也就是说如果没有自定义过类加载器,同时它也是getSystemClassLoader() 的返回值.

这几种类加载器的工作流程被抽象成一个模型,就是双亲委派模型.

www.zeeklog.com  - 深入理解Java ClassLoader及在 JavaAgent 中的应用

工作流程:

  1. 收到类加载的请求
  2. 首先不会自己尝试加载此类,而是委托给父类的加载器去完成.
  3. 如果父类加载器没有,继续寻找父类加载器.
  4. 搜索了一圈,发现都找不到,然后才是自己尝试加载此类.

这基本就是双亲委派模型.

但是这种模型只是一种推荐的方式,并不是强制的,你也可以尝试打破这种规则.
自所以这样约定,还是有一定的好处的, Java 类随着它的类加载器一起具备了一种带有优先级的层次关系.
比如自己定义了java.lang.Object 对象,那么按照上面的流程,他永远都是被启动类加载器加载的rt.jar 中的那个类,而不是自己定义的这个类,这样就保证了兄运行的稳定,否则,可能变得非常混乱,可以随意改写任何类.

在 JavaAgent 中的应用

大多数情况下,其实我们并不需要知道这些,因为你的程序也会运行的非常正常,虽然像Tomcat,Spring Boot 都有自己定义的类加载器,但是我们在不用关心的情况下也会运行的好好地.

那么类加载器可以被运行在哪些地方呢?

  • 从远程(或者文件)加载类,有时候需要加载的类可能并不是在当前的 classpath, 可能需要自己定义类加载器去加载.
  • 自己想实现一个JavaAgent来增强字节码的时候.

JavaAgent 的使用后续文章补上.先上一张图.

www.zeeklog.com  - 深入理解Java ClassLoader及在 JavaAgent 中的应用

顶层是应用代码实际运行的 ClassLoader, 可能是Application ClassLoader, 也有可能是 tomcat 的webapp ClassLoader 或者其他容器自定义的类加载器,总是是真实 的用户编写的代码运行的 classloader.

我们如果要在javaagent中增强用户或者用户使用的包进行增强的话,必须实现一个自定义的 classloader 来"继承"(委派)应用代码的类加载器.为什么?

javaagent 的代码永远都是被应用类加载器( Application ClassLoader)所加载,和应用代码的真实加载器无关,举个栗子,当前运行在 tomcat 中的代码是webapp ClassLoader 加载的,如果启动参数加上-javaagent, 这个 javaagent 还是在Application ClassLoader中加载的.

按照上面的双亲委派模型,如果我们在 javaagent 中想要访问应用里面的 api 包或者类,这是不可能的,因为按照双亲委派模型,通俗来说就是,子加载器可以访问父加载器中的类,但是反过来就行不通.

那么这个时候有没有办法能够做到呢?

我们可以自定义自己的类加载器继承应用代码类加载器(可以在 javaagent 中完成, javaagent 每加载一个类,就会回调传回真实的类加载器),然后我们在Application ClassLoader 中用自定义的类加载器去加载子类,并创建好实例(newInstance()), 将实例的引用保存 在变量中.

真实运行的时候,就会通过这个变量,去访问我们自定义加载器的内容,又由于我们的自定义类加载器是继承自应用代码的类加载器的,所以自定义类加载器中的代码可以访问应用的代码.

总结一句就是,父类加载器无法加载子类加载器的类,但是可以持有子类加载器所加载类的实例,从而实现父类加载器的代码可以调用子类加载器的代码的形式

貌似比较抽象,后面会补上详细的例子供参考.

例子

针对上面的情形,我们定义一个例子,可以详细解释 ClassLoader 的加载使用,

  1. 假如我们有如下的 ClassLoader,FooClassLoader:
package com.example.test;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 * @author lican
 */
public class FooClassLoader extends ClassLoader {

    private static final String NAME = "/Users/lican/git/test/foo/";

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass == null) {
            String s = name.substring(name.lastIndexOf(".") + 1) + ".class";
            File file = new File(NAME + s);
            try (FileInputStream fileInputStream = new FileInputStream(file)) {
                byte[] b = new byte[fileInputStream.available()];
                fileInputStream.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return loadedClass;
    }

}
  1. 被加载的类定义,然后我们将这个类放到不是源代码的路径比如我放到
    /Users/lican/git/test/foo/这里的,主要是方便测试.

package com.example.test;

public class FooTest {

    public String getFoo() {
        return "foo";
    }
}

然后测试程序为:

package com.example.test;

import java.lang.reflect.Method;

/**
 * @author lican
 */
public class ClassLoaderTest {

    private Object fooTestInstance;
    private FooClassLoader fooClassLoader = new FooClassLoader();


    public static void main(String[] args) throws Exception {
        ClassLoaderTest classLoaderTest = new ClassLoaderTest();
        classLoaderTest.initAndLoad();
        Object fooTestInstance = classLoaderTest.getFooTestInstance();
        System.out.println(fooTestInstance.getClass().getClassLoader());


        Method getFoo = fooTestInstance.getClass().getMethod("getFoo");
        System.out.println(getFoo.invoke(fooTestInstance));

        System.out.println(classLoaderTest.getClass().getClassLoader());
    }

    private void initAndLoad() throws Exception {
        Class<?> aClass = Class.forName("com.example.test.FooTest", true, fooClassLoader);
        fooTestInstance = aClass.newInstance();
    }

    public Object getFooTestInstance() {
        return fooTestInstance;
    }
}

我们用FooClassLoader来加载com.example.test.FooTest, 然后在 AppClassLoader中持有引用.被后续使用.

引用

  • 深入理解 Java 虚拟机(第二版)

Read more

三种适用于Web版IM(即时通讯)聊天信息的加密算法实现方案

三种适用于Web版IM(即时通讯)聊天信息的加密算法实现方案

文章目录 * **第一部分:引言与核心密码学概念** * **1.1 为什么IM需要端到端加密(E2EE)?** * **1.2 核心密码学概念与工具** * **第二部分:方案一:静态非对称加密(基础方案)** * **2.1 方案概述与流程** * **2.2 前端Vue实现(使用node-forge)** * **1. 安装依赖** * **2. 核心工具类 `crypto.js`** * **3. Vue组件中使用** * **2.3 后端Java实现(Spring Boot)** * **1. 实体类** * **2. Controller层** * **3. WebSocket配置** * **2.4 密钥管理、注册与登录集成** * **1. 用户注册/登录时生成密钥** * **2. 密钥设置页面** * **2.

Go语言中的未来:从泛型到WebAssembly

Go语言中的未来:从泛型到WebAssembly 前言 作为一个在小厂挣扎的Go后端老兵,我对Go语言未来的理解就一句话:能进化的绝不固步自封。 想当年刚接触Go语言时,它还没有泛型,没有模块系统,甚至连错误处理都被人诟病。现在的Go语言已经今非昔比,泛型来了,模块系统完善了,错误处理也有了更多选择。 今天就聊聊Go语言的未来发展,从泛型到WebAssembly,给大家一个能直接抄作业的方案。 为什么需要关注Go语言的未来? 我见过不少小团队,只关注当前的技术,不关心语言的发展趋势,结果技术栈逐渐落后。关注Go语言的未来能带来很多好处: * 提前准备:了解未来的特性,提前调整代码结构 * 技术选型:根据未来趋势,做出更合理的技术选型 * 职业发展:掌握最新技术,提升个人竞争力 * 项目规划:根据语言发展,制定更合理的项目规划 泛型 泛型是Go 1.18引入的重要特性,它能让我们编写更加通用的代码。 基本用法 // 定义泛型函数 func Map[T, U any](s []T, f

猫头虎AI分享 | 从SEO到GEO:315晚会曝光的“AI投毒“黑产,技术人该如何防御?

猫头虎AI分享 | 从SEO到GEO:315晚会曝光的“AI投毒“黑产,技术人该如何防御?

🐯 猫头虎AI分享 | 从SEO到GEO:315晚会曝光的"AI投毒"黑产,技术人该如何防御? 标签:AI安全大模型攻防GEO优化RAG安全内容风控315晚会深度拆解 阅读时长: 25分钟 | 难度: 进阶 | 收藏: 建议先码后看 猫头虎说: 兄弟们,2026年315晚会这次爆的料太狠了!作为一个深耕AI领域多年的老博主,我看到这条新闻的时候直接拍桌子——这哪是什么营销优化,这TM是针对大模型的数据层攻击!今天咱们不聊虚的,直接从技术架构、代码实现到防御方案,手把手拆解这个GEO黑产到底是怎么给AI"投毒"的。建议先收藏,这篇文章值得你反复看三遍! 文章目录 * 🐯 猫头虎AI分享 | 从SEO到GEO:315晚会曝光的"AI投毒"黑产,技术人该如何防御? * 一、事件回顾:当315晚会遇上AI安全 * 1.1 晚会曝光核心内容 * 1.2

AKSHARE中文官网:AI如何助力金融数据爬取与分析

快速体验 1. 打开 InsCode(快马)平台 https://www.inscode.net 2. 输入框内输入如下内容: 使用AKSHARE中文官网的API接口,开发一个AI驱动的金融数据分析工具。该工具应能自动爬取股票、基金、期货等金融数据,进行数据清洗和预处理,并利用机器学习模型进行趋势预测和可视化分析。要求支持多种数据源,提供实时数据更新和自定义分析功能,最终生成可视化报告。 1. 点击'项目生成'按钮,等待项目生成完整后预览效果 在金融数据分析领域,数据获取和处理往往是耗时费力的环节。最近尝试用AKSHARE的API结合AI技术搭建了一个自动化分析工具,整个过程让我深刻体会到技术组合带来的效率提升。这里分享几个关键环节的实践心得: 1. 数据获取的智能化改造 AKSHARE提供了丰富的金融数据接口,但传统调用方式需要手动处理参数和返回值。通过AI辅助生成适配代码模板,能自动匹配不同接口的数据结构。比如获取股票历史行情时,AI会建议最佳的时间字段格式化方式,避免常见的日期格式错误。 2. 数据清洗的自动化流程 金融数据常存在缺失