概览
Java 15 引入了不少新特性,其中隐藏类(Hidden Classes)是值得关注的一个亮点(详见 JEP-371)。这个特性本质上是取代 Unsafe API 的标准方案——毕竟 Unsafe API 本身就不推荐在 JDK 外使用。
对于需要操作动态字节码或开发 JVM 语言的开发者来说,隐藏类简直是个实用工具箱。它能解决动态类生成效率问题,同时避免内存浪费。
什么是隐藏类?
动态生成的类对低延迟应用至关重要,但这类类通常生命周期很短。如果让它们和静态类一样存活整个应用周期,内存占用就太浪费了。传统方案(比如为每个类单独搞个类加载器)既笨重又低效。
从 Java 15 开始,隐藏类成了动态类生成的标准解决方案。
核心定义:隐藏类是一种无法被字节码或其他类直接使用的类。 注意这里的“类”实际包含类和接口两种类型。它还能加入访问控制嵌套(access control nest),并且可以独立于其他类被卸载。
隐藏类的核心特性
这些动态生成的类有几个关键属性:
- 不可发现性:JVM 在链接阶段找不到它,类加载器也查不到。反射方法如
Class::forName
、ClassLoader::findLoadedClass
、Lookup::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 获取。