前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >java类加载器挖坑记

java类加载器挖坑记

原创
作者头像
挖坑工程师
发布2023-09-22 14:38:12
3420
发布2023-09-22 14:38:12
举报
文章被收录于专栏:个人学习摘要

概念

Java类加载器时Java虚拟机(JVM)的一部分,负责将类的字节码加载到内存中,并将其转换为可执行的Java对象。Java中每个类都是由特定的类加载器加载,并在运行时创建为一个Class对象。类加载器支持从文件系统、网络、内存等多个不同来源加载类的字节码,同时还负责分析类的依赖关系,加载所需的关联类。

类加载器分类

Java虚拟机定义了三个主要类加载器:

  • 启动类加载器(Bootstrap Class Loader) 负责将存放在<JAVA_HOME>\lib等目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名,如rt.jar)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,若需要把加载请求委派给引导类加载器,直接用null代替即可。
  • 扩展类加载器(JDK9之前) 负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库。
  • 平台类加载器(JDK9之后) 加载指定modules的类库。
  • 应用程序类加载器 可通过ClassLoader中的getSystemClassLoader()方法的获取,被称为系统类加载器,负责加载用户类路径(classpath)上所指定的类库。
  • 自定义类加载器 自定义类加载器时通常使用应用程序类加载器作为父类加载器,然后内部逻辑不一定遵循双亲委派机制。

备注:类具体加载范围说明不一定准确,在java启动时通过-cp,-mp等参数修改类加载器扫描的范围

类加载器原理

类加载器工作主要分三个步骤:

  1. 加载(Loading):读取类字节码,可以通过类全限定名从加载器支持的路径下获取,也可以从网络、内存等地方获取。
  2. 链接(Linking):将类的字节码转换为可以在jvm中运行的格式
  3. 验证(Verification):验证字节码准确性和安全性
  4. 准备(Preparation):为类的静态变量分配内存,并初始化默认值
  5. 分析(Resolution):将类的符号引用解析为直接引用
  6. 初始化(Initialization):执行类的初始化代码。
代码语言:txt
复制
flowchart LR
    RF(读取字节码) --> DF(调用defineClass实现类定义/链接)
    DF --> IT(进行类对象初始化)

类加载器使用双亲委派模型来加载类,但是在部分自定义类加载器中不一定遵循该模型,比如Tomcat实现的类加载器。

双亲委派机制

类加载器之间的父子关系不会以继承来实现,而是以组合(Composition)关系来复用父加载器的代码。加载时优先委派给父加载器进行加载,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去完成加载。JDK9之后在委派给父加载器前,先判断该类是否能够归属到某一个系统模块中,若可以找到归属关系,则优先委派给负责那个模块的类加载器完成加载。

比如自定义ClassLoader加载一个类的顺序从1逐层往上委派父加载器:

具体踩坑记录

java类加载器机制现在随便问谁都能说出双亲委派机制,但是他真的是完全的双亲委派么?会不会在某些情况下不遵循双亲委派机制呢?还有在双亲委派机制中,由父加载器加载的class中如果依赖了只能由子类加载器加载的class会不会由问题呢?下面通过实例来记录踩坑记录。

Bootstrap类加载器能获取到么?

代码语言:java
复制
/**
 * TestClassLoader
 */
public class TestClassLoader {

    public static void main(String[] args) {
        // 获取应用程序类加载器
        ClassLoader applicationLoader = ClassLoader.getSystemClassLoader();
        System.out.println("应用程序类加载器:" + applicationLoader);

        // 应用程序类加载器父加载器:Ext Class Loader
        System.out.println("扩展类加载器:" + applicationLoader.getParent());

        // 获取Bootstrap 类加载器,这里会返回 null
        System.out.println("启动类加载器:" + applicationLoader.getParent().getParent());

        // ClassLoader 是由BootstrapClassLoader加载,再次验证classLoader为空
        System.out.println("启动类加载器:" + ClassLoader.class.getClassLoader());
    }
}

// 输出结果
// 应用程序类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
// 扩展类加载器:sun.misc.Launcher$ExtClassLoader@3b22cdd0
// 启动类加载器:null
// 启动类加载器:null

类加载依赖时只能由当前类加载器及其父加载器加载

java每个类加载器都有自己的空间,同时会包含父加载器的命名空间。父加载器加载的类对子加载器是可见的,但是子加载器加载的类对父加载器不可见。同时相互没有父子关系的类加载器之间加载的类也是不可见的。

