本文概述Java Unsafe类,并举例说明其应用场景,快速浏览下即可
阅读了美团2019技术年货,有一篇文章是对Java魔法类——Unsafe的讲解。文章不错,在此结合源码作一个总结,并添加个人的一些理解和学习文章资源。
目录
Unsafe类简介
Unsafe类使用
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 来实现,如下:
创建DirectByteBuffer时,通过unsafe.allocateMemory分配内存,并通过unsafe.setMemory进行内存初始化
构建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)方法释放了堆外内存,防止内存泄漏。
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表达式定义逻辑的功能。
对象操作
Unsafe类提供了操作对象成员属性及非常规的对象实例化方法。
先了解下对象实例化的两种方式:
常规对象实例化方式
本质是通过new机制来实现对象的创建。new机制的特点是必须提供构造函数且传入指定数量的参数,存在一定局限性。
非常规的实例化方式
使用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 defaultConstructor = newDefaultConstructor(rawType);
if (defaultConstructor != null) {
return defaultConstructor;
}
// 是接口,获取默认接口实现类
ObjectConstructor 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 newInstance(Class 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底层库中,涉及大量底层知识,要想正确使用并掌握它还是任重而道远啊。