前言
Unsafe是位于sun.misc包下的一个类,主要为我们提供了一些用于执行级别低,不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这种机制仅供java核心类库使用,而不应该被普通用户使用。其实例一般情况是获取不到的,源码中的设计是采用单例模式,不是启动类加载器加载初始化就会抛SecurityException异常。
这个类的提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。但是,它是一把双刃剑:正如它的名字所预示的那样,它是不安全的,它所分配的内存需要手动free(不被GC回收)。如果对Unsafe类理解的不够透彻,就进行使用的话,就等于给自己挖了无形之坑,最为致命。
由于sun并没有将其开源,也没给出官方的Document。但是,为了更好地了解java的生态体系,我们应该去学习它,去了解它,不求深入到底层的C/C++代码,但求能了解它的基本功能。
一、获取Unsafe实例
如下Unsafe源码所示,Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。

VM类中的isSystemDomainLoader()
方法

它在返回实例之前会获取当前调用者的类加载器,如果不是启动类加载器就会抛出SecurityException异常。
获取Unsafe实例的正确姿势
那如若想使用这个类,该如何获取其实例?有如下两个可行方案。
其一,从getUnsafe方法的使用限制条件出发,通过Java命令行命令-Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
其二,通过反射获取单例对象theUnsafe。查看源码,我们发现Unsafe内部有一个属性叫theUnsafe,我们直接通过反射拿到它即可。

public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Class<Unsafe> clazz = Unsafe.class;
Field field = clazz.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe);
}
}
二、功能介绍

如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。
内存操作
这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。
//分配内存, 相当于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);
通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。
DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
下图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。

