性能文章>Unsafe那些事>

Unsafe那些事原创

https://a.perfma.net/img/2521381
1年前
7178317

简介

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于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.
https://docs.oracle.com/javase/specs/jls/se7/html/jls-10.html

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

比如下面的一段JOL输出,实际上对象的属性数据是从OFFSET 16的位置开始的,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之后的操作方式一样**

请先登录,查看3条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

不起眼,但是足以让你有收获的JVM内存分析案例
分析 这个问题说白了,就是说有些int[]对象不知道是哪里来的,于是我拿他的例子跑了跑,好像还真有这么回事。点该 dump 文件详情,查看相关的 int[] 数组,点该对象的“被引用对象”,发现所
从一起GC血案谈到反射原理
前言 首先回答一下提问者的问题。这主要是由于存在大量反射而产生的临时类加载器和 ASM 临时生成的类,这些类会被保留在 Metaspace,一旦 Metaspace 即将满的时候,就会触发 Fu
关于内存溢出,咱再聊点有意思的?
概述 上篇文章讲了JVM在GC上的一个设计缺陷,揪出一个导致GC慢慢变长的JVM设计缺陷,可能有不少人还是没怎么看明白的,今天准备讲的大家应该都很容易看明白 本文其实很犹豫写不写,因为感觉没有
协助美团kafka团队定位到的一个JVM Crash问题
概述 有挺长一段时间没写技术文章了,正好这两天美团kafka团队有位小伙伴加了我微信,然后咨询了一个JVM crash的问题,大家对crash的问题都比较无奈,因为没有现场,信息量不多,碰到这类问题我
又发现一个导致JVM物理内存消耗大的Bug(已提交Patch)
概述 最近我们公司在帮一个客户查一个JVM的问题(JDK1.8.0_191-b12),发现一个系统老是被OS Kill掉,是内存泄露导致的。在查的过程中,阴差阳错地发现了JVM另外的一个Bug。这个B
JVM实战:优化我的IDEA GC
IDEA是个好东西,可以说是地球上最好的Java开发工具,但是偶尔也会卡顿,仔细想想IDEA也是Java开发的,会不会和GC有关,于是就有了接下来对IDEA的GC进行调优 IDEA默认JVM参数: -
不起眼,但是足以让你收获的JVM内存案例
今天的这个案例我觉得应该会让你涨姿势吧,不管你对JVM有多熟悉,看到这篇文章,应该还是会有点小惊讶的,不过我觉得这个案例我分享出来,是想表达不管多么奇怪的现象请一定要追究下去,会让你慢慢变得强大起来,
如何通过反射获得方法的真实参数名(以及扩展研究)
前段时间,在做一个小的工程时,遇到了需要通过反射获得方法真实参数名的场景,在这里我遇到了一些小小的问题,后来在部门老大的指导下,我解决了这个问题。通过解决这个问题,附带着我了解到了很多新的知识,我觉得