Java后端|Unsafe类及其应用

本文概述Java Unsafe类,并举例说明其应用场景,快速浏览下即可

阅读了美团2019技术年货,有一篇文章是对Java魔法类——Unsafe的讲解。文章不错,在此结合源码作一个总结,并添加个人的一些理解和学习文章资源。

原文链接:美团点评 2019 技术年货 P2

目录

  1. Unsafe类简介
  2. Unsafe类使用
  3. Unsafe类应用

Unsafe类简介

Java作为一种面向对象编程语言,相对于C++,其具有的自动垃圾回收机制大大降低了编程的复杂度,但同时导致性能较低、空间占用大等问题。

为解决上述问题,Java提供了Unsafe类(位于sun.misc包)。该类提供了一些底层原生方法,可直接访问系统内存资源、自主管理内存资源等。通过该类的方法,可以弥补Java这一上层语言的不足,提升程序运行效率以及对系统资源的管控能力。有很多Java工具包和框架都使用了Unsafe类,如java.nio包、java.util.concurrent包、Netty、Kafka、Hadoop等。

但Unsafe类也是一把双刃剑。比如内存分配及回收操作(类似C语言指针),对于适应了JVM自动管理内存的Java程序员来说,很容易出现内存泄漏等问题。因此要对Unsafe抱有敬畏之心。

查看Unsafe类源码中定义的方法,可知其主要功能,如下图:

图片说明

下面首先介绍如何使用Unsafe类。

Unsafe类使用

Unsafe类的方法基本都是实例方法,因此需获取unsafe实例。

查看Unsafe类的源码可知,Unsafe类是饿汉式单例模式的设计,通过静态代码块对单例对象theUnsafe进行实例化,通过getUnsafe静态方法获取实例。

public final class Unsafe {
    // 单例对象
  private static final Unsafe theUnsafe;

  static {
    registerNatives();
    Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
    // 创建实例
    theUnsafe = new Unsafe();
  }

  private Unsafe() {
  }

  @CallerSensitive
  public static Unsafe getUnsafe() {
        ...
  }

  ...
}

因此想要获取Unsafe对象,有如下两种方式:

1. 调用Unsafe方法

阅读源码,发现调用getUnsafe方法的类必须是被BootstrapClassLoader加载的,否则会抛出异常!

@CallerSensitive
public static Unsafe getUnsafe() {
  Class var0 = Reflection.getCallerClass();
  if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
    throw new SecurityException("Unsafe");
  } else {
    return theUnsafe;
  }
}

可通过Java cmd命令的-Xbootclasspath/a参数把调用Unsafe方法的类所在jar包路径追加到默认的Bootstrap路径中,使得该类可被BootstrapClassLoader加载,从而获取Unsafe实例。

java -Xbootclasspath/a: ${path}   // path为调用Unsafe方法的类所在jar包路径

2. 反射

Java反射机制能够动态生成对象和获取、调用任意类的静态属性及方法。

可以使用反射获取unsafe实例:

try {
  Field field = Unsafe.class.getDeclaredField("theUnsafe");
  field.setAccessible(true);
  return (Unsafe) field.get(null);
} catch (Exception e) {
  return null;
}

获取到unsafe实例后,便可以愉快地使用其方法了。

Unsafe类应用

堆外内存操作

通常Java中新建的对象存储在JVM堆内存中,受配置限制,由GC自动回收。

而Unsafe类提供JVM管辖外的堆外内存(由操作系统管理)的操作,包括内存分配、拷贝、释放、给定地址值操作等。

为什么要使用堆外内存?

  • 通过使用堆外内存,减少JVM堆内内存占用,从而减少垃圾回收停顿对于应用性能的影响。

  • 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,存在堆内内存到堆外内存的数据拷贝。因此,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存,节约数据拷贝的时间开销。

源码

// 分配内存, 相当于C++的malloc函数
public native long allocateMemory(long bytes);
// 扩充内存
public native long reallocateMemory(long address, long bytes);
// 释放内存
public native void freeMemory(long address);
// 设置指定内存块的值
public native void setMemory(Object o, long offset, long bytes, byte value);
// 内存拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);

应用

1. DirectByteBuffer类

DirectByteBuffer类是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,在 Netty、MINA 等 NIO 框架中应用广泛。

