1. 概述
本文将深入探讨 JRE 中一个极具魅力的类——sun.misc.Unsafe
。这个类提供了底层操作机制,最初设计仅限 Java 核心库内部使用,普通开发者本不该接触它。这些底层机制主要服务于核心库的内部实现需求。
2. 获取 Unsafe 实例
要使用 Unsafe
类,首先需要获取其实例——但这并不简单,因为该类设计初衷就是禁止外部直接调用。
⚠️ 官方提供的静态方法 getUnsafe()
默认会抛出 SecurityException
。
不过,我们可以通过反射来绕过限制:
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
3. 使用 Unsafe 实例化对象
假设有一个简单类,其构造函数会在对象创建时初始化变量:
class InitializationOrdering {
private long a;
public InitializationOrdering() {
this.a = 1;
}
public long getA() {
return this.a;
}
}
通过构造函数初始化对象时,getA()
会返回 1:
InitializationOrdering o1 = new InitializationOrdering();
assertEquals(o1.getA(), 1);
但使用 Unsafe
的 allocateInstance()
方法时,只会分配内存而不会调用构造函数:
InitializationOrdering o3
= (InitializationOrdering) unsafe.allocateInstance(InitializationOrdering.class);
assertEquals(o3.getA(), 0);
注意构造函数未被调用,因此 getA()
返回了 long
类型的默认值 0。
4. 修改私有字段
假设有一个类包含私有字段 secret
:
class SecretHolder {
private int SECRET_VALUE = 0;
public boolean secretIsDisclosed() {
return SECRET_VALUE == 1;
}
}
利用 Unsafe
的 putInt()
方法,我们可以直接修改私有字段,破坏对象状态:
SecretHolder secretHolder = new SecretHolder();
Field f = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
unsafe.putInt(secretHolder, unsafe.objectFieldOffset(f), 1);
assertTrue(secretHolder.secretIsDisclosed());
通过反射获取字段后,就能用 Unsafe
将其值修改为任意 int
值。
5. 抛出异常
Unsafe
执行的代码不受编译器常规检查。我们可以用 throwException()
抛出任何异常,即使受检异常也无需声明或捕获:
@Test(expected = IOException.class)
public void givenUnsafeThrowException_whenThrowCheckedException_thenNotNeedToCatchIt() {
unsafe.throwException(new IOException());
}
抛出 IOException
(受检异常)后,既不需要捕获也不需在方法签名中声明。
6. 堆外内存
当 JVM 内存不足时,频繁触发 GC 会严重影响性能。理想情况下,我们需要一块不受 GC 管理的堆外内存。
Unsafe
的 allocateMemory()
方法能分配巨大的堆外对象,这块内存对 GC 和 JVM 不可见。
✅ 优势明显,但需手动管理:使用后必须调用 freeMemory()
释放内存,否则会内存泄漏。
下面实现一个堆外字节数组:
class OffHeapArray {
private final static int BYTE = 1;
private long size;
private long address;
public OffHeapArray(long size) throws NoSuchFieldException, IllegalAccessException {
this.size = size;
address = getUnsafe().allocateMemory(size * BYTE);
}
private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}
public void set(long i, byte value) throws NoSuchFieldException, IllegalAccessException {
getUnsafe().putByte(address + i * BYTE, value);
}
public int get(long idx) throws NoSuchFieldException, IllegalAccessException {
return getUnsafe().getByte(address + idx * BYTE);
}
public long size() {
return size;
}
public void freeMemory() throws NoSuchFieldException, IllegalAccessException {
getUnsafe().freeMemory(address);
}
}
在构造函数中分配指定大小的数组,起始地址保存在 address
字段。set()
方法通过索引写入值,get()
方法通过偏移量读取值。
创建超大堆外数组:
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);
写入并读取数据验证正确性:
int sum = 0;
for (int i = 0; i < 100; i++) {
array.set((long) Integer.MAX_VALUE + i, (byte) 3);
sum += array.get((long) Integer.MAX_VALUE + i);
}
assertEquals(array.size(), SUPER_SIZE);
assertEquals(sum, 300);
最后必须手动释放内存:
array.freeMemory();
7. CompareAndSwap 操作
java.concurrent
包中的高效类(如 AtomicInteger
)底层都依赖 Unsafe
的 CAS 操作,以实现极致性能。这种机制在无锁算法中广泛使用,利用处理器 CAS 指令比传统悲观锁快得多。
用 compareAndSwapLong()
实现一个 CAS 计数器:
class CASCounter {
private Unsafe unsafe;
private volatile long counter = 0;
private long offset;
private Unsafe getUnsafe() throws IllegalAccessException, NoSuchFieldException {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}
public CASCounter() throws Exception {
unsafe = getUnsafe();
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
}
public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}
public long getCounter() {
return counter;
}
}
构造函数中获取 counter
字段的内存地址。increment()
方法在循环中使用 CAS 操作:如果当前值未被修改,则更新;否则重试。这种无阻塞机制称为无锁算法。
多线程测试:
int NUM_OF_THREADS = 1_000;
int NUM_OF_INCREMENTS = 10_000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);
CASCounter casCounter = new CASCounter();
IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
.forEach(i -> service.submit(() -> IntStream
.rangeClosed(0, NUM_OF_INCREMENTS - 1)
.forEach(j -> casCounter.increment())));
验证计数器最终值:
assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());
8. Park/Unpark 机制
Unsafe
API 中有两个有趣的方法:park()
和 unpark()
,JVM 用它们进行线程上下文切换。
- **park()**:阻塞线程(类似
Object.wait()
,但直接调用操作系统代码,性能更优) - **unpark()**:唤醒被阻塞的线程
在应用线程池的线程转储中经常能看到这两个方法。
9. 总结
本文探讨了 Unsafe
类的核心功能:
- ✅ 访问私有字段
- ✅ 分配堆外内存
- ✅ 实现 CAS 无锁算法
所有示例代码可在 GitHub 项目 中获取(Maven 项目,可直接导入运行)。