阅读 240

JDK中为了性能大量使用的Unsafe类,你会用吗?

简介

Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用(比如JUC/NIO)。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再 “安全”,因此对 Unsafe 的使用一定要慎重。

获取 Unsafe 实例

private static sun.misc.Unsafe getUnsafe() {
        try {
            return AccessController.doPrivileged(new PrivilegedExceptionAction<Unsafe>() {
                @Override
                public sun.misc.Unsafe run() throws Exception {
                    Class<sun.misc.Unsafe> k = sun.misc.Unsafe.class;
                    for (Field f : k.getDeclaredFields()) {
                        f.setAccessible(true);
                        Object x = f.get(null);
                        if (k.isInstance(x)) {
                            return k.cast(x);
                        }
                    }
                    // The sun.misc.Unsafe field does not exist.
                    throw new Error("unsafe is null");
                }
            });
        } catch (Throwable e) {
            throw new Error("get unsafe failed", e);
        }
    }
复制代码

Unsafe 功能列表

  • allocateMemory/freeMemory,分配、释放堆外内存 DirectMemory(和 c/cpp 中的 malloc 一样)
  • CAS 操作
  • copyMemory
  • defineClass(without security checks)
  • get/put address 使用堆外内存地址进行数据的读写操作
  • get/put volatile 使用堆外内存地址进行数据的读写操作 - volatile 版本
  • loadFence/storeFence/fullFence 禁止指令重排序
  • park/unpark 阻塞 / 解除阻塞线程

Unsafe 的数组操作

unsafe 中,有两个关于数组的方法:

public native int arrayBaseOffset(Class<?> arrayClass);
public native int arrayIndexScale(Class<?> arrayClass);
复制代码

base offset 含义

首先,在 Java 中,数组也是对象

In the Java programming language, arrays are objects (§4.3.1), are dynamically created, and may be assigned to variables of type Object (§4.3.2). All methods of class Object may be invoked on an array.
docs.oracle.com/javase/spec…

那么既然是对象,就会有 object header,占用一部分空间,那么理解数组的 base offset 也就不难了

比如下面的一段 JOL 输出,实际上对象的属性数据是从 OFFSET 12 的位置开始的,0-12 的空间被 header 所占用

HotSpot 64-bit VM, COOPS, 8-byte alignment

lambda.Book object internals:
 OFFSET  SIZE           TYPE DESCRIPTION                               VALUE
      0    12                (object header)                           N/A
     12     4            int Book.sales                                N/A
     16     4         String Book.title                                N/A
     20     4      LocalDate Book.publishTime                          N/A
     24     4         String Book.author                               N/A
     28     4   List<String> Book.tags                                 N/A
Instance size: 32 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
复制代码

那么如果要访问对象的属性数据,需要基于基地址(base address)进行偏移,基地址 + 基础偏移(base offset)+ 属性偏移(field offset)才是数据的内存地址(逻辑),那么 title 属性的内存地址实际上就是:

book(instance).title field address = book object base address + base offset + title field offset
复制代码

数组在 Java 里可以视为一种特殊的对象,无论什么类型的数组,他们在内存中都会有一分基础偏移的空间,和 object header 类似

经过测试,64 位的 JVM 数组类型的基础偏移都是 16(测试结果在不同 JVM 下可能会有所区别)
原始类型的基础偏移都是 12(测试结果在不同 JVM 下可能会有所区别)
可以使用 JOL 工具查看原始类型包装类的 offset,比如 int 就看 Integer 的,虽然 jdk 没有提供函数,但是通过 JOL 也是可以获取的,jvm 类型和对其方式匹配即可

index scale 含义

就是指数组中每个元素所占用的空间大小,比如 int[] scale 就是 4,long[] scale 就是 8,object[] scale 就是 4(指针大小)

有了这个 offset,就可以对数组进行 copyMemory 操作了

public native void copyMemory(Object srcBase, long srcOffset,
                                  Object destBase, long destOffset,
                                  long bytes);
复制代码