其对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存 API 来实现,如下:

  1. 创建DirectByteBuffer时,通过unsafe.allocateMemory分配内存,并通过unsafe.setMemory进行内存初始化

  2. 构建Cleaner对象(继承了PhantomReference,为虚引用)用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。

    DirectByteBuffer(int cap) {
      ...
      cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    }

    当DirectByteBuffer仅被Cleaner引用(即为虚引用)时,其可以在任意GC时段被回收。当DirectByteBuffer实例对象被回收时,在ReferenceHandler线程操作中,会调用Cleaner的clean方法根据创建Cleaner时传入的Deallocator来进行堆外内存的释放。

    Deallocator实现了Runnable接口,在run方法中调用了unsafe.freeMemory(address)方法释放了堆外内存,防止内存泄漏。

深入了解可以看这篇文章:深入理解DirectByteBuffer

CAS

CAS即比较并替换(compare and swap),是实现并发、锁机制时常用的技术。

CAS操作包含三个操作数:内存位置、预期原值及新值。执行 CAS 操作时,先定位到指定位置的内存,将该内存的值与预期原值比较,若匹配,CPU会将该内存位置的值更新为新值,否则,CPU不做任何操作。

CAS底层是基于一条CPU的原子指令(cmpxchg 指令)实现,是原子操作,再并发时能够保证数据一致性。

源码

Unsafe类提供了三种类型的CAS,包括对象、整型和长整形,使用简单:

/**     
 * CAS
 * @param o         包含要修改field的对象
 * @param offset    对象中某field的偏移量  
 * @param expected  期望值  
 * @param update    更新值     
 * @return          true | false
 */
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected, int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

应用

CAS在并发编程中应用广泛,比如Concurrent并发包中的原子类和同步器。

1. Atomic原子类

以AtomicInteger为例,其内部更新值的方法均基于unsafe的CAS实现,代码如下:

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

在该Atomic类初始化时,会通过静态代码块调用unsafe.objectFieldOffset来获取该字段相对于Atomic类的地址偏移值,并赋值给valueOffset静态属性,用作上述CAS的参数,代码如下:

private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}
2. AQS

AQS即AbstractQueuedSynchronizer(等待队列同步器),是ReentrantLock(可重入锁)等实现的关键技术,其内部维护了一个volatile关键字修饰的state字段表示同步状态,通过CAS实现了对该字段的原子更新。部分源码如下:

/**
  * The synchronization state.
  */
private volatile int state;

protected final int getState() {
  return state;
}

protected final void setState(int newState) {
  state = newState;
}

/**
  * Atomically sets synchronization state to the given updated
  * value if the current state value equals the expected value.
  * This operation has memory semantics of a {@code volatile} read
  * and write.
  *
  * @param expect the expected value
  * @param update the new value
  * @return {@code true} if successful. False return indicates that the actual
  *         value was not equal to the expected value.
  */
protected final boolean compareAndSetState(int expect, int update) {
  // See below for intrinsics setup to support this
  return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}

深入了解可以看这篇文章:Java AQS详解

线程调度

Unsafe提供了线程的挂起/恢复、锁的获取/释放操作。

源码

// 阻塞线程
public native void park(boolean isAbsolute, long time);
// 取消阻塞线程
public native void unpark(Object thread);
// 获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
// 释放对象锁
@Deprecated
public native void monitorExit(Object o);
// 尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);

应用

1. AQS

AQS中,对于锁的操作是调用了LockSupport的相关方法实现,比如park、unpark等,而这些方法底层是调用了unsafe类的线程调度方法实现,源码如下:

// 线程挂起
public static void park(Object blocker) {
  Thread t = Thread.currentThread();
  setBlocker(t, blocker);
  UNSAFE.park(false, 0L);
  setBlocker(t, null);
}
// 线程恢复
public static void unpark(Thread thread) {
  if (thread != null)
    UNSAFE.unpark(thread);
}

Class相关

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

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

应用

1. Java8 Lambda表达式

Java8的Lambda表达式基于虚拟机指令invokedynamic及VM Anonymous Class机制实现。

invokedynamic

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

VM Anonymous Class

可看做一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类。而后通过Unsafe.defineAnonymousClass方法定义具体类时填充模板的占位符生成具体的匿名类。

