浅析 Unsafe 的使用

1 Unsafe 简介

Unsafe 是 java 留给开发者的后门,用于直接操做系统内存且不受 jvm 管辖,实现相似 C++ 风格的操做。java

Oracle 官方通常不建议开发者使用 Unsafe 类,由于正如这个类的类名同样,它并不安全,使用不当会形成内存泄露。sql

在平时的业务开发中,这个类基本是不会有接触到的,可是在 java 的并发包和众多偏向底层的框架中,都有大量应用。shell

值得一提的是,该类的大部分方法均为 native 修饰,即为直接调用的其它语言(大多为 C++)编写的方法来进行操做,不少细节没法追溯,只能大体了解。api

一 Unsafe 的获取

jdk8 中的 Unsafe 在包路径 sun.misc 下,引用全名 sun.misc.Unsafe。而在 jdk9 中,官方在 jdk.internal.misc 包下又增长了一个 Unsafe 类,引用全名 jdk.internal.misc.Unsafe。安全

这两个 Unsafe 的构造方法均被 private 修饰,且类中有一个自身的静态实例对象,即经典的单例模式实现,而且提供了 getUnsafe() 方法调用:网络

Unsafe unsafe = Unsafe.getUnsafe();

可是其实这个方法是没法在平常开发中使用的,具体的等下分析。并发

1 jdk.internal.misc.Unsafe

从代码量和注释量上来讲,jdk.internal.misc.Unsafe 比另外一者要丰富一些。框架

可是笔者尝试以后发现该类应该是没法直接在代码中使用的。jvm

该类位于 java.base 模块下,根据一些网络资料,笔者尝试在 idea 的 compiler.xml 文件中导入了该模块:maven

--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED

以及在 maven 的 pom.xml 中加入该模块:

<compilerArgs>
    <arg>--add-exports=java.base/jdk.internal.misc=ALL-UNNAMED</arg>
</compilerArgs>

任然没法使用该类,启动报错:

Exception in thread "main" java.lang.IllegalAccessError: class test.jdk.UnsafeTest (in unnamed module @0x57829d67) cannot access class jdk.internal.misc.Unsafe (in module java.base) because module java.base does not export jdk.internal.misc to unnamed module @0x57829d67

有一些大神在博文中提到在 jdk9 中可使用导入模块去使用该类,笔者未作尝试。

可能有一些别的蹊径可使用该类,可是笔者对于 jdk 中的模块系统也不算特别熟悉,本题是研究 Unsafe 的使用,因此这部分暂时很少研究了。

2 sun.misc.Unsafe

sun.misc.Unsafe 是 jdk 中一直存在的 Unsafe,通常的第三方库的实现会使用该类。

该类在 jdk9 以后移动到了 jdk.unsupported 模块中。

在 jdk.unsupported 模块的 module-info.class 中能够看到:

//jdk.unsupported 模块下的 module-info.class
module jdk.unsupported {
    exports com.sun.nio.file;
    exports sun.misc; //sun.misc.Unsafe 所在的路径
    exports sun.reflect;

    opens sun.misc;
    opens sun.reflect;
}

也就是说该模块将该类开放了出来,其它应用可使用该类。

在 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 时期就有相似传闻。可是笔者站在一个普通开发者的角度,其实不太但愿这样的状况出现,由于笔者认为 Oracle 做为 java 的标准制定者,应该给 java 留下足够的自由度,让开发者可以充分发挥聪明才智开发出更强大的轮子,成熟的开发者应该能为本身的行为负责,而不须要官方摆出一幅 我来手把手教你 的模样。】

3 Unsafe 对象获取

因为 jdk.internal.misc.Unsafe 没法使用,因此如下均使用 sun.misc.Unsafe 来作 demo。

以前提到了 Unsafe 类的 getUnsafe() 静态获取单例的方法,可是其实那个方法是不对普通开发者开放的,笔者尝试使用以后报错:

Exception in thread "main" java.lang.SecurityException: Unsafe

笔者查看了一些第三方库对 Unsafe 的使用,也确实不会直接使用该方式,而是使用反射机制去获取该类:

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

二 内存

在 Unsafe 中能够直接申请一块内存:

//须要传入一个 long 类型的参数,做为申请的内存的大小,单位为 byte
//返回这块内存的 long 类型地址
long memoryAddress = unsafe.allocateMemory(8);

Unsafe 申请的内存不在 jvm 管辖范围内,须要手动释放:

//传入以前申请的内存的地址就能够释放该块内存了
unsafe.freeMemory(memoryAddress);

注意,若是申请了内存,可是中途报错致使中断了代码执行,没有执行内存的释放,就出现了内存泄漏。因此为了保险起见,实际生产中尽可能在 finally 区域里进行内存的释放操做。

还有一个从新分配内存的方法:

//传入以前申请的内存的地址和一个 long 类型的参数做为新的内存的 byte 大小
//此方法会释放掉以前地址的内存,而后从新申请一块符合要求大小的内存
//若是以前那块内存上已经存在对象了,就会被拷贝到新的内存上
long newMemoryAddress = unsafe.reallocateMemory(memoryAddress, 32);

三 存取对象

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 方法的思路都比较简单,使用思路能够概括为:

1 用反射获取变量对象 (getDeclaredField)
2 开放权限,屏蔽 private 关键字的影响 (setAccessible(true))
3 调用相关方法获取到该对象中的该变量对象的内存地址 (staticFieldOffset/objectFieldOffset)
4 经过内存地址去修改该对象的值 (putInt/putObject)
5 获取对象的值 (getInt/getObject)

四 线程的挂起和恢复

线程的挂起调用 park(…) 方法:

//该方法第二个参数为 long 类型对象,表示该线程准备挂起到的时间点
//注意,此为时间点,而非时间,该时间点从 1970 年(即元年)开始
//第一个参数为 boolean 类型的对象,用来表示挂起时间的单位,true 表示毫秒,false 表示纳秒
//第一个参数为 true,第二个参数为 0 的时候,线程会直接返回,不太清楚机理
unsafe.park(false,0L);

与之对应的 unpark(…) 方法:

//此处传入线程对象
unsafe.unpark(thread);

请注意,挂起时是不须要传入线程对象的,即只有线程自身能够执行此方法用于挂起自身,可是恢复方法是须要其它线程来帮助恢复的。

五 CAS

Unsafe 中提供了一套原子化的判断和值替换 api,来看一下例子:

//建立一个 Integer 对象,value 为 1
Integer i = 1;

//获取到内部变量 value,这个变量用于存放值
Field valueField = Integer.class.getDeclaredField("value");
valueField.setAccessible(true);

//获取到内存地址
long valueAddress = unsafe.objectFieldOffset(valueField);

//该方法用户比较及替换值
//第一个参数为要替换的对象自己,第二个参数为值的内存地址
//第三个参数为变量的预期值,第四个参数为变量要换的值
//若是变量目前的值等于预期值(第三个参数),则会将变量的值换成新值(第四个参数),返回 true
//若是不等于预期,则不会改变,并返回 false
boolean isOk = unsafe.compareAndSwapInt(i,valueAddress,1,5);

//此处输出 true
System.out.println(isOk);
//此处输出 5
System.out.println(i);

六 一点唠叨

Unsafe 的 api 众多,可是网络资料很少,且功能较为晦涩,不太好写 demo。可是在近期学习 jdk 并发包的时候常常会接触到,因此在此先记录一些看到过的方法的具体应用。其它的有缘补充。

相关文章
相关标签/搜索