Unsafe类
内容参考节选:
https://blog.csdn.net/qq_34436819/article/details/102637289
https://blog.csdn.net/qq_34436819/article/details/102723579
https://www.jianshu.com/p/db8dce09232d
https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
一:问题
- Unsafe是什么类
- 与原子类原子操作有什么关系
- Unsafe类有什么功能
二:Unsafe类简介
Unsafe是位于sun.misc包下的一个类,主要提供一些用于低级别、不安全操作的方法,如直接访问系统内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言不再“安全”,因此对Unsafe类的使用一定要慎重。
Unsafe类中方法很多,但大致可以分为8大类。CAS操作、内存操作、线程调度、数组相关、对象相关操作、Class相关操作、内存屏障相关、系统相关。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pzwUBwF4-1577278059389)(https://p1.meituan.net/travelcube/f182555953e29cec76497ebaec526fd1297846.png)]
三:获取Unsafe实例
Unsafe类被final修饰了,表示Unsafe不能被继承;同时Unsafe的构造函数用Private修饰,表示外部无法直接通过构造方法去创建实例。实际上Unsafe是一个单例对象。下面使Unsafe有关构造定义方面的代码。
public final class Unsafe {
private static final Unsafe theUnsafe;
// 构造函数私有化,不能通过构造方法创建实例
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
Unsafe类中有一个getUnsafe()方法,可以返回一个Unsafe对象theUnsafe,但是实际开发中,在自己开发的类中是无法通过Unsafe.getUnsafe()方法来获取实例的,会抛出SecurityException异常。
public class theUnsafe {
public static void main(String[] args) {
Unsafe unsafe = Unsafe.getUnsafe();
}
}
Exception in thread "main" java.lang.SecurityException: Unsafe
at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
at theUnsafe.main(theUnsafe.java:5)
为什么会出现异常:
为什么会出现SecurityException异常呢?这是因为在Unsafe类的getUnsafe()方法中,它做了一层校验,判断当前类(Demo)的类加载器(ClassLoader)是不是启动类加载器(Bootstrap ClassLoader),如果不是,则会抛出SecurityException异常。在JVM的类加载机制中,自定义的类使用的类加载器是应用程序类加载器(Application ClassLoader),所以这个时候校验失败,会抛出异常。
那么如何才能获取到Unsafe类的实例呢?有两种方案。
第一方案:将我们自定义的类(如Demo类)所在的jar包所在的路径通过-Xbootclasspath参数添加到Java命令中,这样当程序启动时,Bootstrap ClassLoader会加载Demo类,这样校验就通过了。显然这种方式比较麻烦,而且不太实用,因为在项目中,可能需要在很多地方都使用Unsafe类,如果通过Java命令行这种方式去指定,就会很麻烦,而且容易出现纰漏,因此不会常用。
第二种方案:通过反射来创建Unsafe类的实例。
public static void main(String[] args) {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 忽略访问权限修饰符的安全检查
field.setAccessible(true);
// 因为theUnsafe字段在Unsafe类中是一个静态字段,所以通过Field.get()获取字段值时,可以传null获取
Unsafe unsafe = (Unsafe) field.get(null);
// 控制台能打印出对象哈希码
System.out.println(unsafe);
} catch (Exception e) {
e.printStackTrace();
}
}
四:Unsafe功能介绍
在上文中将Unsafe的功能主要分为8大类。下面具体对每一类进行解析。
4.1 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的全称是Compare And Swap,翻译过来就是比较并交换。是实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。假设内存中数据的值为V,旧的预期值为A,新的修改值为B。那么CAS操作可以分为三个步骤:1)将旧的预期值A与内存中的值V比较;2)如果A与V的值相等,那么就将V的值设置为B;3)返回操作是否成功,下图为Atomic Integer类调用Unsafe的CAS操作示意图。在多处理器的机器上,当有多个线程对内存中的数据进行CAS操作时,处理器能保证只会有一个线程能修改成功。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NPBQNQA7-1577278059391)(https://p0.meituan.net/travelcube/6e8b1fe5d5993d17a4c5b69bb72ac51d89826.png)]
在Java中可以通过Unsafe类实现CAS操作,而Unsafe类最终调用的是native方法,即具体实现是由JVM中的方法实现的。而JVM中通过C++调用处理器的指令cmpxchg来实现的。
CAS产生的问题
- ABA问题。在CAS操作时会先检查值有没有变化,如果没有变化则执行更新操作,如果有变化则不执行更新操作。假设原来的值为A,后来更新成了B,然后又更新成了A,这个时候去执行CAS的检查操作时,内存中的值还是A,就会误以为内存中的值没有变化,然后执行更新操作,实际上,这个时候内存中的值发生过变化。那么怎么解决ABA的问题呢?可以在每次更新值的时候添加一个版本号,那么A->B->A就变为了1A->2B->3A,这个时候就不会出现ABA的问题了。在JDK1.5开始,JUC包下提供了AtomicStampedReference类来解决ABA的问题。这个类的compareAndSet()方法会首先检查当前引用是否等于预期的引用,然后检查当前标志是都等于预期标志,如果都相等,才会调用casPair()方法执行更新操作。casPair()方法最终也是调用了Unsafe类中的CAS方法。
- 性能问题。CAS会采用循环的方式来实现原子操作,如果长时间的循环设置不成功,就会一直占用CPU,给CPU带来很大的执行开销,降低应用程序的性能。
- 只能保证一个共享变量的原子操作。对一个共享共享变量执行CAS操作时,能保证原子操作,但是如果同时对多个共享变量执行操作时,CAS就无法同时保证这多个共享变量的原子性。这个时候可以使用将多个共享变量封装到一个对象中,然后使用JUC包下提供的AtomicReference类来实现原子操作。另外一种方案就是使用锁。
4.2 内存操作
这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。
//分配内存, 相当于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);
//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: 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);
//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
public native byte getByte(long address);
//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的)
public native void putByte(long address, byte x);
Unsafe能直接操作内存,它能直接进行申请内存、释放内存、内存拷贝等操作。值得注意的是Unsafe直接申请的内存是堆外内存。何谓堆外内存呢?堆外是相对于JVM的内存来说的,通常我们应用程序运行后,创建的对象均在JVM内存中的堆中,堆内存的管理是JVM来管理的,而堆外内存指的是计算机中的直接内存,不受JVM管理。因此使用Unsafe类来申请对外内存时,要特别注意,否则容易出现内存泄漏等问题。
使用堆外内存的原因
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
- 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
典型应用
Unsafe类对内存的操作在网络通信框架中应用广泛,如:Netty、MINA等通信框架。在java.nio包中的DirectByteBuffer中,内存的申请、释放等逻辑都是调用Unsafe类中的对应方法来实现的。下面是DirectByteBuffer类的部分源码。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-npSNGEBP-1577278059392)(https://p0.meituan.net/travelcube/5eb082d2e4baf2d993ce75747fc35de6486751.png)]
java.nio.ByteBuffer
类是通过DirectByteBuffer类来操作内存,DirectByteBuffer又是通过Unsafe类来操作内存,所以最终实际上Netty对堆外的内存的操作是通过Unsafe类中的API来实现的。
4.3 线程调度
包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
public native void monitorEnter(Object o);
//释放对象锁
public native void monitorExit(Object o);
//尝试获取对象锁
public native boolean tryMonitorEnter(Object o);
如上源码说明中,方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。
Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()
和LockSupport.unpark()
实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。下面是LockSupport类的部分源代码。
public class LockSupport {
// UNSAFE是Unsafe类的实例
public static void park() {
// 阻塞线程
UNSAFE.park(false, 0L);
}
public static void unpark(Thread thread) {
if (thread != null)
// 唤醒线程
UNSAFE.unpark(thread);
}
}
4.4 数组相关
Unsafe类中和数组相关的方法有两个:arrayBaseOffset()、arrayIndexScale()
。
// 返回数组中第一个元素在内存中的偏移量
public native int arrayBaseOffset(Class<?> arrayClass);
// 返回数组中每个元素占用的内存大小,单位是字节
public native int arrayIndexScale(Class<?> arrayClass);
两者配合起来使用,即可定位数组中每个元素在内存中的位置。
这两个与数据操作相关的方法,在java.util.concurrent.atomic 包下的AtomicIntegerArray(可以实现对Integer数组中每个元素的原子性操作)中有典型的应用,如下图AtomicIntegerArray源码所示,通过Unsafe的arrayBaseOffset、arrayIndexScale分别获取数组首元素的偏移地址base及单个元素大小因子scale。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位。
public class AtomicIntegerArray implements java.io.Serializable {
private static final long serialVersionUID = 2862133569453604235L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
// 获取数组中第一元素在内存中的偏移量
private static final int base = unsafe.arrayBaseOffset(int[].class);
private static final int shift;
private final int[] array;
static {
// 获取数组中每个元素占用的内存大小
// 对于int类型的元素,占用的是4个字节大小,所以此时返回的是4
int scale = unsafe.arrayIndexScale(int[].class);
if ((scale & (scale - 1)) != 0)
throw new Error("data type scale not a power of two");
shift = 31 - Integer.numberOfLeadingZeros(scale);
}
private static long byteOffset(int i) {
// 根据数组中第一个元素在内存中的偏移量和每个元素占用的大小,
// 计算出数组中第i个元素在内存中的偏移量
return ((long) i << shift) + base;
}
}
4.5 对象相关
此部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。
//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
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) throws InstantiationException;
普通读写
通过Unsafe可以读写一个类的属性,即使这个属性是私有的,也可以对这个属性进行读写。
读写一个Object属性的相关方法
public native int getInt(Object var1, long var2);
public native void putInt(Object var1, long var2, int var4);
getInt用于从对象的指定偏移地址处读取一个int。putInt用于在对象指定偏移地址处写入一个int。其他的primitive type也有对应的方法。
Unsafe还可以直接在一个地址上读写
public native byte getByte(long var1);
public native void putByte(long var1, byte var3);
getByte用于从指定内存地址处开始读取一个byte。putByte用于从指定内存地址写入一个byte。其他的primitive type也有对应的方法。
volatile读写
普通的读写无法保证可见性和有序性,而volatile读写就可以保证可见性和有序性。volatile读写相对普通读写是更加昂贵的,因为需要保证可见性和有序性,而与volatile写入相比putOrderedXX写入代价相对较低,putOrderedXX写入不保证可见性,但是保证有序性,所谓有序性,就是保证指令不会重排序。
public native int getIntVolatile(Object var1, long var2);
public native void putIntVolatile(Object var1, long var2, int var4);
什么是volatile语义?就是读数据时每次都从内存中取最新的值,而不是使用CPU缓存中的值;存数据时将值立马刷新到内存,而不是先写到CPU缓存,等以后再刷新回内存。
有序写入
有序写入只保证写入的有序性,不保证可见性,就是说一个线程的写入不保证其他线程立马可见。
public native void putOrderedObject(Object var1, long var2, Object var4);
public native void putOrderedInt(Object var1, long var2, int var4);
public native void putOrderedLong(Object var1, long var2, long var4);
objectFieldOffset()方法
对象相关操作的方法还有一个十分常用的方法:objectFieldOffset()。它的作用是获取对象的某个非静态字段相对于该对象的偏移地址,它与staticFieldOffset()的作用类似,但是存在一点区别。staticFieldOffset()获取的是静态字段相对于类对象(即类所对应的Class对象)的偏移地址。静态字段存在于方法区中,静态字段每次获取的偏移量的值都是相同的。
objectFieldOffset()的应用场景十分广泛,因为在Unsafe类中,大部分API方法都需要传入一个offset参数,这个参数表示的是偏移量,要想直接操作内存中某个地址的数据,就必须先找到这个数据在哪儿,而通过offset就能知道这个数据在哪儿。因此这个方法应用得十分广泛。下面以AtomicInteger类为例:在静态代码块中,通过objectFieldOffset()
获取了value属性在内存中的偏移量,这样后面将value写入到内存时,就能根据offset来写入了。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 在static静态块中调用objectFieldOffset()方法,获取value字段在内存中的偏移量
// 因为后面AtomicInteger在进行原子操作时,需要调用Unsafe类的CAS方法,而这些方法均需要传入offset这个参数
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
}
非常规实例化方法
Unsafe中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
//绕过构造方法、初始化代码来创建对象
public native Object allocateInstance(Class<?> cls) throws InstantiationException;
4.6 Class相关操作
此部分主要提供Class和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验&确保初始化等。
//获取给定静态字段的内存地址偏移量,这个值对于给定的字段是唯一且固定不变的
public native long staticFieldOffset(Field f);
//获取一个静态类中给定字段的对象指针
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类,通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。 当且仅当ensureClassInitialized方法不生效时返回false。
public native boolean shouldBeInitialized(Class<?> c);
//检测给定的类是否已经初始化。通常在获取一个类的静态属性的时候(因为一个类如果没初始化,它的静态属性也不会初始化)使用。
public native void ensureClassInitialized(Class<?> c);
//定义一个类,此方法会跳过JVM的所有安全检查,默认情况下,ClassLoader(类加载器)和ProtectionDomain(保护域)实例来源于调用者
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain);
//定义一个匿名类
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
在JDK1.8开始,Java开始支持lambda表达式,而lambda表达式的实现是由字节码指令invokedynimic和VM Anonymous Class模板机制来实现的,VM Anonymous Class模板机制最终会使用到Unsafe类的defineAnonymousClass()方法来创建匿名类。
4.7 内存屏障
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
loadFence:保证在这个屏障之前的所有读操作都已经完成。
storeFence:保证在这个屏障之前的所有写操作都已经完成。
fullFence:保证在这个屏障之前的所有读写操作都已经完成。
4.8 系统相关
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
这两个方法在java.nio.Bits
类中有实际应用。Bits作为工具类,提供了计算所申请内存需要占用多少内存页的方法,这个时候需要知道硬件的内存页大小,才能计算出占用内存页的数量。因此在这里借助了Unsafe.pageSize()方法来实现。Bits
类的部分源码如下。
class Bits {
static int pageSize() {
if (pageSize == -1)
// 获取内存页大小
pageSize = unsafe().pageSize();
return pageSize;
}
// 根据内存大小,计算需要的内存页数量
static int pageCount(long size) {
return (int)(size + (long)pageSize() - 1L) / pageSize();
}
}
五:收获
- 基本了解了Unsafe类中的内容
- 对其和CAS操作相关的方法有进一步的了解,了解了一些并发编程中Unsafe的使用情况
class Bits {
static int pageSize() {
if (pageSize == -1)
// 获取内存页大小
pageSize = unsafe().pageSize();
return pageSize;
}
// 根据内存大小,计算需要的内存页数量
static int pageCount(long size) {
return (int)(size + (long)pageSize() - 1L) / pageSize();
}
}
五:收获
- 基本了解了Unsafe类中的内容
- 对其和CAS操作相关的方法有进一步的了解,了解了一些并发编程中Unsafe的使用情况
- 回顾了一下反射知识,新学了native、volatile知识点
来源:CSDN
作者:Asia_Wyz
链接:https://blog.csdn.net/weixin_42183401/article/details/103705482