参考文档:
浅析 Unsafe 的使用
干掉Random:这个类已经成为获取随机数的王者
1. Unsafe 简介
Unsafe 是 java 留给开发者的后门,用于直接操作系统内存且不受 jvm 管辖,实现类似 C++ 风格的操作。
Oracle 官方一般不建议开发者使用 Unsafe 类,因为正如这个类的类名一样,它并不安全,使用不当会造成内存泄露。
在平时的业务开发中,这个类基本是不会有接触到的,但是在 java 的并发包和众多偏向底层的框架中,都有大量应用。
值得一提的是,该类的大部分方法均为 native 修饰,即为直接调用的其它语言(大多为 C++)编写的方法来进行操作,很多细节无法追溯,只能大致了解。
2. 对象的获取
在 jdk9+, 我们能搜到两个 Unsafe。
dk8 中的 Unsafe 在包路径 sun.misc 下,引用全名 sun.misc.Unsafe。而在 jdk9 中,官方在 jdk.internal.misc 包下又增加了一个 Unsafe 类,引用全名 jdk.internal.misc.Unsafe。
从代码量和注释量上来说,jdk.internal.misc.Unsafe 要丰富一些,但根据 jdk9 模块的概念来说,这个类不能在外部使用(只暴露给固定模块)。
所以我们使用 sun.misc.Unsafe。
我们查看源码,发现 Unfase 有一个静态方法,理论上我们可以这样获取对象。
Unsafe unsafe = Unsafe.getUnsafe();
但实际会抛异常,我们通过反射机制获取
try {
// 获取 Unsafe 内部的私有的实例化单例对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 无视权限
field.setAccessible(true);
unsafe = (Unsafe) field.get(null);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
3. 类方法介绍
sun.misc.Unsafe 是 jdk 中一直存在的 Unsafe,一般的第三方库的实现会使用该类。
该类在 jdk9 之后移动到了 jdk.unsupported 模块中。
在 jdk11 中,该类的 api 实现很有意思:
// sun.misc.Unsafe.class
@ForceInline
public void putInt(Object o, long offset, int x) {
theInternalUnsafe.putInt(o, offset, x);
}
@ForceInline
public Object getObject(Object o, long offset) {
return theInternalUnsafe.getObject(o, offset);
}
@ForceInline
public void putObject(Object o, long offset, Object x) {
theInternalUnsafe.putObject(o, offset, x);
}
@ForceInline
public boolean getBoolean(Object o, long offset) {
return theInternalUnsafe.getBoolean(o, offset);
}
@ForceInline
public void putBoolean(Object o, long offset, boolean x) {
theInternalUnsafe.putBoolean(o, offset, x);
}
...
此处仅举部分例子,在这个 Unsafe 类中,大多数的实现都调用了 theInternalUnsafe 这个对象的相关方法。
而这个对象,则是一个 jdk.internal.misc.Unsafe 对象:
// sun.misc.Unsafe.class
private static final jdk.internal.misc.Unsafe theInternalUnsafe = jdk.internal.misc.Unsafe.getUnsafe();
在 java.base 的 module-info.class 中笔者也看到了这样的配置:
// java.base 模块下的 module-info.class
exports jdk.internal.misc to // jdk.internal.misc 是 jdk.internal.misc.Unsafe 所在的包路径
java.desktop,
java.logging,
java.management,
java.naming,
java.net.http,
java.rmi,
java.security.jgss,
java.sql,
java.xml,
jdk.attach,
jdk.charsets,
jdk.compiler,
jdk.internal.vm.ci,
jdk.jfr,
jdk.jlink,
jdk.jshell,
jdk.net,
jdk.scripting.nashorn,
jdk.scripting.nashorn.shell,
jdk.unsupported; // jdk.unsupported 是 sun.misc.Unsafe 所在的模块
可见,java.base 只是将该类所在的包路径开放给了有限的几个模块,而没有完全开放给广大开发者。
看到此处,大致可以猜想,Oracle 应该是希望使用 jdk.internal.misc.Unsafe 作为真正的 Unsafe 使用,但是为了兼容性考虑保留了 sun.misc.Unsafe。
并且其实从 api 来说,jdk.internal.misc.Unsafe 的数量更多,权限更大;sun.misc.Unsafe 则比较有限。
在这里说一些题外话,从 jdk.unsupported 这个模块名可以看出,Oracle 确实不太希望开发者使用该模块内的类,甚至 Oracle 在未来的版本里是有可能完全封闭 Unsafe 的使用的,早在 jdk9 时期就有类似传闻。
4. 内存
在 Unsafe 中可以直接申请一块内存:
// 需要传入一个 long 类型的参数,作为申请的内存的大小,单位为 byte
// 返回这块内存的 long 类型地址
long memoryAddress = unsafe.allocateMemory(8);
Unsafe 申请的内存不在 jvm 管辖范围内,需要手动释放:
//传入之前申请的内存的地址就可以释放该块内存了
unsafe.freeMemory(memoryAddress);
注意,如果申请了内存,但是中途报错导致中断了代码执行,没有执行内存的释放,就出现了内存泄漏。所以为了保险起见,实际生产中尽量在 finally 区域里进行内存的释放操作。
还有一个重新分配内存的方法:
// 传入之前申请的内存的地址和一个 long 类型的参数作为新的内存的 byte 大小
// 此方法会释放掉之前地址的内存,然后重新申请一块符合要求大小的内存
// 如果之前那块内存上已经存在对象了,就会被拷贝到新的内存上
long newMemoryAddress = unsafe.reallocateMemory(memoryAddress, 32);
5. 存取对象
Unsafe 中有数量众多的 put 和 get 方法,用于将对象存入内存或者从内存中获取值。原理类似,可以选取几个来进行理解:
// 将 int 型整数 5 存入到指定地址中
unsafe.putInt(menmoryAddress,5);
// 根据地址获取到整数
int a = unsafe.getInt(menmoryAddress);
// 打印,得到 5
System.out.println(a);
这是最基本的 putInt 和 getInt 的运用,除此之外还有 putLong/getLong、putByte/getByte 等等,覆盖了几个基本类型。
但是 put 和 get 方法还有一套常用的重载方法,在这里先借助一个 bean 进行测试:
class UnsafeBean{
// 测试1 测试 static 修饰的 int 类型的存取
private static int staticInt = 5;
// 测试2 测试 static 修饰的 object 类型的存取
private static String staticString = "static_string";
// 测试3 测试 final 修饰的 int 类型的存取
private final int finalInt = 5;
// 测试4 测试 final 修饰的 object 类型的存取
private final String finalString = "final_string";
// 测试5 测试一般的 int 类型的存取
private int privateInt;
// 测试6 测试一般的 object 类型的存取
private String privateString;
}
测试内容:
UnsafeBean bean = new UnsafeBean();
// 1 测试 staticInt
// 先通过变量名反射获取到该变量
Field staticIntField = UnsafeBean.class.getDeclaredField("staticInt");
//无视权限
staticIntField.setAccessible(true);
//staticFieldOffset(...) 方法能够获取到类中的 static 修饰的变量
long staticIntAddress = unsafe.staticFieldOffset(staticIntField);
// 使用 put 方法进行值改变,需要传入其所在的 class 对象、内存地址和新的值
unsafe.putInt(UnsafeBean.class,staticIntAddress,10);
// 使用 get 方法去获取值,需要传入其所在的 class 对象和内存地址
int stiatcIntTest = unsafe.getInt(UnsafeBean.class,staticIntAddress);
// 此处输出为 10
System.out.println(stiatcIntTest);
// 2 测试 staticString
// 基本流程相同,只是 put 和 get 方法换成了 getObject(...) 和 putObject(...)
Field staticStringField = UnsafeBean.class.getDeclaredField("staticString");
staticStringField.setAccessible(true);
long staticStringAddress = unsafe.staticFieldOffset(staticStringField);
unsafe.putObject(UnsafeBean.class,staticStringAddress,"static_string_2");
String staticStringTest = (String)unsafe.getObject(UnsafeBean.class,staticStringAddress);
/// 此处输出为 static_string_2
System.out.println(staticStringTest);
// 3 测试 finalInt
// 基本流程相同,只是 staticFieldOffset(...) 方法换成了 objectFieldOffset(...) 方法
Field finalIntField = UnsafeBean.class.getDeclaredField("finalInt");
finalIntField.setAccessible(true);
long finalIntAddress = unsafe.objectFieldOffset(finalIntField);
// 需要注意的是,虽然该变量是 final 修饰的,理论上是不可变的变量,但是 unsafe 是具有修改权限的
unsafe.putInt(bean,finalIntAddress,10);
int finalIntTest = unsafe.getInt(bean,finalIntAddress);
// 此处输出为 10
System.out.println(finalIntTest);
// 4 测试 finalString
Field finalStringField = UnsafeBean.class.getDeclaredField("finalString");
finalStringField.setAccessible(true);
long finalStringAddress = unsafe.objectFieldOffset(finalStringField);
unsafe.putInt(bean,finalStringAddress,"final_string_2");
String finalStringTest = (String)unsafe.getObject(bean,finalStringAddress);
/// 此处输出为 final_string_2
System.out.println(finalStringTest);
// 测试5 和 测试6 此处省略,因为和上述 final 部分的测试代码一模一样
put 和 get 方法还有一组很类似的 api,是带 volatile 的:
public int getIntVolatile(Object o, long offset);
public void putIntVolatile(Object o, long offset, int x);
public Object getObjectVolatile(Object o, long offset);
public void putObjectVolatile(Object o, long offset, Object x);
...
这一组 api 的使用方式和上述一样,只是增加了对 volatile 关键词的支持。测试发现,该组 api 也支持不使用 volatile 关键词的变量。
get 和 put 方法的思路都比较简单,使用思路可以归纳为:
- 用反射获取变量对象 (getDeclaredField)
- 开放权限,屏蔽 private 关键字的影响 (setAccessible(true))
- 调用相关方法获取到该对象中的该变量对象的内存地址 (staticFieldOffset/objectFieldOffset)
- 通过内存地址去修改该对象的值 (putInt/putObject)
- 获取对象的值 (getInt/getObject)
6. 线程挂起和恢复
线程的挂起调用 park(...) 方法:
// 该方法第二个参数为 long 类型对象,表示该线程准备挂起到的时间点
// 注意,此为时间点,而非时间,该时间点从 1970 年(即元年)开始
// 第一个参数为 boolean 类型的对象,用来表示挂起时间的单位,true 表示毫秒,false 表示纳秒
// 第一个参数为 true,第二个参数为 0 的时候,线程会直接返回,不太清楚机理
unsafe.park(false,0L);
与之对应的 unpark(...) 方法:
// 此处传入线程对象
unsafe.unpark(thread);
请注意,挂起时是不需要传入线程对象的,即只有线程自身可以执行此方法用于挂起自身,但是恢复方法是需要其它线程来帮助恢复的。
7. CAS
Unsafe 中提供了一套原子化的判断和值替换 api,来看一下例子:
// 创建一个 Integer 对象,value 为 1
Integer i = 1;
// 获取到内部变量 value,这个变量用于存放值
Field valueField = Integer.class.getDeclaredField("value");
valueField.setAccessible(true);
// 获取到内存偏移量值
long valueAddress = unsafe.objectFieldOffset(valueField);
// getIntVolatile 使用偏移量获取到对象内指定变量的值
int originValue_1 = unsafe.getIntVolatile(i,valueAddress);
// getIntVolatile 获取 int 的值
// getDoubleVolatile 获取 double 的值
// getLongVolatile 获取 long 的值
// getObjectVolatile 获取 object 的值
// getAndAddInt 方法和 getIntVolatile 一样会获取到变量的值,同时会加上一个值
int originValue_2 = unsafe.getAndAddInt(i,valueAddress,5);
// getAndAddInt 获取值并加上一个 int 类型的整数
// getAndAddLong 获取值并加上一个 long 类型的整数
// getAndAddInt 方法和 getIntVolatile 一样会获取到变量的值,同时会替换一个值
int originValue_3 = unsafe.getAndSetInt(i,valueAddress,5);
// 该方法用户比较及替换值
// 第一个参数为要替换的对象本身,第二个参数为值的内存地址
// 第三个参数为变量的预期值,第四个参数为变量要换的值
// 如果变量目前的值等于预期值(第三个参数),则会将变量的值换成新值(第四个参数),返回 true
// 如果不等于预期,则不会改变,并返回 false
boolean isOk = unsafe.compareAndSwapInt(i,valueAddress,1,5);
//此处输出 true
System.out.println(isOk);
//此处输出 5
System.out.println(i);
8. 不安全性
作为 Unsafe 类内的方法,它也透露着一股 “Unsafe” 的气息,具体表现就是可以直接操作内存,而不做任何安全校验,如果有问题,则会在运行时抛出 Fatal Error
,导致整个虚拟机的退出。
在我们的常识里,get 方法是最容易抛异常的地方,比如空指针、类型转换等,但 Unsafe.getLong() 方法是个非常安全的方法,它从某个内存位置开始读取四个字节,而不管这四个字节是什么内容,总能成功转成 long 型,至于这个 long 型结果是不是跟业务匹配就是另一回事了。而 set 方法也是比较安全的,它把某个内存位置之后的四个字节覆盖成一个 long 型的值,也几乎不会出错。
那么这两个方法”不安全”在哪呢?
它们的不安全并不是在这两个方法执行期间报错,而是未经保护地改变内存,会引起别的方法在使用这一段内存时报错。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// Unsafe 设置了构造方法私有,getUnsafe 获取实例方法包私有,在包外只能通过反射获取
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
// Test 类是一个随手写的测试类,只有一个 String 类型的测试类
Test test = new Test();
test.ttt = "12345";
unsafe.putLong(test, 12L, 2333L);
System.out.println(test.value);
}
运行上面的代码会得到一个 fatal error,报错信息为 “A fatal error has been detected by the Java Runtime Environment: … Process finished with exit code 134 (interrupted by signal 6: SIGABRT)”。
可以从报错信息中看到虚拟机因为这个 fatal error abort 退出了,原因也很简单,我使用 unsafe 将 Test 类 value 属性的位置设置成了 long 型值 2333,而当我使用 value 属性时,虚拟机会将这一块内存解析为 String 对象,原 String 对象对象头的结构被打乱了,解析对象失败抛出了错误,更严重的问题是报错信息中没有类名行号等信息,在复杂项目中排查这种问题真如同大海捞针。
不过 Unsafe 的其他方法可不一定像这一对方法一样,使用他们时可能需要注意另外的安全问题。