概览

Java 15 引入了不少新特性,其中隐藏类(Hidden Classes)是值得关注的一个亮点(详见 JEP-371)。这个特性本质上是取代 Unsafe API 的标准方案——毕竟 Unsafe API 本身就不推荐在 JDK 外使用。

对于需要操作动态字节码或开发 JVM 语言的开发者来说,隐藏类简直是个实用工具箱。它能解决动态类生成效率问题,同时避免内存浪费。

什么是隐藏类?

动态生成的类对低延迟应用至关重要,但这类类通常生命周期很短。如果让它们和静态类一样存活整个应用周期,内存占用就太浪费了。传统方案(比如为每个类单独搞个类加载器)既笨重又低效。

从 Java 15 开始,隐藏类成了动态类生成的标准解决方案。

核心定义:隐藏类是一种无法被字节码或其他类直接使用的类。 注意这里的“类”实际包含类和接口两种类型。它还能加入访问控制嵌套(access control nest),并且可以独立于其他类被卸载。

隐藏类的核心特性

这些动态生成的类有几个关键属性:

  • 不可发现性:JVM 在链接阶段找不到它,类加载器也查不到。反射方法如 Class::forNameClassLoader::findLoadedClassLookup::findClass 全部失效
  • 不能作为基础类型:无法用作超类、字段类型、返回类型或参数类型
  • 内部直接访问:隐藏类内部的代码可以直接使用自身,无需依赖 Class 对象
  • 不可变 final 字段:声明的 final 字段无论访问权限如何都无法修改
  • 扩展嵌套访问:能将不可发现的类加入访问控制嵌套
  • 独立卸载:即使定义类加载器仍可达,也能被卸载
  • 栈跟踪隐藏:默认不显示方法名和类名(但可通过 JVM 参数调整)

创建隐藏类

隐藏类不由任何类加载器创建——它和查找类(lookup class)共享同一个定义类加载器、运行时包和保护域。

首先创建 Lookup 对象:

MethodHandles.Lookup lookup = MethodHandles.lookup();

然后通过 Lookup::defineHiddenClass 方法创建隐藏类,它接收字节数组作为参数。这里我们准备一个简单示例类:

public class HiddenClass {
    public String convertToUpperCase(String s) {
        return s.toUpperCase();
    }
}

获取类路径并转为字节数组:

Class<?> clazz = HiddenClass.class;
String className = clazz.getName();
String classAsPath = className.replace('.', '/') + ".class";
InputStream stream = clazz.getClassLoader()
    .getResourceAsStream(classAsPath);
byte[] bytes = IOUtils.toByteArray(stream);

最后调用定义方法:

Class<?> hiddenClass = lookup.defineHiddenClass(
    bytes, 
    true, 
    ClassOption.NESTMATE
).lookupClass();

参数说明:

  • true 表示立即初始化类
  • ClassOption.NESTMATE 将隐藏类加入查找类的嵌套,可访问嵌套内所有私有成员

如果需要强绑定类加载器,用 ClassOption.STRONG —— 这样只有当定义加载器不可达时才能卸载。

使用隐藏类

框架通常在运行时生成隐藏类,通过反射间接使用它们。基于上节创建的类,我们来看看实际用法:

由于无法直接转换类型,先用 Object 存储实例:

Object hiddenClassObject = hiddenClass.getConstructor().newInstance();

获取方法并调用:

Method method = hiddenClassObject.getClass()
    .getDeclaredMethod("convertToUpperCase", String.class);

验证隐藏类特性:

// 确认是隐藏类
Assertions.assertEquals(true, hiddenClass.isHidden());

// 规范名称为 null
Assertions.assertEquals(null, hiddenClass.getCanonicalName());

// 共享类加载器
Assertions.assertEquals(
    this.getClass().getClassLoader(), 
    hiddenClass.getClassLoader()
);

// 不可发现性验证
Assertions.assertThrows(
    ClassNotFoundException.class, 
    () -> Class.forName(hiddenClass.getName())
);

关键点:其他类只能通过 Class 对象使用隐藏类,任何常规查找方式都会失败。

匿名类 vs 隐藏类

匿名类(无显式名称的内部类)和隐藏类有本质区别:

对比维度 匿名类 隐藏类
命名规则 动态生成含 $ 的名称 HiddenClass/1234
创建方式 Unsafe::defineAnonymousClass(已废弃) Lookup::defineHiddenClass
常量池 支持常量池补丁(预解析常量) 不支持
访问权限 可跨包访问宿主类 protected 成员 无此特权
嵌套能力 可嵌套其他类访问成员 不能嵌套其他类

重要提示:隐藏类不是匿名类的替代品,但在 JDK 内部(如 Java 15 的 lambda 表达式)已逐步取代匿名类。

总结

隐藏类为动态字节码操作提供了安全高效的解决方案,解决了传统方案的性能和内存问题。它通过不可发现性、独立卸载等特性,成为框架和 JVM 语言开发的利器。完整示例代码可在 GitHub 获取。


原始标题:Hidden Classes in Java | Baeldung