本文主要介绍一下 JDK 里的核弹之 sun.misc.Unsage 类。

0. 概述

本文主要介绍一下 JDK 里的核弹之 sun.misc.Unsage 类。这个类提供了更加底层的操作,主要是服务于 Java 的核心类库。普通用户一般情况下,并不会直接用到该类。

1. 获取 Unsafe 实例

Unsafe 提供了 getUnsafe 方法,代码实例如下所示:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
Unsafe.getUnsafe();
}
}

但是,会有如下报错:

1
2
3
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at com.bendcap.java.jvm.unsafe.Main.main(Main.java:13)

这是因为 Unsafe 类主要是 JDK 内部使用,并不提供给普通用户调用,也就是其名字所暗示的那样,这些操作不安全。

但是我们仍让可以通过反射获取到实例:

1
2
3
4
5
6
7
8
9
10
11
12
public static Unsafe createUnsafe() {
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

2. 不调用构造方法方法创建对象

我们知道,JVM 在创建对象实例的时候会调用默认或者有参构造方法,在字节码中对应 init 方法,那如何实例话一个对象,而不调用 init 方法呢? 答案就是 Unsafe

假设我们有一个类 Person:

class Person {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
    private String name;
private int age;

static {
System.out.println("Person static code block");
}

public Person() {
System.out.println("Person default constructor");
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

那么我们可以通过 Unsafe 在内存中直接创建 Person 的实例:

1
2
3
4
5
6
7
8
9
10
public Person instancePerson() {
Person person = null;
try {
person = (Person) createUnsafe().allocateInstance(Person.class);
} catch (InstantiationException e) {
e.printStackTrace();
}
person.setName("Unsafer");
return person;
}

Unsafe 只会分配 Person 对应的内存空间,而不触发构造函数。
: Class 文件中的 clinit 仍然会执行。

举一个生产实例。Gson 中反序列化的时候,ReflectiveTypeAdapterFactory 类负责通过反射设置字段值,其中在或许反序列化对应的 Class 实例的时候就用到了 Unsafe, 关键代码摘抄如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public abstract class UnsafeAllocator {
public abstract <T> T newInstance(Class<T> c) throws Exception;

public static UnsafeAllocator create() {
// try JVM
// public class Unsafe {
// public Object allocateInstance(Class<?> type);
// }
try {
Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
Field f = unsafeClass.getDeclaredField("theUnsafe");
f.setAccessible(true);
final Object unsafe = f.get(null);
final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
return new UnsafeAllocator() {
@Override
@SuppressWarnings("unchecked")
public <T> T newInstance(Class<T> c) throws Exception {
assertInstantiable(c);
return (T) allocateInstance.invoke(unsafe, c);
}
};
} catch (Exception ignored) {
}
...
// give up
return new UnsafeAllocator() {
@Override
public <T> T newInstance(Class<T> c) {
throw new UnsupportedOperationException("Cannot allocate " + c);
}
};
}
}

3.改变私有字段

假设我们有如下类:

1
2
3
4
5
6
7
public class SecretHolder {
private int SECRET_VALUE = 0;

public boolean secretValueDisclosed() {
return SECRET_VALUE == 1;
}
}

下面我们通过 Unsafe 来改变私有属性 SECRET_VALUE 的值。

1
2
3
4
5
6
7
8
9
10
SecretHolder secretHolder = new SecretHolder();
Field field = null;
try {
field = secretHolder.getClass().getDeclaredField("SECRET_VALUE");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
Unsafe unsafe = createUnsafe();
unsafe.putInt(secretHolder, unsafe.objectFieldOffset(field), 1);
return secretHolder.secretValueDisclosed();

我们通过 unsafe.putInt 直接改变了 SecretHolder 的私有属性的值。一旦我们通过反射获得了类的私有属性字段,我们就可以直接操作它的值。

4. 抛出异常而不会触发 CE(Checked Exception)

通过 unsafe.throwExceptio 创建的异常不会被编译器检查,方法的调用者也不需要处理异常。

1
2
3
4
public void throwException() {
Unsafe unsafe = createUnsafe();
unsafe.throwException(new IOException());
}

5. 堆外内存

Java 中对象分配一般是在 Heap 中进行的(例外是 TLAB等),当应用内存不足的时候,可以通过触发 GC 进行垃圾回收,但是如果有大量对象存活到永久代,并且仍然引用可达,那么我们就需要堆外内存(Off-Heap Memory)来缓解频繁 GC 造成的压力。

Unsafe.allocateMemory 给了我们在直接内存中分配对象的能力,这块内存是非堆内存,因此,不会受到 GC 的频繁分析和干扰。

虽然这样可以缓解大量对象占用内存对 GC 和 JVM 造成的压力,这也就需要我们手动管理内存,因此,在合适的事后我们需要手动调用 freeMemory 来释放内存。

举例,我们在内存中分配字节数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class OffHeapArray {
private final static int BYTE = 1;
private long size;
private long address;
private Unsafe unsafe;

public OffHeapArray(long size, Unsafe unsafe) {
this.size = size;
this.unsafe = unsafe;
address = unsafe.allocateMemory(size * BYTE);
}

public void set(long i, byte value) {
unsafe.putByte(address + i * BYTE, value);
}

public int get(long idx) {
return unsafe.getByte(address + idx * BYTE);
}

public long size() {
return size;
}

public void freeMemory() {
unsafe.freeMemory(address);
}
}

我们可以通过如何代码分配内存空间:

1
2
long SUPER_SIZE = (long) Integer.MAX_VALUE * 2;
OffHeapArray array = new OffHeapArray(SUPER_SIZE);

测试用例如下:

1
2
3
4
5
6
7
8
9
10
@Test
public void testOffHeap() {
int sum = 0;
for (int i = 0; i < 100; i++) {
offHeapArray.set((long) Integer.MAX_VALUE + i, (byte) 3);
sum += offHeapArray.get((long) Integer.MAX_VALUE + i);
}
assertEquals(offHeapArray.size(), SUPER_SIZE);
assertEquals(sum, 300);
}

一定不要忘了,在合适的时候释放内存。

6. CAS(CompareAndSwap)

java.concurrent 包中提供了大量并发相关的操作,例如 AtomicInteger 就用了 Unsafe.compareAndSwap 操作来实现 lock-free 的操作,保证更好的性能。

假设我们做一个累加器,开启 1000 个线程,每个线程循环累加 10_000 次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CASCounter {
private Unsafe unsafe;
private volatile long counter = 0;
private long offset;

public CASCounter(Unsafe unsafe) {
this.unsafe = unsafe;
try {
offset = unsafe.objectFieldOffset(CASCounter.class.getDeclaredField("counter"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}

public void increment() {
long before = counter;
while (!unsafe.compareAndSwapLong(this, offset, before, before + 1)) {
before = counter;
}
}


public long getCounter() {
return counter;
}
}

counter 需要声明为 volatile 来保证对所有线程可见(参考 Java 内存模型以及指令集重排)

这里的关键是 increment 方法,我们在 while 循环里不断尝试调用 compareAndSwapLong,检查在我们方法内部累加的同事,counter 的值有没有被其他线程改变。如有没有,就提交更改,如果不一致,那么继续尝试提交更改。

测试用例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void testCAS() {
int NUM_OF_THREADS = 1_000;
int NUM_OF_INCREMENTS = 10_000;
ExecutorService service = Executors.newFixedThreadPool(NUM_OF_THREADS);

IntStream.rangeClosed(0, NUM_OF_THREADS - 1)
.forEach(i -> service.submit(
() -> IntStream
.rangeClosed(0, NUM_OF_INCREMENTS - 1)
.forEach(j -> casCounter.increment())
)
);
try {
Thread.sleep(5_000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
assertEquals(NUM_OF_INCREMENTS * NUM_OF_THREADS, casCounter.getCounter());
}

由于累加操作需要计算时间,我们这里暴力休眠 5s 后再验证结果。

7. Park/Unpark

Park/Unpark 主要是被 JVM 用来做线程的上下文切换。当线程需要等待某个条件的时候,JVM 会调用 park 来阻塞该线程。

这和 Object.await 非常类似,但是 park 是操作系统调用,因此,在某些操作系统架构上,这会带来更好的性能。

当线程阻塞后,需要再次执行, JVM 会调用 unpark 方法使得该线程变得活跃。

结论

俗话说,面试造核弹,工作拧螺丝,虽然 Unsafe 看起来不会被用到,但是能帮助我们更好的理解 JVM 以及 JDK 中 lock-free 的实现。还有一点就是 Off-Heap Memory, 如果做服务端开发中确实遇到了大内存对象并且常驻内存的情况,堆外分配不失为一个好的策略来减轻 GC 以及 GC 带来的系统负担(可参见 R 大在阿里 JVM 中所做的一些优化),与之对应的就是 TLAB(thread local allocation buffer),后面有机会再整理。