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

Unsafe 类被设计为单例实现,提供了静态方法 getUnsafe 获取 Unsafe 实例,为了保证不被滥用,当且仅当调用 getUnsafe 方法的类为引导类加载器所加载时才合法,否则抛出 SecurityException 异常。如果想要在自己的代码中使用这个类,可通过反射绕过权限获取 Unsafe 对象。

  1. private static Unsafe reflectGetUnsafe() {
  2. try {
  3. Field field = Unsafe.class.getDeclaredField("theUnsafe");
  4. field.setAccessible(true);
  5. return (Unsafe) field.get(null);
  6. } catch (Exception e) {
  7. log.error(e.getMessage(), e);
  8. return null;
  9. }
  10. }

Unsafe 提供的 API 大致可分为如下几类:
f182555953e29cec76497ebaec526fd1297846.png

内存操作

这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。

  1. //分配内存, 相当于C++的malloc函数
  2. public native long allocateMemory(long bytes);
  3. //扩充内存
  4. public native long reallocateMemory(long address, long bytes);
  5. //释放内存
  6. public native void freeMemory(long address);
  7. //在给定的内存块中设置值
  8. public native void setMemory(Object o, long offset, long bytes, byte value);
  9. //内存拷贝
  10. public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
  11. //获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等
  12. public native Object getObject(Object o, long offset);
  13. //为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
  14. public native void putObject(Object o, long offset, Object x);
  15. //获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
  16. public native byte getByte(long address);
  17. //为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
  18. public native void putByte(long address, byte x);

通常,我们在 Java 中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM 所管理内存空间,JVM 会采用垃圾回收机制统一管理堆内内存。与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法。

典型的应用场景便是 DirectByteBuffer,通常在网络 IO 中用做缓冲池,以减少一次堆内内存到堆外内存的数据拷贝操作。创建 DirectByteBuffer 时,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存通过 Unsafe.freeMemory 一起被释放。

CAS

  1. /**
  2. * @param o 包含要修改field的对象
  3. * @param offset 对象中某field的偏移量
  4. * @param expected 期望值
  5. * @param update 更新值
  6. * @return true | false
  7. */
  8. public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
  9. public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
  10. public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

CAS 操作包含三个操作数:内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。CAS 是一条 CPU 的原子指令,所以可以保证原子性,不会造成所谓的数据不一致问题。

例如在 AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时通过 Unsafe 的 objectFieldOffset 方法获取。在 AtomicInteger 中提供的线程安全方法中,通过字段 valueOffset 的值可以定位到 AtomicInteger 对象中 value 的内存地址,从而可以利用 CAS 实现对 value 字段的原子操作。
image.png
下图为某个 AtomicInteger 对象实例自增操作前后的内存示意图,对象的基地址 baseAddress=0x110000,通过 baseAddress+valueOffset 得到 value 的内存地址 valueAddress=0x11000c,然后通过 CAS 进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。
6e8b1fe5d5993d17a4c5b69bb72ac51d89826.png

Class 相关

此部分主要提供 Class 和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验以及确保初始化等。

  1. //获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
  2. public native long staticFieldOffset(Field f);
  3. //获取一个静态类中给定字段的对象指针
  4. public native Object staticFieldBase(Field f);
  5. //判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
  6. public native boolean shouldBeInitialized(Class<?> c);
  7. //检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
  8. public native void ensureClassInitialized(Class<?> c);
  9. //定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
  10. public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
  11. //定义一个匿名类
  12. public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);

JDK 8 使用 invokedynamic 及 VM Anonymous Class 结合实现 Java 语言层面上的 Lambda 表达式。

  • invokedynamic 是 Java 7 为实现在 JVM 上运行动态语言而引入的一条新指令,它可以实现在运行期动态解析出调用点限定符所引用的方法,然后再执行该方法,该指令的分派逻辑是由用户设定的引导方法决定。


  • VM Anonymous Class 可以看做是一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类,而后通过 Unsafe.defineAnonymousClass 方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何 ClassLoader 下面,只要当该类没有存在的实例对象、且没有强引用来引用该类的 Class 对象时,该类就会被 GC 回收。故 VM Anonymous Class 相比 Java 语言层面的匿名内部类无需通过 ClassLoader 进行类加载且更易回收。

在 Lambda 表达式实现中,通过 invokedynamic 指令调用引导方法生成调用点,在此过程中,会通过 ASM 动态生成字节码,而后利用 Unsafe 的 defineAnonymousClass 方法定义实现相应的函数式接口的匿名类,然后再实例化此匿名类,并返回与此匿名类中函数式方法的方法句柄关联的调用点;而后可以通过此调用点实现调用相应 Lambda 表达式定义逻辑的功能。下面以如下图所示的 Test 类来举例说明。
7707d035eb5f04314b3684ff91dddb1663516.png
Test 类的字节码如下图一所示,从中可以看到 main 方法的指令实现、invokedynamic 指令调用的引导方法 BootstrapMethods 以及静态方法 lambda$main$0(实现了 Lambda 表达式中字符串打印逻辑)等。在引导方法执行过程中,会通过 Unsafe.defineAnonymousClass 生成如下图二所示的实现 Consumer 接口的匿名类。其中 accept 方法通过调用 Test 类中的静态方法 lambda$main$0 来实现 Lambda 表达式中定义的逻辑。而后执行语句 consumer.accept(”lambda”)其实就是调用下图二所示的匿名类的 accept 方法。
1038d53959701093db6c655e4a342e30456249.png

