Unsafe,一个“反 Java”的 class
Unsafe 类位于 sun.misc 包中,它提供了一组用于执行低级别、不安全操作的方法。尽管 Unsafe 类及其所有方法都是公共的,但它的使用受到限制,因为只有受信任的代码才能获取其实例。这个类通常被用于一些底层的、对性能敏感的操作,比如直接内存访问、CAS(Compare and Swap)操作等。本文将介绍这个“反 Java”的类及其方法的典型使用场景。
由于
Unsafe类涉及到直接内存访问和其他底层操作,使用它需要极大的谨慎,因为它可以绕过Java语言的一些安全性和健壮性检查。在正常的应用程序代码中,最好避免直接使用Unsafe类,以确保代码的可读性和可维护性。在一些特殊情况下,比如一些高性能库的实现,可能会使用Unsafe类来进行一些性能优化。
尽管在生产中需要谨慎使用
Unsafe,但是可以在测试中使用它来更真实地接触Java对象在内存中的存储结构,验证自己的理论知识。
获取 Unsafe 实例
在
Java 9及之后的版本中,Unsafe类中的getUnsafe()方法被标记为不安全(Unsafe),不再允许普通的Java应用程序代码通过此方法获取Unsafe实例。这是为了提高Java的安全性,防止滥用Unsafe类的功能。
在正常的 Java 应用程序中,获取 Unsafe 实例是不被推荐的,因为它违反了 Java 语言的安全性和封装原则。Unsafe 类的设计本意是为了 Java 库和虚拟机的实现使用,而不是为了普通应用程序开发者使用。Unsafe 对象为调用者提供了执行不安全操作的能力,它可用于在任意内存地址读取和写入数据,因此返回的 Unsafe 对象应由调用者仔细保护。它绝不能传递给不受信任的代码。此类中的大多数方法都是非常低级的,并且对应于少量硬件指令。
获取 Unsafe 实例的静态方法如下:
1 |
|
Unsafe 使用单例模式,可以通过静态方法 getUnsafe 获取 Unsafe 实例,并且调用方法的类为启动类加载器所加载才不会抛出异常。获取 Unsafe 实例有以下两种可行方案:
- 通过
-Xbootclasspath/a:${path}把调用方法的类所在的jar包路径追加到启动类路径中,使该类被启动类加载器加载。关于启动类路径的信息可以参考Java 类加载器源码分析 | ClassLoader 的搜索路径 - 通过反射获取
Unsafe类中的Unsafe实例
1 | private static Unsafe getUnsafe() { |
内存操作
Unsafe 类中包含了一些关于内存操作的方法,这些方法通常被认为是不安全的,因为它们可以绕过 Java 语言的内置安全性和类型检查。以下是一些常见的 Unsafe 类中关于内存操作的方法:
allocateMemory: 分配一个给定大小(以字节为单位)的本地内存块,内容未初始化,通常是垃圾。生成的本地指针永远不会为零,并且将针对所有类型进行对齐。
1 | public native long allocateMemory(long bytes); |
reallocateMemory: 将本地内存块的大小调整为给定大小(以字节为单位),超过旧内存块大小的内容未初始化,通常是垃圾。当且仅当请求的大小为零时,生成的本地指针才为零。传递给此方法的地址可能为空,在这种情况下将执行分配。
1 | public native long reallocateMemory(long address, long bytes); |
freeMemory: 释放之前由allocateMemory或reallocateMemory分配的内存。
1 | public native void freeMemory(long address); |
setMemory: 将给定内存块中的所有字节设置为固定值(通常为零)。
1 | public native void setMemory(Object o, long offset, long bytes, byte value); |
copyMemory: 复制指定长度的内存块
1 | public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); |
putXxx: 将指定偏移量处的内存设置为指定的值,其中Xxx可以是Object、int、long、float和double等。
1 | public native void putObject(Object o, long offset, Object x); |
getXxx: 从指定偏移量处的内存读取值,其中Xxx可以是Object、int、long、float和double等。
1 | public native Object getObject(Object o, long offset); |
putXxx和getXxx也提供了按绝对基地址操作内存的方法。
1 | public native byte getByte(long address); |
从内存读取值时,除非满足以下情况之一,否则结果不确定:
- 偏移量是通过
objectFieldOffset从字段的Field对象获取的,o指向的对象的类与字段所属的类兼容。 - 偏移量和
o指向的对象(无论是否为null)分别是通过staticFieldOffset和staticFieldBase从Field对象获得的。 o指向的是一个数组,偏移量是一个形式为B+N*S的整数,其中N是数组的有效索引,B和S分别是通过arrayBaseOffset和arrayIndexScale获得的值。
做一些“不确定”的测试,比如使用
byte相关的方法操作int所在的内存块,是有意思且有帮助的,了解如何破坏,也可以更好地学习如何保护。
分配堆外内存
在 Java NIO(New I/O)中,分配堆外内存使用了 Unsafe 类的 allocateMemory 方法。堆外内存是一种在 Java 虚拟机之外分配的内存,它不受 Java 堆内存管理机制的控制。这种内存分配的主要目的是提高 I/O 操作的性能,因为它可以直接与底层操作系统进行交互,而不涉及 Java 堆内存的复杂性。Java 虚拟机的垃圾回收器虽然不直接管理这块内存,但是它通过一种称为“引用清理”(Reference Counting)的机制来处理。
1 | DirectByteBuffer(int cap) { |
当 DirectByteBuffer 对象仅被 Cleaner 对象(虚引用)引用时,它可以在任意一次 GC 中被垃圾回收。在 DirectByteBuffer 对象被垃圾回收后,Cleaner 对象会被加入到引用队列,ReferenceHandler 线程将调用 Deallocator 对象的 run 方法,从而实现本地内存的自动释放。
1 | private static class Deallocator implements Runnable { |
CAS 相关
Unsafe 提供了 3 个 CAS 相关操作的方法,方法将内存位置的值与预期原值比较,如果相匹配,则 CPU 会自动将该位置更新为新值,否则,CPU 不做任何操作。这些方法的底层实现对应着 CPU 指令 cmpxchg。
1 | // 如果 Java 变量当前符合预期,则自动将其更新为 x。 |
在 AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 Unsafe 的 objectFieldOffset 方法获取。
1 | public class AtomicInteger extends Number implements java.io.Serializable { |
CAS 更新变量的值的内存变化如下:

配合 ClassLayout 打印 AtomicInteger 的内部结构更直观地感受 offset 的含义:
1 | java.util.concurrent.atomic.AtomicInteger object internals: |