代码语言:java
复制
import sun.misc.Unsafe;
import test.ClassJavaFileManager;
import test.MemoryClassLoader;
import test.StringJavaFileObject;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

/**
 * TestClassLoader
 */
public class TestClassLoader {

    private static final String animalCode = "public class Animal {\n" +
            "    public void sayHello() {\n" +
            "        System.out.println(\"animal sayHello: animal\");\n" +
            "    }\n" +
            "}";

    private static final String dogCode = "public class Dog {\n" +
            "    public void sayHello() throws Exception {\n" +
            "        System.out.println(\"dog sayHello: bingo dog\");\n" +
            "        // 这里使用Dog类加载器加载Animal,用于验证依赖加载\n" +
            "        Class<?> animalCls = Class.forName(\"Animal\");\n" +
            "        Object animalInstance = animalCls.newInstance();\n" +
            "        animalCls.getMethod(\"sayHello\").invoke(animalInstance);\n" +
            "    }\n" +
            "}";

    public static void main(String[] args) throws Exception {
        // Dog使用自定义类加载器加载,父加载器为 "应用程序加载器"
        //ClassLoader dogClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader dogClassLoader = new MemoryClassLoader(Collections.EMPTY_MAP);

        // Animal使用应用程序加载器加载
        // ClassLoader animalClassLoader = new MemoryClassLoader(Collections.EMPTY_MAP);
        ClassLoader animalClassLoader = ClassLoader.getSystemClassLoader();

        // 将Animal加载到Application Class Loader中
        loadClass("Animal", animalCode, animalClassLoader);

        // 将Dog加载到Application Class Loader中
        // loadClass("Dog", dogCode, animalClassLoader);
        loadClass("Dog", dogCode, dogClassLoader);

        // 通过自定义类加载器加载编译后的类文件
        // Class<?> dogCls = Class.forName("Dog", true, animalClassLoader);
        Class<?> dogCls = Class.forName("Dog", true, dogClassLoader);

        // 通过反射创建类实例,需要无餐构造方法
        Object instance = dogCls.newInstance();

        // 通过反射回调hello方法
        dogCls.getMethod("sayHello").invoke(instance);
    }

    public static void loadClass(String className, String code, ClassLoader classLoader) throws Exception {
        // 获取当前平台编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

        // 获取java文件管理器,默认会将编译后的class保存到当前目录下
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);

        // 自定义class文件管理器,用于在内存中保存字节码信息
        ClassJavaFileManager classJavaFileManager = new ClassJavaFileManager(fileManager);

        // 创建需要编译的java文件对象
        StringJavaFileObject strJavaFileObject = new StringJavaFileObject(className, code);

        // 构建需要编译的java文件集合
        Iterable<? extends JavaFileObject> fileObjects = Collections.singletonList(strJavaFileObject);

        // 设置编译选项
        List<String> options = Arrays.asList("-classpath", System.getProperty("java.class.path"));

        // 创建编译任务
        JavaCompiler.CompilationTask task = compiler.getTask(null, classJavaFileManager, null, options, null,
                fileObjects);

        // 执行编译任务
        Boolean result = task.call();

        // Check the result of compilation
        if (result == null || !result) {
            throw new Exception("Compilation failed");
        }

        // 通过反射获取Unsafe实例,Unsafe只能在通过bootstrapClassLoader加载的类中直接获取
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        Unsafe unsafe = (Unsafe) field.get(null);

        // 获取编译后的字节码
        byte[] classByte = classJavaFileManager.getClsJavaFileObject().getBytes();

        // 将该类动态加入应用类加载器中
        unsafe.defineClass(className, classByte, 0, classByte.length, classLoader, null);
    }
}

// 输出:
// 1. Animal使用自定义的MemoryClassLoader加载,而Dog使用应用程序类加载器加载时运行会提示错误:
// dog sayHello: bingo dog 
// ...
// Caused by: java.lang.ClassNotFoundException: Animal

// 2. Animal和Dog使用相同的MemoryClassLoader加载,运行时会正常输出:
// dog sayHello: bingo dog
// animal sayHello: animal Animal

// 3. Animal使用应用程序类加载器加载,Dog使用自定义的MemoryClassLoader加载,运行时会正常输出
// dog sayHello: bingo dog 
// ...
// Caused by: java.lang.ClassNotFoundException: Animal

// 备注: Java的依赖关系通常只能由自己的类加载器或其父加载器来处理。

如何在运行时动态修改应用程序类加载器的扫描路径