array copy to direct memory

在使用 copyMemory 操作时,需要传入对象及对象的 base offset,对于数组来说,offset 就是上面介绍的 offset,比如现在将一个数组中的数据拷贝至 DirectBuffer

byte[] byte = new byte[4096];
unsafe.copyMemory(byte,ARRAY_BYTE_BASE_OFFSET,null,directAddr,4096);
复制代码

之所以使用 unsafe 而不是 ByteBuffer 的方法来操作 DirectBuffer,是因为 ByteBuffer 不够灵活。

比如我想把一个 byte[] 拷贝至 DirectBuffer 的某个位置中,就没有相应的方法;只能先设置 position,然后再 put(byte[], int, int),非常麻烦,而且并发访问时 position(long) 和 put 也是个非原子性操作

但是用 unsafe 来操作的话就很轻松了,直接 copyMemory,直接指定 address + offset 就行

unsafe.copyMemory(byte,ARRAY_BYTE_BASE_OFFSET,null,directAddr,4096);
复制代码

copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes)

  • srcBase 原数据对象,可以是对象、数组(对象),也可以是 Null(为 Null 时必须指定 offset,offset 就是 address)
  • srcOffset 原数据对象的 base offset,如果 srcBase 为空则此项为 address
  • destBase 目标数据对象,规则同 src
  • destOffset 目标数据对象的 base offset,规则同 src
  • bytes 要拷贝的数据大小(字节单位)

通过 copyMemory 方法,可以做各种拷贝操作:

对象(一般是数组)拷贝到指定堆外内存地址

long l = unsafe.allocateMemory(1);
data2[0] = 5;
//目标是memory address,destBase为null,destOffset为address
unsafe.copyMemory(data2,16,null,l,1);
复制代码

将对象拷贝到对象

byte[] data1 = new byte[1];
data1[0] = 9;

byte[] data2 = new byte[1];
unsafe.copyMemory(data1,16,data2,16,1);
复制代码

将堆外内存地址的数据拷贝到堆内(一般是数组)

byte[] data2 = new byte[1];

long l = unsafe.allocateMemory(1);
unsafe.putByte(l, (byte) 2);
//源数据是memory address,srcBase为null,srcOffset为address
unsafe.copyMemory(null,l,data2,16,1);
复制代码

堆外内存地址互相拷贝

long l = unsafe.allocateMemory(1);
long l2 = unsafe.allocateMemory(1);
unsafe.putByte(l, (byte) 2);
//源数据是memory address,srcBase为null,srcOffset为address
//目标是memory address,destBase为null,destOffset为address
unsafe.copyMemory(null,l,null,l2,1);
复制代码

Benchmark

sun.misc.Unsafe#putInt(java.lang.Object, long, int) & object field manual set

禁用JIT结果:
Benchmark                           Mode  Cnt        Score   Error   Units
ObjectFieldSetBenchmark.manualSet  thrpt    2  8646455.472          ops/ns
ObjectFieldSetBenchmark.unsafeSet  thrpt    2  7901066.170          ops/ns
复制代码
启用JIT结果:
Benchmark                           Mode  Cnt          Score   Error   Units
ObjectFieldSetBenchmark.manualSet  thrpt    2  477232013.545          ops/ns
ObjectFieldSetBenchmark.unsafeSet  thrpt    2  499135982.962          ops/ns
复制代码

在本机环境下,测试的结果区别不大,服务器环境下应该会有更大的差距

什么时候用 Unsafe

一般使用 DirectBuffer 时,需要配合 Unsafe 来使用,因为 DirectBuffer 的内存是分配在 JVM Heap 之外的,属于 C Heap,所以需要用直接操作内存地址(逻辑),和 C 里 malloc 之后的操作方式一样

或者一些追求极致效率的代码,在JUC或者NIO中,也有很多使用Unsafe的地方

原创不易,转载请在开头著名文章来源和作者。如果我的文章对您有帮助,请点赞收藏鼓励支持。

文章分类
后端
文章标签