一、JVM层
在java.util.concurrent
包下面的很多类为了追求性能都采用了sun.misc.Unsafe
类中的CAS
操作,从而避免使用synchronized
等加锁方式带来性能上的不足。
sun.misc.Unsafe
sun.misc.Unsafe是jdk中为了方便使用java语言灵活操作内存预留的类,由于该类是直接操作内存,所以从java的角度被定义为不安全的,也就是类名的由来。
经常分析jdk源码的同学肯定不陌生,因为jdk中很多地方都用到了这个类。
创建实例:、
sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe()
主要方法如下,本质都是传入一个对象以及字段在内存中相对于对象起始地址的偏移量,还有设置的值:
//获取字段在内存中相对于对象起始地址的偏移量
private static final long parkBlockerOffset = UNSAFE.objectFieldOffset(Thread.class.getDeclaredField("parkBlocker"));
// CAS操作(需要CPU的支持)设置对象的属性值,类似的还有compareAndSwapInt等
unsafe.compareAndSwapObject(this, tailOffset, expect, update); 类似的还有compareAndSwapInt等
// 给指定的对象设值参数,第二个参数是属性在内存中相对于对象起始地址的偏移量
UNSAFE.putObject(targetObj, parkBlockerOffset, arg);
UNSAFE.putObjectVolatile(targetObj, parkBlockerOffset, arg);
// 获取对象的属性值
UNSAFE.getObject(targetObj, parkBlockerOffset);
UNSAFE.getObjectVolatile(targetObj, parkBlockerOffset);
// 挂起线程以及唤醒线程,具体参见我的其他相关随笔。
UNSAFE.park(isAbsolute, long)
UNSAFE.unpark(thread);
在sun.misc.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);
这三个方法都是native
方法,可以查看hotspot
源码查看其底层实现:(hotspot/src/share/vm/prims/unsafe.cpp)
#define FN_PTR(f) CAST_FROM_FN_PTR(void*, &f)
{CC"compareAndSwapObject", CC"("OBJ"J"OBJ""OBJ")Z", FN_PTR(Unsafe_CompareAndSwapObject)},
{CC"compareAndSwapInt", CC"("OBJ"J""I""I"")Z", FN_PTR(Unsafe_CompareAndSwapInt)},
{CC"compareAndSwapLong", CC"("OBJ"J""J""J"")Z", FN_PTR(Unsafe_CompareAndSwapLong)},
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
UnsafeWrapper("Unsafe_CompareAndSwapObject");
oop x = JNIHandles::resolve(x_h); // 更新值
oop e = JNIHandles::resolve(e_h); // 期望值
oop p = JNIHandles::resolve(obj); // 更新对象
// 根据偏移量offset获取内存中的具体位置
HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);
oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true); // 调用方法执行CAS操作
jboolean success = (res == e); // 如果返回值res==e则表明满足compare条件,swap成功
if (success)
update_barrier_set((void*)addr, x); // 更新memory barrier
return success;
UNSAFE_END
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
UnsafeWrapper("Unsafe_CompareAndSwapInt");
oop p = JNIHandles::resolve(obj);
jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapLong(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jlong e, jlong x))
UnsafeWrapper("Unsafe_CompareAndSwapLong");
Handle p (THREAD, JNIHandles::resolve(obj));
jlong* addr = (jlong*)(index_oop_from_field_offset_long(p(), offset));
if (VM_Version::supports_cx8())
return (jlong)(Atomic::cmpxchg(x, addr, e)) == e;
else {
jboolean success = false;
ObjectLocker ol(p, THREAD);
if (*addr == e) { *addr = x; success = true; }
return success;
}
UNSAFE_END
先来看下Unsafe_CompareAndSwapObject
方法,该方法通过调用index_oop_from_field_offset_long
方法找到需要执行CAS
对象的具体地址,然后调用atomic_compare_exchange_oop
方法执行CAS
操作。
继续深入atomic_compare_exchange_oop
方法看一下,源码如下
// 声明在hotspot/src/share/vm/oops/oop.hpp
static oop atomic_compare_exchange_oop(oop exchange_value,
volatile HeapWord *dest,
oop compare_value,
bool prebarrier = false);
// 定义在hotspot/src/share/vm/oops/oop.inline.hpp
inline oop oopDesc::atomic_compare_exchange_oop(oop exchange_value,
volatile HeapWord *dest,
oop compare_value,
bool prebarrier) {
if (UseCompressedOops) {
if (prebarrier) {
update_barrier_set_pre((narrowOop*)dest, exchange_value);
}
// encode exchange and compare value from oop to T
narrowOop val = encode_heap_oop(exchange_value);
narrowOop cmp = encode_heap_oop(compare_value);
narrowOop old = (narrowOop) Atomic::cmpxchg(val, (narrowOop*)dest, cmp);
// decode old from T to oop
return decode_heap_oop(old);
} else {
if (prebarrier) {
update_barrier_set_pre((oop*)dest, exchange_value);
}
return (oop)Atomic::cmpxchg_ptr(exchange_value, (oop*)dest, compare_value);
}
}
在atomic_compare_exchange_oop
方法中,核心的CAS
操作最终是调用了Atomic::cmpxchg(val, (narrowOop*)dest, cmp)
函数或者Atomic::cmpxchg_ptr(exchange_value, (oop*)dest, compare_value)
函数。
二、内核层
Atomic::cmpxchg(val, (narrowOop*)dest, cmp)
函数虽然有很多重载函数,但最终都是调用的下面的函数:
// hotspot/src/share/vm/runtime/Atomic.cpp
jbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {
assert(sizeof(jbyte) == 1, "assumption.");
uintptr_t dest_addr = (uintptr_t)dest;
uintptr_t offset = dest_addr % sizeof(jint);
volatile jint* dest_int = (volatile jint*)(dest_addr - offset);
jint cur = *dest_int; // 对象当前值
jbyte* cur_as_bytes = (jbyte*)(&cur); // 当前值cur的地址
jint new_val = cur;
jbyte* new_val_as_bytes = (jbyte*)(&new_val); // new_val地址
// new_val存exchange_value,后面修改则直接从new_val中取值
new_val_as_bytes[offset] = exchange_value;
// 比较当前值与期望值,如果相同则更新,不同则直接返回
while (cur_as_bytes[offset] == compare_value) {
// 调用汇编指令cmpxchg执行CAS操作,期望值为cur,更新值为new_val
jint res = cmpxchg(new_val, dest_int, cur);
if (res == cur) break;
cur = res;
new_val = cur;
new_val_as_bytes[offset] = exchange_value;
}
// 返回当前值
return cur_as_bytes[offset];
}
Atomic::cmpxchg_ptr(exchange_value, (oop*)dest, compare_value)
函数在不同系统中都有各自的声明,但是最终都是调用的下面的函数:
// hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
//判断当前执行环境是否为多处理器环境
int mp = os::is_MP();
//LOCK_IF_MP(%4) 在多处理器环境下,为cmpxchgl指令添加lock前缀,以达到内存屏障的效果
//cmpxchgl指令是包含在x86架构及IA-64架构中的一个原子条件指令,
//它会首先比较 dest 指针指向的内存值是否和compare_value的值相等,
//如果相等,则双向交换dest与exchange_value,否则就单方面地将dest指向的内存值交给exchange_value。
//这条指令完成了整个CAS操作,因此它也被称为CAS指令。
__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
: "=a" (exchange_value)
: "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
: "cc", "memory");
return exchange_value;
cmpxchgl的详细执行过程:
- 首先,输入是"r" (exchange_value), “a” (compare_value), “r” (dest), “r” (mp),表示compare_value存入eax寄存器,而exchange_value、dest、mp的值存入任意的通用寄存器。嵌入式汇编规定把输出和输入寄存器按统一顺序编号,顺序是从输出寄存器序列从左到右从上到下以“%0”开始,分别记为%0、%1···%9。也就是说,输出的eax是%0,输入的exchange_value、compare_value、dest、mp分别是%1、%2、%3、%4。
- 因此,cmpxchg %1,(%3)实际上表示cmpxchg exchange_value,(dest)
需要注意的是cmpxchg有个隐含操作数eax,其实际过程是先比较eax的值(也就是compare_value)和dest地址所存的值是否相等。 - 输出是"=a" (exchange_value),表示把eax中存的值写入exchange_value变量中。
Atomic::cmpxchg这个函数最终返回值是exchange_value,也就是说,如果cmpxchgl执行时compare_value和dest指针指向内存值相等则会使得dest指针指向内存值变成exchange_value,最终eax存的compare_value赋值给了exchange_value变量,即函数最终返回的值是原先的compare_value。 - 此时Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功。如果cmpxchgl执行时compare_value和(dest)不等则会把当前dest指针指向内存的值写入eax,最终输出时赋值给exchange_value变量作为返回值,导致(jint)(Atomic::cmpxchg(x, addr, e)) == e得到false,表明CAS失败。
源码的核心点
- 不管是Hotspot中的Atomic::cmpxchg方法,还是Java中的compareAndSwapInt方法,它们本质上都是对相应平台的CAS指令的一层简单封装。
- CAS指令作为一种硬件原语,有着天然的原子性,这也正是CAS的价值所在。
三、Unsafe功能
Unsafe 类提供了一些能够绕过 Java 语言安全机制的方法,例如直接操作内存、CAS(比较并交换)操作、分配和释放内存等。这使得开发者可以在某些情况下获得更高的性能,但同时也需要承担更大的风险和责任。
一些用途包括:
- 手动管理内存: 开发者可以使用 Unsafe 类手动分配和释放内存,从而实现更精细的内存管理。
- 原子操作: Unsafe 提供了原子操作方法,使开发者可以实现高效的多线程并发控制。
- 绕过安全检查: Unsafe 可以绕过一些 Java 语言层面的安全检查,但这也会导致潜在的安全漏洞。
打破Java的安全管控
Java是一种安全而强大的开发工具,它能有效地防止许多低级错误,特别是与内存管理相关的错误。然而,在某些情况下,Unsafe类可以被用于一些高级开发需求,例如在底层内存操作和性能优化方面。Unsafe类确实具有许多强大的功能,如上图Unsafe功能所示。
Unsafe类属于sun. API,但并不是J2SE的官方一部分,所以你可能很难找到官方文档进行参考,在开发过程中,我们建议开发者谨慎使用Unsafe类,并遵循Java的最佳实践。尽可能地使用官方支持的API和框架来完成开发任务。这样可以确保代码的安全性和可维护性,并降低潜在错误的风险。
关于使用Unsafe的编程建议
通过使用Unsafe类,开发人员可以直接操作内存,从而实现一些高级功能和性能优化。
但是,使用Unsafe类需要非常谨慎,因为它可以绕过Java语言的安全机制,可能导致严重的安全漏洞和内存错误。
为了确保安全性和可靠性,开发人员应该遵循Java的最佳实践,并尽量避免使用Unsafe类。
3.1 内存操作
3.1.1 方法概述
//分配新的本地空间
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 native void copyMemory(Object srcBase, long srcOffset,Object destBase, long destOffset,long bytes);
//清除内存
public native void freeMemory(long address);
3.1.2 简单测试
Unsafe unsafe = getUnsafe();
int size = 4;
long addr = unsafe.allocateMemory(size);
long addr3 = unsafe.reallocateMemory(addr, size * 2);
System.out.println("addr: "+addr);
System.out.println("addr3: "+addr3);
try {
//使用setMemory方法将addr指向的内存块的前4个字节设置为值1。
unsafe.setMemory(null,addr ,size,(byte)1);
//使用copyMemory方法将addr指向的内存块的内容复制到addr3指向的内存块的前4个字节,以及从addr3+4开始的4个字节。
for (int i = 0; i < 2; i++) {
unsafe.copyMemory(null,addr,null,addr3+size*i,4);
}
//使用getInt方法从addr指向的内存块读取一个整数(应该是0x01010101)。
System.out.println(unsafe.getInt(addr));
//使用getLong方法从addr3指向的内存块读取一个长整数。但由于只设置了前4个字节为1,其余字节的值是未定义的,因此这个长整数的值是不确定的。
System.out.println(unsafe.getLong(addr3));
}finally {//释放内存
unsafe.freeMemory(addr);
unsafe.freeMemory(addr3);
}
分析:
分析一下运行结果,首先使用allocateMemory方法申请 4 字节长度的内存空间,调用setMemory方法向每个字节写入内容为byte类型的 1,当使用 Unsafe 调用getInt方法时,因为一个int型变量占 4 个字节,会一次性读取 4 个字节,组成一个int的值,对应的十进制结果为 16843009。
你可以通过下图理解这个过程:
在代码中调用reallocateMemory方法重新分配了一块 8 字节长度的内存空间,通过比较addr和addr3可以看到和之前申请的内存地址是不同的。在代码中的第二个 for 循环里,调用copyMemory方法进行了两次内存的拷贝,每次拷贝内存地址addr开始的 4 个字节,分别拷贝到以addr3和addr3+4开始的内存空间上:
拷贝完成后,使用getLong方法一次性读取 8 个字节,得到long类型的值为 72340172838076673。
需要注意,通过这种方式分配的内存属于 堆外内存 ,是无法进行垃圾回收的,需要我们把这些内存当做一种资源去手动调用freeMemory方法进行释放,否则会产生内存泄漏。通用的操作内存方式是在try中执行对内存的操作,最终在finally块中进行内存的释放
为什么要使用堆外内存?
- 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。
- 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
3.3.3 典型应用
DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。
下图为 DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 分配内存并返回基地址
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 内存初始化
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 跟踪 DirectByteBuffer 对象的垃圾回收,以实现堆外内存释放
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
3.2 内存屏障
3.2.1 介绍
在介绍内存屏障前,需要知道编译器和 CPU 会在保证程序输出结果一致的情况下,会对代码进行重排序,从指令优化角度提升性能。而指令重排序可能会带来一个不好的结果,导致 CPU 的高速缓存和内存中数据的不一致,而内存屏障(Memory Barrier)就是通过阻止屏障两边的指令重排序从而避免编译器和硬件的不正确优化情况。
在硬件层面上,内存屏障是 CPU 为了防止代码进行重排序而提供的指令,不同的硬件平台上实现内存屏障的方法可能并不相同。在 Java8 中,引入了 3 个内存屏障的函数,它屏蔽了操作系统底层的差异,允许在代码中定义、并统一由 JVM 来生成内存屏障指令,来实现内存屏障的功能。
Unsafe 中提供了下面三个内存屏障相关方法:
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
内存屏障可以看做对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。以loadFence方法为例,它会禁止读操作重排序,保证在这个屏障之前的所有读操作都已经完成,并且将缓存数据设为无效,重新从主存中进行加载。
看到这估计很多小伙伴们会想到volatile关键字了,如果在字段上添加了volatile关键字,就能够实现字段在多线程下的可见性。基于读内存屏障,我们也能实现相同的功能。下面定义一个线程方法,在线程中去修改flag标志位,注意这里的flag是没有被volatile修饰的:
@Getter
class ChangeThread implements Runnable{
/**volatile**/ boolean flag=false;
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("subThread change flag to:" + flag);
flag = true;
}
}
在主线程的while循环中,加入内存屏障,测试是否能够感知到flag的修改变化:
public static void main(String[] args){
ChangeThread changeThread = new ChangeThread();
new Thread(changeThread).start();
while (true) {
boolean flag = changeThread.flag;
unsafe.loadFence(); //加入读内存屏障
if (flag){
System.out.println("detected flag changed");
break;
}
}
System.out.println("main thread end");
}
3.2.2 典型应用
在 Java 8 中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程“饥饿”现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题。
为了解决这个问题,StampedLock 的 validate 方法会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障。
public boolean validate(long stamp) {
U.loadFence();
return (stamp & SBITS) == (state & SBITS);
}
3.3 对象操作
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class Main {
private int value;
public static void main(String[] args) throws Exception{
Unsafe unsafe = reflectGetUnsafe();
assert unsafe != null;
long offset = unsafe.objectFieldOffset(Main.class.getDeclaredField("value"));
Main main = new Main();
System.out.println("value before putInt: " + main.value);
unsafe.putInt(main, offset, 42);
System.out.println("value after putInt: " + main.value);
System.out.println("value after putInt: " + unsafe.getInt(main, offset));
}
private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
对象成员属性的内存偏移量获取,以及字段属性值的修改,在上面的例子中我们已经测试过了。除了前面的putInt、getInt方法外,Unsafe 提供了全部 8 种基础数据类型以及Object的put和get方法,并且所有的put方法都可以越过访问权限,直接修改内存中的数据。阅读 openJDK 源码中的注释发现,基础数据类型和Object的读写稍有不同,基础数据类型是直接操作的属性值(value),而Object的操作则是基于引用值(reference value)。下面是Object的读写方法:
//在对象的指定偏移地址获取一个对象引用
public native Object getObject(Object o, long offset);
//在对象指定偏移地址写入一个对象引用
public native void putObject(Object o, long offset, Object x);
除了对象属性的普通读写外,Unsafe 还提供了 volatile 读写和有序写入方法。volatile读写方法的覆盖范围与普通读写相同,包含了全部基础数据类型和Object类型,以int类型为例:
//在对象的指定偏移地址处读取一个int值,支持volatile load语义
public native int getIntVolatile(Object o, long offset);
//在对象指定偏移地址处写入一个int,支持volatile store语义
public native void putIntVolatile(Object o, long offset, int x);
相对于普通读写来说,volatile读写具有更高的成本,因为它需要保证可见性和有序性。在执行get操作时,会强制从主存中获取属性值,在使用put方法设置属性值时,会强制将值更新到主存中,从而保证这些变更对其他线程是可见的。
有序写入:
public native void putOrderedObject(Object o, long offset, Object x);
public native void putOrderedInt(Object o, long offset, int x);
public native void putOrderedLong(Object o, long offset, long x);
有序写入的成本相对volatile较低,因为它只保证写入时的有序性,而不保证可见性,也就是一个线程写入的值不能保证其他线程立即可见。为了解决这里的差异性,需要对内存屏障的知识点再进一步进行补充,首先需要了解两个指令的概念:
- Load:将主内存中的数据拷贝到处理器的缓存中
- Store:将处理器缓存的数据刷新到主内存中。顺序写入与volatile写入的差别在于,在顺序写时加入的内存屏障类型为StoreStore类型,而在volatile写入时加入的内存屏障是StoreLoad类型,如下图所示:
- 在有序写入方法中,使用的是StoreStore屏障,该屏障确保Store1立刻刷新数据到内存,这一操作先于Store2以及后续的存储指令操作。而在volatile写入中,使用的是StoreLoad屏障,该屏障确保Store1立刻刷新数据到内存,这一操作先于Load2及后续的装载指令,并且,StoreLoad屏障会使该屏障之前的所有内存访问指令,包括存储指令和访问指令全部完成之后,才执行该屏障之后的内存访问指令。
综上所述,在上面的三类写入方法中,在写入效率方面,按照put、putOrder、putVolatile的顺序效率逐渐降低。
对象实例化
使用 Unsafe 的 allocateInstance 方法,允许我们使用非常规的方式进行对象的实例化,首先定义一个实体类,并且在构造函数中对其成员变量进行赋值操作:
@Data
public class A {
private int b;
public A(){
this.b =1;
}
}
分别基于构造函数、反射以及 Unsafe 方法的不同方式创建对象进行比较:
public void objTest() throws Exception{
A a1=new A();
System.out.println(a1.getB());
A a2 = A.class.newInstance();
System.out.println(a2.getB());
A a3= (A) unsafe.allocateInstance(A.class);
System.out.println(a3.getB());
}
打印结果分别为 1、1、0,说明通过allocateInstance方法创建对象过程中,不会调用类的构造方法。使用这种方式创建对象时,只用到了Class对象,所以说如果想要跳过对象的初始化阶段或者跳过构造器的安全检查,就可以使用这种方法。在上面的例子中,如果将 A 类的构造函数改为private类型,将无法通过构造函数和反射创建对象(可以通过构造函数对象 setAccessible 后创建对象),但allocateInstance方法仍然有效。
典型应用
- 常规对象实例化方式:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。
- 非常规的实例化方式:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
3.4 数组操作
3.4.1 介绍
arrayBaseOffset 与 arrayIndexScale 这两个方法配合起来使用,即可定位数组中每个元素在内存中的位置。
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
3.4.2 典型应用
这两个与数据操作相关的方法,在 java.util.concurrent.atomic 包下的 AtomicIntegerArray(可以实现对 Integer 数组中每个元素的原子性操作)中有典型的应用,如下图 AtomicIntegerArray 源码所示,通过 Unsafe 的 arrayBaseOffset、arrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale 。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 getAndAdd 方法即通过 checkedByteOffset 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。
3.5 CAS 操作
3.5.1 介绍
/**
* 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? CAS 即比较并替换(Compare And Swap),是实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg 。
3.5.2 典型应用
在 JUC 包的并发工具类中大量地使用了 CAS 操作,像在前面介绍synchronized和AQS的文章中也多次提到了 CAS,其作为乐观锁在并发工具类中广泛发挥了作用。在 Unsafe 类中,提供了compareAndSwapObject、compareAndSwapInt、compareAndSwapLong方法来实现的对Object、int、long类型的 CAS 操作。
以compareAndSwapInt方法为例:
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
参数中o为需要更新的对象,offset是对象o中整形字段的偏移量,如果这个字段的值与expected相同,则将字段的值设为x这个新值,并且此更新是不可被中断的,也就是一个原子操作。下面是一个使用compareAndSwapInt的例子:
private volatile int a;
public static void main(String[] args){
CasTest casTest=new CasTest();
new Thread(()->{
for (int i = 1; i < 5; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
new Thread(()->{
for (int i = 5 ; i <10 ; i++) {
casTest.increment(i);
System.out.print(casTest.a+" ");
}
}).start();
}
private void increment(int x){
while (true){
try {
long fieldOffset = unsafe.objectFieldOffset(CasTest.class.getDeclaredField("a"));
if (unsafe.compareAndSwapInt(this,fieldOffset,x-1,x))
break;
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
}
在上面的例子中,使用两个线程去修改int型属性a的值,并且只有在a的值等于传入的参数x减一时,才会将a的值变为x,也就是实现对a的加一的操作。流程如下所示:
需要注意的是,在调用compareAndSwapInt方法后,会直接返回true或false的修改结果,因此需要我们在代码中手动添加自旋的逻辑。在AtomicInteger类的设计中,也是采用了将compareAndSwapInt的结果作为循环条件,直至修改成功才退出死循环的方式来实现的原子性的自增操作。
3.6 线程调度
3.6.1 介绍
Unsafe 类中提供了park、unpark、monitorEnter、monitorExit、tryMonitorEnter方法进行线程调度。
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o);
方法 park、unpark 即可实现线程的挂起与恢复,将一个线程进行挂起是通过 park 方法实现的,调用 park 方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark 可以终止一个挂起的线程,使其恢复正常。
此外,Unsafe 源码中monitor相关的三个方法已经被标记为deprecated,不建议被使用:
//获得对象锁
@Deprecated
public native void monitorEnter(Object var1);
//释放对象锁
@Deprecated
public native void monitorExit(Object var1);
//尝试获得对象锁
@Deprecated
public native boolean tryMonitorEnter(Object var1);
monitorEnter方法用于获得对象锁,monitorExit用于释放对象锁,如果对一个没有被monitorEnter加锁的对象执行此方法,会抛出IllegalMonitorStateException异常。tryMonitorEnter方法尝试获取对象锁,如果成功则返回true,反之返回false。
3.6.2 典型应用
Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer (AQS),就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而 LockSupport 的 park、unpark 方法实际是调用 Unsafe 的 park、unpark 方式实现的。
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);
}
LockSupport 的park方法调用了 Unsafe 的park方法来阻塞当前线程,此方法将线程阻塞后就不会继续往后执行,直到有其他线程调用unpark方法唤醒当前线程。下面的例子对 Unsafe 的这两个方法进行测试:
package com.zcl.msb.day04;
import sun.misc.Unsafe;
import java.util.concurrent.TimeUnit;
public class UnsafeTest04 {
public static void main(String[] args) {
Unsafe unsafe = UnsafeObject.getUnsafe();
Thread mainThread = Thread.currentThread();
new Thread(()->{
try {
TimeUnit.SECONDS.sleep(5);
System.out.println("subThread try to unpark mainThread");
unsafe.unpark(mainThread);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("park main mainThread");
unsafe.park(false,0L);
System.out.println("unpark mainThread success");
}
}
程序运行的流程也比较容易看懂,子线程开始运行后先进行睡眠,确保主线程能够调用park方法阻塞自己,子线程在睡眠 5 秒后,调用unpark方法唤醒主线程,使主线程能继续向下执行。整个流程如下图所示:
3.6 Class 操作
3.6.1 介绍
Unsafe 对Class的相关操作主要包括类加载和静态变量的操作方法。
静态属性读取相关的方法
//获取静态属性的偏移量
public native long staticFieldOffset(Field f);
//获取静态属性的对象指针
public native Object staticFieldBase(Field f);
//判断类是否需要初始化(用于获取类的静态属性前进行检测)
public native boolean shouldBeInitialized(Class<?> c);
创建一个包含静态属性的类,进行测试:
package com.zcl.msb.day04;
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
class User {
public static String name="Hydra";
int age;
}
public class UnsafeTest05 {
public static void main(String[] args) throws NoSuchFieldException {
new User();
Unsafe unsafe = UnsafeObject.getUnsafe();
System.out.println(unsafe.shouldBeInitialized(User.class));
Field sexField = User.class.getDeclaredField("name");
long fieldOffset = unsafe.staticFieldOffset(sexField);
Object fieldBase = unsafe.staticFieldBase(sexField);
Object object = unsafe.getObject(fieldBase, fieldOffset);
System.out.println(object);
}
}
在 Unsafe 的对象操作中,我们学习了通过objectFieldOffset方法获取对象属性偏移量并基于它对变量的值进行存取,但是它不适用于类中的静态属性,这时候就需要使用staticFieldOffset方法。在上面的代码中,只有在获取Field对象的过程中依赖到了Class,而获取静态变量的属性时不再依赖于Class。
在上面的代码中首先创建一个User对象,这是因为如果一个类没有被初始化,那么它的静态属性也不会被初始化,最后获取的字段属性将是null。所以在获取静态属性前,需要调用shouldBeInitialized方法,判断在获取前是否需要初始化这个类。
使用defineClass方法允许程序在运行时动态地创建一个类
public native Class<?> defineClass(String name, byte[] b, int off, int len, ClassLoader loader,ProtectionDomain protectionDomain);
1
private static void defineTest() {
String fileName="F:\\workspace\\unsafe-test\\target\\classes\\com\\cn\\model\\User.class";
File file = new File(fileName);
try(FileInputStream fis = new FileInputStream(file)) {
byte[] content=new byte[(int)file.length()];
fis.read(content);
Class clazz = unsafe.defineClass(null, content, 0, content.length, null, null);
Object o = clazz.newInstance();
Object age = clazz.getMethod("getAge").invoke(o, null);
System.out.println(age);
} catch (Exception e) {
e.printStackTrace();
}
}
在上面的代码中,首先读取了一个class文件并通过文件流将它转化为字节数组,之后使用defineClass方法动态的创建了一个类,并在后续完成了它的实例化工作,流程如下图所示,并且通过这种方式创建的类,会跳过 JVM 的所有安全检查。
除了defineClass方法外,Unsafe 还提供了一个defineAnonymousClass方法:
public native Class<?> defineAnonymousClass(Class<?> hostClass, byte[] data, Object[] cpPatches);
使用该方法可以用来动态的创建一个匿名类,在Lambda表达式中就是使用 ASM 动态生成字节码,然后利用该方法定义实现相应的函数式接口的匿名类。在 JDK 15 发布的新特性中,在隐藏类(Hidden classes)一条中,指出将在未来的版本中弃用 Unsafe 的defineAnonymousClass方法。
典型应用
Lambda 表达式实现需要依赖 Unsafe 的 defineAnonymousClass 方法定义实现相应的函数式接口的匿名类。
3.7 系统信息
这部分包含两个获取系统相关信息的方法。
//返回系统指针的大小。返回值为4(32位系统)或 8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
实例化Unsafe后门对象
我们来看一下sun.misc.Unsafe类的源码,如下图所示。
如果尝试创建sun.misc.Unsafe类的实例,是不被允许的,主要基于以下两个原因:
1.Unsafe类的构造函数是私有的,无法直接实例化;
2.虽然Unsafe类提供了静态的getUnsafe()方法,但如果尝试调用Unsafe.getUnsafe(),会导致SecurityException异常。这是因为只有由JDK信任的类才能实例化Unsafe类。
然而,总会存在一些变通的解决办法,其中一个简单的方式是利用反射进行实例化,具体示例代码如下所示:
Field f = Unsafe.class.getDeclaredField("theUnsafe"); //Internal reference
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
需要注意的是,IDE(如Eclipse)对于这样的用法可能会报错。不过,不用担心,你可以直接运行代码,它们应该可以正常执行,现在进入主题,使用这个对象我们可以做如下“有趣的”事情。
使用sun.misc.Unsafe
首先,让我们创建一个User类作为我们测试Unsafe操作的目标实体。
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
}
以上是一个简单的User类,包含一个name属性和一个age属性,以及相应的getter和setter方法。这将作为我们接下来进行Unsafe操作的测试实体类。
创建实例
通过使用Unsafe类的allocateInstance()方法,我们可以创建一个类的实例,而无需调用其构造函数、初始化代码、JVM安全检查等底层操作。即使构造函数是私有的,我们也可以使用这个方法来创建实例。
public class UnsafeTest {
public static Unsafe getUnsafe(){
Field f = Unsafe.class.getDeclaredField("theUnsafe"); // Internal reference
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
}
public static void main(String[] args) throws NoSuchFieldException, SecurityException, Illegal ArgumentException, IllegalAccessException, InstantiationException {
User user = (User ) getUnsafe().allocateInstance(User.class);
System.out.println(user.getAge()); // Print 0
user.setAge(45); // Let's now set age 45 to un-initialized object
System.out.println(user.getAge()); // Print 45
}
}
- 在Unsafe类中,提供了一个静态方法getUnsafe,看上去貌似可以用它来获取Unsafe实例,但直接用调用它也会报错,这是因为在getUnsafe方法中,会对调用者的classLoader进行检查,判断当前类是否由Bootstrap classLoader加载,如果不是的话那么就会抛出一个SecurityException异常。也就是说,只有启动类加载器加载的类才能够调用Unsafe类中的方法,来防止这些方法在不可信的代码中被调用。
- 虽然直接调用不能使用,当我们可以通过反射进行调用。
在上述示例中,通过调用Unsafe类的allocateInstance()方法实例化了User类的对象。注意,我们并没有直接调用User类的构造函数,而是绕过了它。
注意,虽然使用allocateInstance()方法可以绕过构造函数的限制,但这意味着我们无法执行构造函数中的初始化逻辑。因此,必须谨慎使用此方法,并确保正确地初始化创建的对象。
单例模式处理
对于喜欢使用单例模式的程序员来说,这种方式可能会让他们感到头疼,因为它绕过了阻止此类调用的机制。让我们看一个实例。
public class Singleton {
// 私有化构造函数,强制使用getInstance()方法获取实例
private Singleton() {
// 构造函数逻辑
}
private static Singleton instance;
// 获取单例实例的方法
public static Singleton getInstance() {
if (instance == null) {
try {
// 使用Unsafe类的allocateInstance()方法创建实例
Unsafe unsafe = Unsafe.getUnsafe();
instance = (Singleton) unsafe.allocateInstance(Singleton.class);
} catch (InstantiationException e) {
e.printStackTrace();
}
}
return instance;
}
// 其他方法和属性...
}
在上述示例中,展示了如何使用Unsafe类的allocateInstance()方法来创建单例模式的实例。请注意,我们绕过了私有构造函数,通过allocateInstance()方法创建了实例。
实现浅克隆(直接获取内存的方式)
对于浅克隆的实现方法,通常是在clone()方法中调用super.clone()来完成。然而,这种方式要求对象必须实现Cloneable接口,并且在需要进行浅克隆的所有对象中都要实现clone()方法。对于一些开发者来说,这可能会带来一定的工作量和复杂性。
直接使用copyMemory
copyMemory函数是一种低级别的内存复制方法,它可以按字节进行复制。
- 确定对象的大小:首先,你需要确定要克隆的对象的大小。这可以通过计算对象的字节数来完成。
- 创建目标对象:使用目标对象的构造函数创建一个新的对象。这个对象将是克隆对象的副本。
- 使用copyMemory进行复制:使用copyMemory函数将原始对象的内存数据复制到新创建的对象中。
public class TestCloneable {
private static Unsafe getUnsafeInstance() throws IllegalAccessException, NoSuchFieldException {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(null);
}
public static void main(String[] args) throws Exception {
// 使用Unsafe类进行浅克隆
User originalPerson = new User("name",12);
Unsafe unsafe = getUnsafeInstance();
User clonedPersonUnsafe = (User) unsafe.allocateInstance(User.class);
// 获取对象的起始地址
long srcAddress = unsafe.objectFieldOffset(User.class.getDeclaredField("name"));
// 获取对象的大小 int类型4个字节。
long objectSize = srcAddress + 4;
// 分配新的内存空间
long clonedObjectAddress = unsafe.allocateMemory(objectSize);
// 执行内存复制操作
unsafe.copyMemory(originalPerson , srcAddress, clonedPersonUnsafe , clonedObjectAddress,
objectSize );
System.out.println("Cloned User (Unsafe): " + clonedPersonUnsafe);
}
}
原理分析
unsafe.copyMemory() 方法是 sun.misc.Unsafe 类中用于在内存中复制数据的方法。它的参数如下:
这个方法用于在内存中直接复制数据,可以用于将一个对象的字节数据复制到另一个对象的内存位置,然后将这个对象转换为需要被克隆的对象类型。
注意,在使用Unsafe类进行对象克隆时,需要特别谨慎,并确保了解其带来的潜在风险。而在实际开发中,为了代码的可读性和可维护性,我们通常建议使用传统的clone()方法或者其他官方支持的克隆方式。
密码安全
开发人员通常会将密码存储在字符串中,并在应用程序中使用这些密码。使用完成后,一些聪明的程序员会将字符串引用设为null,以使其不再被引用,从而容易被垃圾收集器回收。
- 问题分析:在将引用设为null到垃圾收集器实际回收之间的时间段内,该字符串可能仍存在于字符串池中。在这段时间内,虽然机会很小,但仍有可能通过复杂的攻击方式读取到内存区域并获取密码。
- 解决方案:为了解决这个问题,建议使用char[]数组来存储密码。使用完毕后,你可以迭代处理当前数组,修改/清空这些字符,从而防止密码被泄露。
使用Unsafe类—示例代码
当处理敏感数据如密码时,使用char[]数组是一种更安全的方式。以下是一个完善的案例,演示了如何使用char[]数组来存储和处理密码:
public static void main(String[] args) throws Exception {
String password = "l00k@myHor$e";
String fake = password.replaceAll(".", "?");
System.out.println("Original password: " + password);
System.out.println("Fake password: " + fake);
Unsafe unsafe = getUnsafeInstance();
unsafe.copyMemory(fake, 0L, null, toAddress(password), sizeOf(password));
System.out.println("Password after overwrite: " + password);
System.out.println("Fake password after overwrite: " + fake);
}
private static long toAddress(Object object) throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafeInstance();
Object[] array = new Object[] { object };
long offset = unsafe.arrayBaseOffset(Object[].class);
return unsafe.getLong(array, offset);
}
private static int sizeOf(Object object) throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafeInstance();
return (int) (unsafe.getAddress(toAddress(object) + 8));
}
private static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(null);
}
运行时动态创建类
通过使用sun.misc.Unsafe类的defineClass()方法,可以在运行时动态地创建类。这种方式允许我们将一个字节数组(如编译后的.class文件)转换为一个Java类的实例。
下面是一个简单的示例,演示了如何通过sun.misc.Unsafe类动态加载和创建类:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class DynamicClassCreationExample {
public static void main(String[] args) throws Exception {
// 获取Unsafe实例
Unsafe unsafe = getUnsafeInstance();
// 读取.class文件并保存为字节数组
byte[] classBytes = readClassBytes("DynamicClassToBeCreated.class");
// 动态创建类
Class<?> dynamicClass = unsafe.defineClass(null, classBytes, 0, classBytes.length,
DynamicClassCreationExample.class.getClassLoader(), null);
// 使用动态创建的类
Object instance = dynamicClass.getDeclaredConstructor().newInstance();
System.out.println(instance.getClass().getName()); // 输出:DynamicClassToBeCreated
}
private static byte[] readClassBytes(String className) throws IOException {
InputStream inputStream = DynamicClassCreationExample.class.getClassLoader().getResourceAsStream(className);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, length);
}
return outputStream.toByteArray();
}
private static Unsafe getUnsafeInstance() throws IllegalAccessException, NoSuchFieldException {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(null);
}
}
在上述示例中,我们首先获取sun.misc.Unsafe实例,并将编译后的.class文件读取为字节数组。然后,使用defineClass()方法创建一个新的类。通过调用该方法,我们可以指定类加载器、字节数组的偏移量和长度等信息来创建类。
超大数组
在Java中,常量Integer.MAX_VALUE表示数组长度的最大值。如果你想创建一个非常大的数组,可以通过直接分配内存来实现。以下示例演示了如何创建一个分配了连续内存(数组)的示例,其容量为最大容量的两倍:
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class LargeArrayExample {
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
// 获取Unsafe实例
Unsafe unsafe = getUnsafeInstance();
// 计算数组长度
long arrayLength = (long) MAX_ARRAY_SIZE * 2;
// 分配内存
long arrayAddress = unsafe.allocateMemory(arrayLength);
System.out.println("Array allocated at address: " + arrayAddress);
for (int i = 0; i < 12 ; i += blockSize) {
// 获取当前块的地址
long blockAddress = arrayAddress + (i * Integer.BYTES);
// 计算当前块的实际大小
long currentBlockSize = Math.min(12 - i, blockSize);
// 循环添加元素到当前块
for (int j = 0; j < currentBlockSize; j++) {
unsafe.putInt(blockAddress + (j * Integer.BYTES), i + j);
}
}
}
private static Unsafe getUnsafeInstance() throws NoSuchFieldException, IllegalAccessException {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
return (Unsafe) theUnsafeField.get(null);
}
}
总结概括
sun.misc.Unsafe提供了可以随意查看及修改JVM中运行时的数据结构,尽管这些功能在JAVA开发本身是不适用的。Unsafe是一个用于研究学习HotSpot虚拟机非常棒的工具,因为它不需要调用C++代码,或者需要创建即时分析的工具。然而,使用Unsafe类进行直接内存分配是一种非常底层和不安全的操作,绕过了Java内存管理系统,需要谨慎处理,并且仅在特定的情况下才应使用。