由于生成的匿名类不被任何ClassLoader加载,因此只要当该类没有存在的实例对象、且没有强引用来引用该类的 Class对象时,该类就会被 GC 回收。相比于Java语言层面的匿名内部类,节约了通过ClassLoader进行类加载的开销且更易回收。

Lamda表达式实现:

首先,通过invokedynamic指令调用引导方法生成调用点,在此过程中,会通过ASM动态生成字节码,而后利用 Unsafe.defineAnonymousClass方法定义实现函数式接口的匿名类,并实例化此匿名类,并返回与此匿名类中函数式方法的方法句柄关联的调用点;而后可以通过此调用点实现调用相应Lambda表达式定义逻辑的功能。

详细可阅读这篇文章:由浅入深学习java8的Lambda原理

对象操作

Unsafe类提供了操作对象成员属性及非常规的对象实例化方法。

先了解下对象实例化的两种方式:

  1. 常规对象实例化方式

    本质是通过new机制来实现对象的创建。new机制的特点是必须提供构造函数且传入指定数量的参数,存在一定局限性。

  2. 非常规的实例化方式

    使用Unsafe的allocateInstance 方法,仅通过Class对象就可以创建此类的实例对象(类似反射,但反射无法绕过构造方法),而且不需要调用其构造方法、初始化代码、JVM安全检查等。它抑制修饰符检测,即使构造器是private修饰的也能通过此方法实例化,只需提Class对象即可创建相应的对象。灵活性高,得以广泛应用。

源码

// 返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
public native long objectFieldOffset(Field f);
// 获取指定对象偏移地址的值,忽略修饰限定符的访问限制,与此类似操作还有: getInt,getDouble,getLong,getChar等
public native Object getObject(Object o, long offset);
// 为指定对象的偏移地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等
public native void putObject(Object o, long offset, Object x);
// 从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
public native Object getObjectVolatile(Object o, long offset);
// 存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
public native void putObjectVolatile(Object o, long offset, Object x);
// 有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效
public native void putOrderedObject(Object o, long offset, Object x);
// 绕过构造方法、初始化代码来创建对象(非常规实例化)
public native Object allocateInstance(Class<?> cls) throwsInstantiationException;

应用

GSON

GSON是json对象序列化框架,实现了对json字符串与java对象的互相转换。

将json反序列化为java对象时,如果类有默认构造函数或是接口,则通过反射生成实例,否则通过UnsafeAllocator以非常规方式实例化对象。流程源码如下:

// 有默认构造函数
ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
  return defaultConstructor;
}
// 是接口,获取默认接口实现类
ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType);
if (defaultImplementation != null) {
  return defaultImplementation;
}

// 否则使用unsafe生成实例
return newUnsafeAllocator(type, rawType);

newUnsafeAllocator中,先调用UnsafeAllocator.create创建了实现unsafeAllocator.newInstance抽象方法的UnsafeAllocator,该方法通过unsafe.allocateInstance非常规地生成对象实例。而后调用unsafeAllocator.newInstance方法即可生成实例,源码如下:

public static UnsafeAllocator create() {
  // try JVM 获取Unsafe的allocateInstance方法
  // public class Unsafe {
  //   public Object allocateInstance(Class<?> type);
  // }
  try {
    Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
    Field f = unsafeClass.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    final Object unsafe = f.get(null);
    final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class);
    return new UnsafeAllocator() {
      @Override
      @SuppressWarnings("unchecked")
      public <T> T newInstance(Class<T> c) throws Exception {
        assertInstantiable(c);
        return (T) allocateInstance.invoke(unsafe, c);
      }
    };
  }

深入了解请看这里:Gson源码分析

数组相关

Unsafe类不提供对数组的修改操作,只有arrayBaseOffset与arrayIndexScale两个方法。通常两者配合使用,可定位数组中每个元素在内存中的位置。

源码

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

应用

数组中第N个元素的位置公式为:

valueOffset = baseOffset + (scale * N);

可以使用valueOffset及Unsafe的其他方法来获取数组元素或更新数组的值

// CAS更新数组指定下标元素值
long[] longArray = new long[15];
unsafe.compareAndSwapLong(longArray, valueOffset, expectedValue, newValue);