对象操作

此部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。

  1. //返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
  2. public native long objectFieldOffset(Field f);
  3. //获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等
  4. public native Object getObject(Object o, long offset);
  5. //给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等
  6. public native void putObject(Object o, long offset, Object x);
  7. //从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
  8. public native Object getObjectVolatile(Object o, long offset);
  9. //存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
  10. public native void putObjectVolatile(Object o, long offset, Object x);
  11. //有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
  12. public native void putOrderedObject(Object o, long offset, Object x);
  13. //绕过构造方法、初始化代码来创建对象
  14. public native Object allocateInstance(Class<?> cls) throws InstantiationException;

我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但 new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。

而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提供类对象即可创建相应的对象实例。

如下图所示,在 Gson 反序列化时,如果类有默认构造函数,则通过反射调用默认构造函数创建实例,否则通过 UnsafeAllocator 来构造对象实例,UnsafeAllocator 通过调用 Unsafe 的 allocateInstance 方法实现了对象的实例化,保证在目标类无默认构造函数时,反序列化也不受影响。
b9fe6ab772d03f30cd48009920d56948514676.png

数组相关

这部分主要介绍与数据操作相关的 arrayBaseOffset 与 arrayIndexScale 这两个方法,两者配合起来使用,即可定位数组中每个元素在内存中的位置。

  1. //返回数组中第一个元素的偏移地址
  2. public native int arrayBaseOffset(Class<?> arrayClass);
  3. //返回数组中一个元素占用的大小
  4. public native int arrayIndexScale(Class<?> arrayClass);

这两个与数据操作相关的方法,在 AtomicIntegerArray 中有典型的应用,如下图所示,通过 Unsafe 的 arrayBaseOffset、arrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 getAndAdd 方法即通过 checkedByteOffset 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。
160366b0fb2079ad897f6d6b1cb349cd426237.png

内存屏障

内存屏障相关的方法在 Java 8 中引入,用于定义内存屏障(内存屏障是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作)以避免指令重排序。

  1. //内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
  2. public native void loadFence();
  3. //内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
  4. public native void storeFence();
  5. //内存屏障,禁止load、store操作重排序
  6. public native void fullFence();

在 JDK 8 中引入了一种锁的新机制 StampedLock,它可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程饥饿现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题,所以当使用 StampedLock 的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。
839ad79686d06583296f3abf1bec27e3320222.png
在方法 distanceFromOrigin 中,首先通过 tryOptimisticRead 方法获取乐观读标记,然后从主内存中加载点的坐标值,而后通过 validate 方法校验锁状态,判断坐标点从主内存加载到线程工作内存的过程中,主内存的值是否已被其他线程通过 move 方法修改,如果 validate 返回值 true,证明坐标值未被修改;否则,需加悲观读锁再次从主内存加载坐标的最新值。其中,校验锁状态这步操作至关重要,需要判断锁状态是否发生改变,从而判断线程工作内存中的值是否与主内存的值存在不一致。

下图为 StampedLock.validate 方法的源码实现,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障,目的是避免上图用例中步骤 ② 和 StampedLock.validate 中锁状态校验运算发生重排序导致锁状态校验不准确的问题。
256f54b037d07df53408b5eea9436b34135955.png

系统相关

  1. //返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
  2. public native int addressSize();
  3. //内存页的大小,此值为2的幂次方。
  4. public native int pageSize();

下图所示的代码片段为 java.nio 下的工具类 Bits 中计算待申请内存所需内存页数量的静态方法,其依赖于 Unsafe 中 pageSize 方法获取系统内存页大小实现后续计算逻辑。
262470b0c3e79b8f4f7b0c0280b1cc5362454.png

线程调度

  1. //取消阻塞线程
  2. public native void unpark(Object thread);
  3. //阻塞线程
  4. public native void park(boolean isAbsolute, long time);
  5. //获得对象锁(可重入锁)
  6. @Deprecated
  7. public native void monitorEnter(Object o);
  8. //释放对象锁
  9. @Deprecated
  10. public native void monitorExit(Object o);
  11. //尝试获取对象锁
  12. @Deprecated
  13. public native boolean tryMonitorEnter(Object o);

将一个线程进行挂起是通过 park 方法实现的,调用 park 方法后线程将一直阻塞直到超时或者中断;unpark 可以终止一个挂起的线程使其恢复正常。Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer,就是通过调用 LockSupport.park() 和 LockSupport.unpark() 实现线程的阻塞和唤醒的,而 LockSupport 的 park、unpark 方法实际是调用 Unsafe 的 park、unpark 方式来实现的。