代码语言:java
复制
// 1. 借助javaAgent的Instrumentation
// 将jar包加入bootstrapClassLoader扫描范围,常用语加载外部jar
// void appendToBootstrapClassLoaderSearch(JarFile jarfile);

// 将jar包加入AppClassLoader扫描范围,常用于加载外部jar
// void appendToSystemClassLoaderSearch(JarFile jarfile);

// 2. 通过反射将jar加入到java.class.path下
import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

public class ClassUtil {

    private static final Method addURL = initAddURLMethod();

    private static final URLClassLoader system = (URLClassLoader) ClassLoader.getSystemClassLoader();

    private static Method initAddURLMethod() {
        try {
            Method add = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            add.setAccessible(true);
            return add;
        } catch (Exception e) {
            throw new RuntimeException("获取URLClassLoader.addURL方法失败");
        }
    }

    public static void loadJarFile(File file) throws Exception {
        addURL.invoke(system, file.toURI().toURL());
    }
}

// 3. 在java启动时通过 -cp -mp参数指定java.class.path路径
// 3.1 指定目录
// java -cp /path/to/classes MyApplication
// 3.2 指定一个JAR文件作为类路径
// java -cp /path/to/myjar.jar MyApplication
// 3.3 指定多个目录和JAR文件作为类路径
// java -cp /path/to/myjar.jar:/path/to/ MyApplication

ClassNotFoundException 和 NoClassDefFoundError 区别

  1. ClassNotFoundException是在动态加载类的时候调用Class.forName等方法抛出的异常,而NoClassDefFoundError是在编译通过后执行过程中类找不到的错误。
  2. ClassNotFoundException是发生在加载内存阶段,加载时从classpath中找不到需要的类就会出现ClassNotFoundException,出现这种错误可能是调用上述的三个方法加载类,也有可能是已经被一个类加载器加载过的类又被另一个类加载器加载;而NoClassDefFoundError是链接阶段从内存找不到需要的类才会出现,比如maven项目有的时候打包问题会引起这个error报错,这个时候要把仓库中的包删掉重新去拉一下包。
  3. ClassNotFoundException是一个检查异常;而NoClassDefFoundError是一个运行时错误。

JavaAgent部分类/实例变量出现运行时异常

代码语言:java
复制
// 比如,使用ByteBuddy Adivce方式进行方法增强
import net.bytebuddy.asm.Advice;

import java.lang.reflect.Method;
import java.util.logging.Logger;

public class HutoolHttp5Advice {
    // public static final Logger logger = Logger.getLogger("HutoolHttp5Advice");
    private static final Logger logger = Logger.getLogger("HutoolHttp5Advice");
    
    @Advice.OnMethodEnter
    public static void enter(
            @Advice.Origin Method method,
            @Advice.This Object thiz,
            @Advice.AllArguments Object[] args) throws Throwable {
        // 这里在实际运行时会出现异常
        logger.info("record log=====");
    }
}

// 原因:
// 1. HutoolHttp5Advice这个类是由Application Class Loader加载并且时一个单独的Class对象。
// 2. 方法增强实际是通过修改原class字节码,把增强的代码和原方法合并后生成一个新方法并在class中进行覆写,最后对应的类加载器加载生成对应的Class对象。
// 3. 这时访问HutoolHttp5Advice.logger就类似跨包或类访问其他类的private成员变量,因此会出现异常。

// 修复方法:将logger 字段的访问修饰符调整为 public即可。

其他笔记

  1. 同一个类尽量不要通过不同的类加载器加载,因为多个类加载器间可能时相互隔离的,如果类中存在类变量或者线程变量在使用时可能由于类加载器不一致导致数据异常。
  2. 运行过程中如果需要动态加载多个类,需要注意按需卸载避免内存溢出。
  3. Tomcat实现的自定义类加载器在不同webapp之间会创建不同的类加载实例,他们加载的应用类是隔离的,不完全遵循双亲委派模型。所以在Tomcat中如果要运行多个webapp同时还需要使用javaagent进行增强,需要考虑多个webapp之间是否保持行为一致性。比如:通过javaagent对Serverlet进行拦截,那这个对同一个Tomcat下所有webapp都生效的。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 概念
  • 类加载器分类
  • 类加载器原理
  • 双亲委派机制
  • 具体踩坑记录
    • Bootstrap类加载器能获取到么?
      • 类加载依赖时只能由当前类加载器及其父加载器加载
        • 如何在运行时动态修改应用程序类加载器的扫描路径
          • ClassNotFoundException 和 NoClassDefFoundError 区别
            • JavaAgent部分类/实例变量出现运行时异常
              • 其他笔记
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档