// 获取数组指定元素值
String[] stringArray = new String[]{"aaa", "bbb", "ccc"};
String str = (String) unsafe.getObject(stringArray, valueOffset);

系统相关

Unsafe类提供获取系统信息的方法,包括获取系统指针大小、内存页大小、负载情况。

源码

// 返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)
public native int addressSize();
// 内存页的大小,此值为2的幂次方。
public native int pageSize();
// 获取系统的平均负载值
public native int getLoadAverage(double[] loadAvg, int nelems);

第三个方法中,loadAvg这个double数组参数将存放负载值的结果。nelems参数决定样本数量,nelems只能取值为1到3,分别代表最近1、5、15分钟内系统的平均负载。如果无法获取系统的负载,此方法返回-1,否则返回获取到的样本数量(即loadAvg中有效的元素个数)。该方法并不常用,可使用JMX中的相关方法来替代此方法。

应用

1. java.nio.Bits类

Bits是java.nio包中的工具类,具有默认的包访问权限,不对外暴露。

其中,pageCount是计算待申请内存所需内存页数量的静态方法,其依赖Unsafe类的pageSize方法获取系统内存页大小,以计算总页数,源码如下:

private static int pageSize = -1;
// 获取单内存页面大小
static int pageSize() {
    if (pageSize == -1)
        pageSize = unsafe().pageSize();
    return pageSize;
}
// 获取内存页总页数
static int pageCount(long size) {
    return (int)(size + (long)pageSize() - 1L) / pageSize();
}

copySwapMemory方法用于将所有元素从一块内存复制到另一块内存,其中调用了unsafe.addressSize()方法获取系统位数,并对32位系统进行特殊处理,源码如下:

private static void copySwapMemory(Object srcBase, long srcOffset,
                           Object destBase, long destOffset,
                           long bytes, long elemSize) {
  // Sanity check size and offsets on 32-bit platforms. Most
  // significant 32 bits must be zero.
  if (unsafe.addressSize() == 4 &&
      (bytes >>> 32 != 0 || srcOffset >>> 32 != 0 || destOffset >>> 32 != 0)) {
    throw new IllegalArgumentException();
  }
}

Bits类还大量调用了Unsafe类的其他方法,如arrayBaseOffset、copyMemory等,有兴趣的读者可阅读源码自行研究。

内存屏障

Unsafe类在Java 8中引入了一套用于定义内存屏障的方法,能够避免指令重排序

源码

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

应用

1. StampedLock

Java8的StampedLock类对读写锁进行了改进。它的思想是读写锁中读不仅不阻塞读,同时也不应该阻塞写。在读的时候如果发生了写,则应当重读而不是在读的时候直接阻塞写。因为在读线程非常多而写线程比较少的情况下,如果读线程阻塞写线程,写线程可能发生饥饿现象。当读执行的时候另一个线程执行了写,则读线程发现数据不一致则执行重读即可。

因此读写都存在时,使用StampedLock可保证读写线程之间不会互相阻塞,但写线程间仍存在阻塞。

由于 StampedLock 提供的乐观读锁不阻塞写线程获取锁,因此当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题。所以当使用StampedLock的乐观读锁时,遵循下述流程保障数据一致性。

图片说明

第③步校验锁状态操作至关重要,需要判断锁状态是否发生改变,从而判断之前 copy 到线程工作内存中的值是否与主内存的值存在不一致。

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

public boolean validate(long stamp) {
  U.loadFence();
  return (stamp & SBITS) == (state & SBITS);
}

最后

Unsafe类以多种方式应用于Java底层库中,涉及大量底层知识,要想正确使用并掌握它还是任重而道远啊。

#Java工程师#
全部评论
面试中应该不会主动问到,但是“java性能调优”的相关问题可以主动提一下,加分😆
点赞 回复
分享
发布于 2020-01-27 21:49
整理得太棒啦🤣比我看的全多啦
点赞 回复
分享
发布于 2020-02-17 22:11
春招专场
校招火热招聘中
官网直投
老铁666
点赞 回复
分享
发布于 2020-02-18 20:16

相关推荐

头像
03-13 15:53
Java
点赞 评论 收藏
转发
6 59 评论
分享
牛客网
牛客企业服务