目录
引言:为什么要了解 Unsafe 类
1. Unsafe类介绍
2.Unsafe类的使用问题
3. Unsafe详解
3.1 初始化代码
3.2 Unsafe类的API常用方法和使用场景
怎么还有这么一个类?怎么也没见过?这是个什么东西?第一次看到这个类时一连串的问号就出现了。在看JUC源码的时候,很多地方有用到了Unsafe 类,JUC包中涉及并发或资源争用的地方都使用了sun.misc.Unsafe类的方法进行CAS操作,这个时候我们就要本着人类对新事物好奇和对原理知其所以然的态度,一起去瞅瞅 Unsafe 类。
搞Java的都知道 Java 不能直接访问操作系统底层,而是通过本地方法( native 修饰的方法)来访问,即通过本地方法直接调用的其它语言(大多为 C++)编写的方法来进行操作,所以很多细节无法追根追溯,只能大致了解。
Oracle 官方一般不建议开发者使用 Unsafe 类,但是了解Unsafe类有助于我们对并发编程的理解,必要的时候开发者们也可以用该类实现一些功能。
Unsafe 可以说时 java 留给开发者的后门,提供了一些低层次操作,如直接内存访问、线程调度等。用于直接操作系统内存且不受 jvm 管辖,实现类似 C++ 风格的操作。
当然在平时的业务开发中,这个类基本是不会有接触到的,但是在 java 的并发包和众多偏向底层的框架中,都有大量应用。不过在使用的过程中有一点非常重要:Unsafe申请的内存的使用将直接脱离jvm,gc将无法管理Unsafe申请的内存,所以使用之后一定要手动释放内存,避免内存溢出!
首先要说的一点就是:Unsafe原本的设计就只应该被标准库使用,因此Oracle 官方一般不建议开发者使用 Unsafe 类,因为正如这个类的类名一样,它并不安全,使用不当会造成内存泄露。
再从开发者的角度来谈一下为什么不建议使用Unsafe 类:
我们把Unsafe类初始化的代码摘出来,可以发现这是一个单例模式:
public final class Unsafe {
private static final Unsafe theUnsafe;
private static native void registerNatives();
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
static {
registerNatives();
Reflection.registerMethodsToFilter(Unsafe.class, new String[]{"getUnsafe"});
}
}
因为是单例模式,所以Unsafe对象不能直接通过 new Unsafe() 来获取,
但是也不能通过Unsafe.getUnsafe() 获取,原因是getUnsafe()里有类加载器的判断,只有通过BootStrap classLoader加载的类才能获取,否则都会抛出SecurityException 异常。
那如果想使用这个类,该如何获取其实例?有如下两个可行方案:
1)从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
2)通过 Java 反射机制。
通过将 private 单例实例暴力“破解”,设置 accessible 为 true,然后通过 Field 的 get 方法,直接获取一个 Object 强制转换为 Unsafe。
private static Unsafe getUnsafe() throws Exception {
Field f = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
return (Unsafe) f.get(null);
}
Unsafe提供的API大致可分为Class相关、对象操作相关、内存操作相关、数组操作相关、线程调度相关、CAS相关、内存屏障相关、系统信息获取相关等几类,下面将对其相关方法和应用场景进行详细介绍。
3.2.1 Class 相关
// 返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。 不要在这个偏移量上执行任何类型的算术运算,它只是一个被传递给不安全的堆内存访问器的cookie。注意:这个方法仅仅针对静态属性,使用在非静态属性上会抛异常。下面源码中的方法注释估计有误,staticFieldOffset和objectFieldOffset的注释估计是对调了,为什么会出现这个问题无法考究。
public native long staticFieldOffset(Field f);
// 返回给定的静态属性的位置,配合staticFieldOffset方法使用。 实际上,这个方法返回值就是静态属性所在的Class对象的一个内存快照。注释中说到,此方法返回的Object有可能为null,它只是一个'cookie'而不是真实的对象,不要直接使用的它的实例中的获取属性和设置属性的方法,它的作用只是方便调用上面提到的像getInt(Object,long)等等的任意方法。
public native Object staticFieldBase(Field f);
// 检测给定的类是否需要初始化。 通常需要使用在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)。 此方法当且仅当ensureClassInitialized方法不生效的时候才返回false。
public native boolean shouldBeInitialized(Class> c);
// 检测给定的类是否已经初始化。 通常需要使用在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)。
public native void ensureClassInitialized(Class> c);
// 告诉JVM定义一个类,返回类实例,此方法会跳过JVM的所有安全检查,可用于动态创建类。 默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例应该来源于调用者。
public native Class> defineClass(String name, byte[] b, int off, int len,ClassLoader loader,ProtectionDomain protectionDomain);
//定义一个匿名类,可用于动态创建类。 这个方法的使用可以看R大的知乎回答:JVM crashes at libjvm.so
public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches);
典型应用
从Java 8开始,JDK使用invokedynamic及VM Anonymous Class结合来实现Java语言层面上的Lambda表达式。
这里有两个概念需要了解一下:
- invokedynamic: invokedynamic是Java 7为了实现在JVM上运行动态语言而引入的一条新的虚拟机指令,它可以实现在运行期动态解析出调用点限定符所引用的方法,然后再执行该方法,invokedynamic指令的分派逻辑是由用户设定的引导方法决定。
- VM Anonymous Class:可以看做是一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类,而后通过Unsafe.defineAnonymousClass方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何ClassLoader下面,只要当该类没有存在的实例对象、且没有强引用来引用该类的Class对象时,该类就会被GC回收。故而VM Anonymous Class相比于Java语言层面的匿名内部类无需通过ClassClassLoader进行类加载且更易回收。
接下来我们简单说明一下Lambda表达式实现,Lambda表达式在编译后,会在.classs文件中生成invokedynamic指令,在引导方法执行过程中,会通过Unsafe.defineAnonymousClass生成实现Consumer接口的匿名类,然后完成具体的方法调用。
详细了解 Java语言的动态性-invokedynamic:https://blog.csdn.net/hj7jay/article/details/73480386
3.2.2 对象引用 相关
JAVA中对象的字段的定位可能通过staticFieldOffset方法实现,该方法返回给定field的内存地址偏移量,这个值对于给定的filed是唯一的且是固定不变的。例如:
getIntVolatile方法获取对象中offset偏移地址对应的整型field的值,支持volatile load语义。
getLong方法获取对象中offset偏移地址对应的long型field的值
Java 中的8种基本数据类型(boolean、byte、char、short、int、long、float、double)及对象引用类型都有get,put方法,并且支持volatile load语义。下面只列举了Object和int类型的get,put方法以及其支持volatile load语义的对应方法。
// 通过给定的Java变量获取引用值。 这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有getInt、getDouble等等。
public native Object getObject(Object o, long offset);
// 将引用值存储到给定的Java变量中。 这里实际上是设置一个Java对象o中偏移地址为offset的属性的值为x,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有putInt、putDouble等等。
public native void putObject(Object o, long offset, Object x);
// 此方法和上面的getObject功能类似,不过附加了'volatile'加载语义,也就是强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和getObject方法相同。
public native Object getObjectVolatile(Object o, long offset);
// 此方法和上面的putObject功能类似,不过附加了'volatile'加载语义,也就是设置值的时候强制(JMM会保证获得锁到释放锁之间所有对象的状态更新都会在锁被释放之后)更新到主存,从而保证这些变更对其他线程是可见的。类似的方法有putIntVolatile、putDoubleVolatile等等。这个方法要求被使用的属性被volatile修饰,否则功能和putObject方法相同。
public native void putObjectVolatile(Object o, long offset, Object x);
//获得给定对象地址偏移量的int值
public native int getInt(Object o, long offset);
//设置给定对象地址偏移量的int值
public native void putInt(Object o, long offset, int x);
public native void putObjectVolatile(Object o, long offset, Object x);
public native int getIntVolatile(Object var1, long var2);
// 返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。 不要在这个偏移量上执行任何类型的算术运算,它只是一个被传递给不安全的堆内存访问器的cookie。注意:这个方法仅仅针对非静态属性,使用在静态属性上会抛异常。
public native long objectFieldOffset(Field f);
// 通过Class对象创建一个类的实例,不需要调用其构造函数、初始化代码、JVM安全检查等等。 同时,它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化。
public native Object allocateInstance(Class> cls) throws InstantiationException;
// 设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。
public native void putOrderedObject(Object o, long offset, Object x);
典型应用
3.2.3 数组 相关
通过 arrayBaseOffset 和 arrayIndexScale 可定位数组中每个元素在内存中的位置。
Unsafe类中有很多以BASE_OFFSET结尾的常量,比如ARRAY_INT_BASE_OFFSET,ARRAY_BYTE_BASE_OFFSET等,这些常量值是通过arrayBaseOffset方法得到的。arrayBaseOffset方法是一个本地方法,可以获取数组第一个元素的偏移地址。Unsafe类中还有很多以INDEX_SCALE结尾的常量,比如 ARRAY_INT_INDEX_SCALE , ARRAY_BYTE_INDEX_SCALE等,这些常量值是通过arrayIndexScale方法得到的。arrayIndexScale方法也是一个本地方法,可以获取数组的转换因子,也就是数组中元素的增量地址。将arrayBaseOffset与arrayIndexScale配合使用,可以定位数组中每个元素在内存中的位置。
// 返回数组类型的第一个元素的偏移地址(基础偏移地址)。 如果arrayIndexScale方法返回的比例因子不为0,你可以通过结合基础偏移地址和比例因子访问数组的所有元素。Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_BASE_OFFSET等。
public native int arrayBaseOffset(Class arrayClass);
//boolean、byte、short、char、int、long、float、double,及对象类型均有以下方法
/** The value of {@code arrayBaseOffset(boolean[].class)} */
public static final int ARRAY_BOOLEAN_BASE_OFFSET = theUnsafe.arrayBaseOffset(boolean[].class);
/**
* Report the scale factor for addressing elements in the storage
* allocation of a given array class. However, arrays of "narrow" types
* will generally not work properly with accessors like {@link
* #getByte(Object, int)}, so the scale factor for such classes is reported
* as zero.
*
* @see #arrayBaseOffset
* @see #getInt(Object, long)
* @see #putInt(Object, long, int)
*/
// 返回数组类型的比例因子(其实就是数据中元素偏移地址的增量,因为数组中的元素的地址是连续的)。 此方法不适用于数组类型为"narrow"类型的数组,"narrow"类型的数组类型使用此方法会返回0(这里narrow应该是狭义的意思,但是具体指哪些类型暂时不明确,笔者查了很多资料也没找到结果)。Unsafe中已经初始化了很多类似的常量如ARRAY_BOOLEAN_INDEX_SCALE等。
public native int arrayIndexScale(Class arrayClass);
//boolean、byte、short、char、int、long、float、double,及对象类型均有以下方法
/** The value of {@code arrayIndexScale(boolean[].class)} */
public static final int ARRAY_BOOLEAN_INDEX_SCALE = theUnsafe.arrayIndexScale(boolean[].class);
典型应用
3.2.4 多线程同步 相关
线程的挂起与恢复:将一个线程进行挂起是通过park方法实现的,调用 park后,线程将一直阻塞直到超时或者中断等条件出现。unpark可以终止一个挂起的线程,使其恢复正常。整个并发框架中对线程的挂起操作被封装在 LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe.park()方法。
主要包括监视器锁定、解锁、CAS相关(单独介绍)的方法。
// 释放被park创建的在一个线程上的阻塞。这个方法也可以被使用来终止一个先前调用park导致的阻塞。这个操作是不安全的,因此必须保证线程是存活的(thread has not been destroyed)。从Java代码中判断一个线程是否存活的是显而易见的,但是从native代码中这机会是不可能自动完成的。
public native void unpark(Object thread);
// 阻塞当前线程直到一个unpark方法出现(被调用)、一个用于unpark方法已经出现过(在此park方法调用之前已经调用过)、线程被中断或者time时间到期(也就是阻塞超时)。在time非零的情况下,如果isAbsolute为true,time是相对于新纪元之后的毫秒,否则time表示纳秒。这个方法执行时也可能不合理地返回(没有具体原因)。并发包java.util.concurrent中的框架对线程的挂起操作被封装在LockSupport类中,LockSupport类中有各种版本pack方法,但最终都调用了Unsafe#park()方法。
public native void park(boolean isAbsolute, long time);
//获得对象锁
public native void monitorEnter(Object o);
//释放对象锁
public native void monitorExit(Object o);
//尝试获取对象锁,返回 true 或 false 表示是否获取成功
public native boolean tryMonitorEnter(Object o);
典型应用
3.2.5 CAS 相关
compareAndSwap,内存偏移地址 offset,预期值 expected,新值 x。如果变量在当前时刻的值和预期值 expected 相等,尝试将变量的值更新为 x。如果更新成功,返回 true;否则,返回 false。
// 更新变量值为x,如果当前值为expected
// o:对象 offset:偏移量 expected:期望值 x:新值
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);
//增加
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public final long getAndAddLong(Object o, long offset, long delta) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, v + delta));
return v;
}
//设置
public final int getAndSetInt(Object o, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
public final long getAndSetLong(Object o, long offset, long newValue) {
long v;
do {
v = getLongVolatile(o, offset);
} while (!compareAndSwapLong(o, offset, v, newValue));
return v;
}
public final Object getAndSetObject(Object o, long offset, Object newValue) {
Object v;
do {
v = getObjectVolatile(o, offset);
} while (!compareAndSwapObject(o, offset, v, newValue));
return v;
ABA 问题:
3.2.6 内存屏障相关
内存屏障相关的方法是在Jdk8添加的。内存屏障相关的知识可以查看:
深入了解 Java 并发编程之 volatile 关键字和内存屏障(Memory Barrier)
// 在该方法之前的所有读操作,一定在load屏障之前执行完成。
public native void loadFence();
// 在该方法之前的所有写操作,一定在store屏障之前执行完成
public native void storeFence();
// 在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。
public native void fullFence();
典型应用
当StampedLock处于写入模式,在StampedLock.validate方法的源码实现里,会通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,避免其他线程加载变量到工作内存中和StampedLock.validate中锁状态校验运算发生重排序导致锁状态校验不准确的问题。源码如下图:
3.2.7 内存管理(非堆内存)相关
类中提供的3个本地方法allocateMemory、reallocateMemory、freeMemory分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。
// 8种基本数据类型都有以下 get、put 两个方法。
// 获得给定地址上的 int 值
public native int getInt(long address);
// 设置给定地址上的 int 值
public native void putInt(long address, int x);
// 获得本地指针
public native long getAddress(long address);
// 存储本地指针到给定的内存地址
public native void putAddress(long address, long x);
// 分配内存
public native long allocateMemory(long bytes);
// 重新分配内存
public native long reallocateMemory(long address, long bytes);
// 初始化内存内容
public native void setMemory(Object o, long offset, long bytes, byte value);
// 初始化内存内容
public void setMemory(long address, long bytes, byte value) {
setMemory(null, address, bytes, value);
}
// 内存内容拷贝
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);
// 内存内容拷贝
public void copyMemory(long srcAddress, long destAddress, long bytes) {
copyMemory(null, srcAddress, null, destAddress, bytes);
}
// 释放内存
public native void freeMemory(long address);
3.2.8 系统相关
// 返回指针的大小。返回值为4(32位系统)或8(64位系统)
public native int addressSize();
/** The value of {@code addressSize()} */
public static final int ADDRESS_SIZE = theUnsafe.addressSize();
// 内存页的大小。
public native int pageSize();
典型应用
java.nio下的工具类Bits中就有典型的例子:
3.2.9 其他
// 获取系统的平均负载值,loadavg这个double数组将会存放负载值的结果,nelems决定样本数量,nelems只能取值为1到3,分别代表最近1、5、15分钟内系统的平均负载。如果无法获取系统的负载,此方法返回-1,否则返回获取到的样本数量(loadavg中有效的元素个数)。实验中这个方法一直返回-1,其实完全可以使用JMX中的相关方法替代此方法。
public native int getLoadAverage(double[] loadavg, int nelems);
// 绕过检测机制直接抛出异常。
public native void throwException(Throwable ee);
参考资料:
http://www.throwable.club/2018/12/13/java-magic-unsafe/
https://www.jianshu.com/p/2e5b92d0962e
http://blog.itpub.net/31559353/viewspace-2636126/