CAS相关操作
CAS 即Compare And Sweap
或Compare And Set
的英文缩写,中文含义是比较并替换,是实现并发算法时常用到的一种技术。CAS操作需要三个操作数:内存地址、目标预期值、新值。执行CAS替换的时候,会将内存地址中的值和目标预期值比较,当前仅当这两个值相等的时候处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。 CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。具体有以下几个方法:
/**
* 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);
线程调度
这部分,包括线程挂起、恢复、锁机制等方法。
//取消阻塞线程
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);
这一部分的典型应用就是Java锁和同步器框架的核心类AbstractQueuedSynchronizer,通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。
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);
对象操作
此部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。
//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量
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中提供allocateInstance方法,仅通过Class对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM安全检查等。它抑制修饰符检测,也就是即使构造器是private修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance在java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。
数组相关
这部分主要介绍与数据操作相关的arrayBaseOffset与arrayIndexScale这两个方法,两者配合起来使用,即可定位数组中每个元素在内存中的位置。
//返回数组中第一个元素的偏移地址
public native int arrayBaseOffset(Class<?> arrayClass);
//返回数组中一个元素占用的大小
public native int arrayIndexScale(Class<?> arrayClass);
内存屏障
在Java 8中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。
//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
系统相关
这部分包含两个获取系统相关信息的方法。
//返回系统指针的大小。返回值为4(32位系统)或8(64位系统)。
public native int addressSize();
//内存页的大小,此值为2的幂次方。
public native int pageSize();
三、Unsafe的使用示例
1、使用Unsafe实例化一个类
有一个User
类,常规的获取这个类的对象的实例的方法有:使用new关键字,使用Java反射机制、反序列化等操作
public class User {
String name;
int age;
public User(){
name="小明";
age=10;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", stamp=" + stamp +
'}';
}
}
然而通过Unsafe的allocateInstance()方法,我们只要这个类的Class对象就可以实例化对象,而且这种实例化方式是不用调用该类的构造方法的,单例模式瞬间瑟瑟发抖[手动狗头]
/**
* Unsafe类灵魂追问:
* (1)Unsafe是什么?
* <p>
* (2)Unsafe只有CAS的功能吗?
* <p>
* (3)Unsafe为什么是不安全的?
* <p>
* (4)怎么使用Unsafe(怎么获得Unsafe的实例)?
*
*/
public class UnsafeTest {
private static Unsafe unsafe;
//通过反射获取Unsafe对象实例
static {
Class<Unsafe> unsafeClass = Unsafe.class;
Field field = null;
try {
field = unsafeClass.getDeclaredField("theUnsafe");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
assert field != null;
field.setAccessible(true);
try {
unsafe = (Unsafe) field.get(null);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 使用Unsafe实例化一个类
*
* @param clazz
* @param <T>
*/
public static <T> T instanceFor(Unsafe unsafe, Class<T> clazz) throws InstantiationException {
return (T) unsafe.allocateInstance(clazz);
}
//main方法打印实例化的对象
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
System.out.println(instanceFor(unsafe, User.class));
}
}
执行结果:
从打印的结果来看,字段全是默认初始值,没有执行构造方法初始化。其实allocateInstance()
方法只会给对象分配内存,并不会调用构造方法。你问我这个用啥用?这个对于单例模式简直是无解的开挂行为,因为他实例化对象不需要经过构造器。
2、修改私有字段的值
使用Unsafe的putXXX()方法,我们可以修改任意私有字段的值。
public class UnsafeTest {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
Class<Unsafe> unsafeClass = Unsafe.class;
Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
User user =new User();
Field age = user.getClass().getDeclaredField("age");
//使用unsafe.putXxx给属性设值
unsafe.putInt(user,unsafe.objectFieldOffset(age),30);
System.out.println(user);
}
}
这里我们可以配合第1种用法来给一个对象分配内存以及初始化。(当然我们也可以通过反射直接修改。)
3、抛出checked异常
我们知道如果代码抛出了checked异常,要不就使用try...catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。
// 使用Unsafe抛出异常不需要定义在方法签名上往外抛,直接使用API的方式就可以了
public static void throwException() {
unsafe.throwException(new Exception());
}
4、在堆外分配内存
使用java 的new会在堆中为对象分配内存,并且对象的生命周期内,会被JVM GC管理。 Unsafe分配的内存,不受Integer.MAX_VALUE的限制,并且分配在非堆内存,使用它时,需要非常谨慎:忘记手动回收时,会产生内存泄露;非法的地址访问时,会导致JVM崩溃。在需要分配大的连续区域、实时编程(不能容忍JVM延迟)时,可以使用它。java.nio使用这一技术。Spark中的Netty也使用了这个技术。
假设我们要在堆外创建一个巨大的byte数组,我们可以使用allocateMemory()方法来实现:
OffHeapArray
/**
* 堆外分配数组
*
*/
public class OffHeapArray {
private static final int BYTE = 1;
private long size;
private long array;
private static Unsafe unsafe;
static {
try {
Field thUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
thUnsafe.setAccessible(true);
unsafe = (Unsafe) thUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
//构造方法中给数组分配内存,理论上可以分配任意大小的数组
public OffHeapArray(long size) {
this.size = size;
array = unsafe.allocateMemory(size * BYTE);
}
/**
* 获得索引i处的元素
*
* @param i
* @return
*/
public int get(long i) {
return unsafe.getInt(array + i * BYTE);
}
/**
* 设置值
*
* @param i
* @param value
*/
public void set(long i, int value) {
unsafe.putInt(array + i * BYTE, value);
}
/**
* 返回数组大小
*
* @return
*/
public long length() {
return this.size;
}
/**
* 释放内存
*/
public void free() {
unsafe.freeMemory(array);
}
public static void main(String[] args) {
System.out.println(unsafe);
}
}
OffHeapArray offHeapArray = new OffHeapArray((long)Integer.MAX_VALUE+100);
offHeapArray.set(0, 45);
System.out.println("索引0:"+offHeapArray.get(0));
System.out.println("数组大小:"+offHeapArray.length());
//用完之后一定要记得手动释放内存
offHeapArray.free();
执行结果:
你问我这个有啥用?Java的数组最大容量受常量Integer.MAX_VALUE
的限制,如果我们用直接申请内存的方式去创建数组,那么数组大小只会收到堆的大小的限制。当时使用这个方式理论上可以创建任意大小的数组。
5、CAS操作
J.U.C底层大量使用了CAS,在AQS、ConcurrentHashmap、ForkJoinPool、FutureTask、StampedLock等都有大量的应用。它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。
6、park/unpark
在LockSupport类中有两个静态方法park()和unpark(),他们的底层调用的就是Unsafe类中对应的park()/unpark()本地方法,当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。具体的分析请参考我的另一篇文章LockSupport深入源码剖析 。
7、计算对象的内存大小
基本的思路如下:
(1)通过反射获得一个类的Field
(2)通过Unsafe的objectFieldOffset()获得每个Field的offSet (3)对Field按照offset排序,取得最大的offset,然后加上这个field的长度,再加上Padding对齐
public class UnsafeTest {
private static Unsafe unsafe;
static {
Class<Unsafe> unsafeClass = Unsafe.class;
Field field = null;
try {
field = unsafeClass.getDeclaredField("theUnsafe");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
assert field != null;
field.setAccessible(true);
try {
unsafe = (Unsafe) field.get(null);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
System.out.println(sizeOf(new Student()));
}
/**
* 计算一个对象的大小,单位:byte
*
* @param object
* @return
*/
public static long sizeOf(Object object) {
HashSet<Field> fields = new HashSet<>();
Class<?> clazz = object.getClass();
while (clazz != Object.class) {
for (Field field : clazz.getDeclaredFields()) {
if ((field.getModifiers() & Modifier.STATIC) == 0) {
fields.add(field);
}
}
clazz = clazz.getSuperclass();
}
long maxSize = 0;
for (Field field : fields) {
long offset = unsafe.objectFieldOffset(field);
if (offset > maxSize) {
maxSize = offset;
}
}
return ((maxSize / 8) + 1) * 8; //对齐填充
}
}
Student类
,有一个String类型的引用4字节,int类型4字节,然后MarkWord 8字节,类型指针4字节,这总共8+4+4+4=20字节,然后需要对齐填充到最近的8字节整数倍,应该是24字节。
public class Student {
String name; //4B
int age; //4B
//8+4+8 ==> 24B
}
执行结果:
可以看到执行结果符合我们的预期值。