Java 的 Unsafe 类
/ / 点击 / 阅读耗时 13 分钟本文主要介绍一下 JDK 里的核弹之 sun.misc.Unsage
类。
0. 概述
本文主要介绍一下 JDK 里的核弹之 sun.misc.Unsage
类。这个类提供了更加底层的操作,主要是服务于 Java 的核心类库。普通用户一般情况下,并不会直接用到该类。
1. 获取 Unsafe
实例
Unsafe
提供了 getUnsafe
方法,代码实例如下所示:
1 | public class Main { |
但是,会有如下报错:
1 | Exception in thread "main" java.lang.SecurityException: Unsafe |
这是因为 Unsafe
类主要是 JDK 内部使用,并不提供给普通用户调用,也就是其名字所暗示的那样,这些操作不安全。
但是我们仍让可以通过反射获取到实例:
1 | public static Unsafe createUnsafe() { |
2. 不调用构造方法方法创建对象
我们知道,JVM 在创建对象实例的时候会调用默认或者有参构造方法,在字节码中对应 init
方法,那如何实例话一个对象,而不调用 init
方法呢? 答案就是 Unsafe
。
假设我们有一个类 Person
:
1 | private String name; |
那么我们可以通过 Unsafe
在内存中直接创建 Person
的实例:
1 | public Person instancePerson() { |
Unsafe
只会分配 Person
对应的内存空间,而不触发构造函数。
注: Class 文件中的 clinit
仍然会执行。
举一个生产实例。Gson
中反序列化的时候,ReflectiveTypeAdapterFactory
类负责通过反射设置字段值,其中在或许反序列化对应的 Class 实例的时候就用到了 Unsafe
, 关键代码摘抄如下:
1 | public abstract class UnsafeAllocator { |
3.改变私有字段
假设我们有如下类:
1 | public class SecretHolder { |
下面我们通过 Unsafe
来改变私有属性 SECRET_VALUE
的值。
1 | SecretHolder secretHolder = new SecretHolder(); |
我们通过 unsafe.putInt
直接改变了 SecretHolder
的私有属性的值。一旦我们通过反射获得了类的私有属性字段,我们就可以直接操作它的值。
4. 抛出异常而不会触发 CE
(Checked Exception)
通过 unsafe.throwExceptio
创建的异常不会被编译器检查,方法的调用者也不需要处理异常。
1 | public void throwException() { |
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
28public 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 | long SUPER_SIZE = (long) Integer.MAX_VALUE * 2; |
测试用例如下:
1 |
|
一定不要忘了,在合适的时候释放内存。
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
26public 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 |
|
由于累加操作需要计算时间,我们这里暴力休眠 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),后面有机会再整理。