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);

但使用 UnsafeallocateInstance() 方法时,只会分配内存而不会调用构造函数

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;
    }
}

利用 UnsafeputInt() 方法,我们可以直接修改私有字段,破坏对象状态:

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 管理的堆外内存

UnsafeallocateMemory() 方法能分配巨大的堆外对象,这块内存对 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 项目,可直接导入运行)。


原始标题:Guide to sun.misc.Unsafe