目录
Java与C++最主要的区别是
无法直接操作内存卡
,包括申请内存和释放内存。但是,jre
rt.jar
包却悄然提供了
Unsafe
类,让Java拥有C++低层次内存、线程操作能力,可以认为
Unsafe
是Java
留下的后门
。
Unsafe
可坐落于jre/lib/rt.jar包,全类名
sun.misc.Unsafe
。
Unsafe
不属于Java标准,官方也不推荐使用;然而,很多Java基础操作,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop和Kafka等。主要原因是Unsafe可以直接访问系统内存资源并进行自主管理,极大提升运行效率和增强Java底层操作能力。
一、概述
Unsafe
提供的API大都是native
方法,主要包括以下几类方法操作:
- Class类:Class类和类静态属性;
- Object对象:Object对象和对象属性;
- Array对象:数组和数组元素;
- 直接内存操作:直接内存读写操作;
- 并发相关:低级别同步原语,如CAS、线程调度、volatile、内存屏障等;
- 系统信息:返回某些低级别的内存信息,如地址大小、内存页大小。
二、获取Unsafe对象
查看Unsafe
源码,Unsafe
是单例设计模式设计的类,既然官网不推荐使用,那么可以解释为什么Unsafe
类theSafe
字段由private
修饰了。Unsafe
提供getUnsafe
方法获取实例,但是限定调用的类是又BootClassLoader
加载,所以反射方式成为获取Unsafe
实例的最佳路径。
public final class Unsafe {
/** 类实例对象 **/
private static final Unsafe theUnsafe;
@CallerSensitive
public static Unsafe getUnsafe() {
// 反射获取调用者类对象
Class var0 = Reflection.getCallerClass();
// 类加载器判断,限定类加载器必须是BootClassLoader
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
}
示例:
private static Unsafe getUnsafe(){
try {
// 通过反射机制获取Unsafe类theUnsafe属性的Field对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
field.setAccessible(true);
// 获取Field对应的实例对象
Unsafe unsafe = (Unsafe) field.get(null);
return unsafe;
} catch (Exception e) {
// TODO
}
return null;
}
三、内存操作
Unsafe
类中提供的3个本地方法allocateMemory
、reallocateMemory
、freeMemory
分别用于分配内存,扩充内存和释放内存,与C语言中的3个方法对应。
3.1 直接内存分配
Unsafe提供了native long allocateMemory(long bytes)
方法用于分配给定字节大小的本地内存块,并返回一个非零内存地址,如果分配失败会抛出java.lang.OutOfMemoryError
异常。其中,申请的内存块是未被初始化的,也就是内容是Undefined
,此时如果调用方法只能获得内存地址值只会返回内存地址。可以通过调用{@link #freeMemory}
释放该地址内存,或使用{@link #reallocateMemory}
调整其大小。
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeSample
{
public static void main(String[] args) throws Exception
{
// 获取Unsafe类theUnsafe属性的Field
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
theUnsafeField.setAccessible(true);
// 获取Unsafe属性值
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// Integer类型长度
int intByteLength = Integer.BYTES;
// 分配内存块大小, 分配的内存块必须是类型的整数倍
int allocateMemorySize = intByteLength * 3;
// 分配内存
long memoryAddress = unsafe.allocateMemory(allocateMemorySize);
// 内存地址数据赋值
unsafe.putInt(memoryAddress, 15);
unsafe.putInt(memoryAddress + intByteLength, 20);
// 读取内存数据
int value1 = unsafe.getInt(memoryAddress);
int value2 = unsafe.getInt(memoryAddress + intByteLength);
// 未被初始化, 返回内存地址
int value3 = unsafe.getInt(memoryAddress + intByteLength * 2);
// 打印数据
System.out.printf("allocate value1=%d %d %d\n", value1, value2, value3);
}
}
注释:Unsafe
提供了putX
和getX
方法,更新和获取直接内存地址数据,包括基本数据类型赋值和取值操作。如果内存地址没有初始化操作,方法返回操作地址值。方法申明如下:
/**
* 对给定地址赋值
* @param address 赋值地址
* @param x 赋值地址值
*/
public native void putInt(long address, int x);
/**
* 获取给定地址值
* @param offset 给定地址偏移量,初始地址 + 数据类型位数
*/
public native int getInt(long offset);
注意:Unsafe
allocateMemory
方法分配的是直接内存,不由JVM管理,必须手动管理分配的内存空间并将其释放。
3.2 重新分配内存
Unsafe提供public native long reallocateMemory(long address, long bytes)
方法用于重新分配内存,并把数据从原内存缓冲区(address指向的地方)拷贝到新分配内存块中,返回新内存缓冲区地址。如果参数address=0,等同于allocateMemory
方法的效果。
3.3 释放分配内存
Unsafe提供public native void freeMemory(long address)
方法用于释放由allocateMemory和reallocateMemory两方法分配的内存空间,如果address=0不进行任何操作直接返回 。
四、类操作
Unsafe
提供了对类、对象和数组的操作。其中,类对象包括Class和类静态属性,对象包括Object和对象属性,数组包括Array和数组元素。
4.1 类对象操作
Unsafe
可以直接对类进行操作,包括类加载、类对象初始化,操作方法如下:
// 获取字段对应的类对象
public native Object staticFieldBase(Field f);
// 判断是否需要初始化一个类
public native boolean shouldBeInitialized(Class<?> clazz);
// 确保类被初始化
public native void ensureClassInitialized(Class<?> c);
// 定义一个类,可用于动态创建类
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);
4.2 创建类实例
Unsafe
可以通过allocateMemory
方法会创建类对象创建实例以及分配内存。
注意:调用allocateMemory
方法,会检查并且初始化类对象(即class对象),然后为类对象创建实例并分配内存,但是不会调用类构造器,类对象实例仍旧由JVM管理。
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeSample
{
@Data
static class User {
private String name;
private Integer age;
}
public static void main(String[] args) throws Exception
{
// 获取Unsafe类theUnsafe属性的Field
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
theUnsafeField.setAccessible(true);
// 获取Unsafe属性值
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 为类对象创建实例并分配内存
User user = (User)unsafe.allocateInstance(User.class);
}
}
4.3 类静态属性读写
Unsafe
可以获取类对象静态属性内存地址偏移量,然后根据内存地址偏移量对静态属性进行修改。注意,操作无视熟悉的可见性。
/** 获取静态属性所在的内存地址 */
public native long staticFieldOffset(Field f);
示例
import com.google.common.collect.Lists;
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
import java.util.List;
public class UnsafeSample
{
@Data
static class User {
private static String UID;
}
public static void main(String[] args)
throws NoSuchFieldException, IllegalAccessException
{
// 获取Unsafe类theUnsafe属性的Field
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
theUnsafeField.setAccessible(true);
// 获取Unsafe对象
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 创建对象
User user = (User) unsafe.allocateInstance(User.class);
// 获取字段
Field uidField = user.getClass().getDeclaredField("UID");
// 获取静态属性(static)内存地址偏移量
long uidFieldAddress = unsafe.staticFieldOffset(uidField);
// 设置属性值,包括静态属性值
// 注意: user不能为空,否则会异常退出
unsafe.putObject(user, uidFieldAddress, "USER_ID");
// 获取值
Object value = unsafe.getObject(user, uidFieldAddress);
// 打印
System.out.printf("uid: %s\n", value);
}
}
注意:Unsafe
中任何函数如果标注对基本类型(Int、Float和Boolean等)操作,不能自动转换为包装类,否则会抛出NullPointerException
异常。
4.4 类对象属性读写
Unsafe
可以获取类对象属性内存地址偏移量,然后根据内存地址偏移量对属性进行修改。注意,属性是否可见都可以进行操作。
/** 获取类对象属性所在的内存地址 */
public native long objectFieldOffset(Field f);
示例
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeSample
{
@Data
static class User {
private static String UID;
private String name;
private String[] array;
}
public static void main(String[] args)
throws NoSuchFieldException, IllegalAccessException
{
// 获取Unsafe类theUnsafe属性的Field
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
theUnsafeField.setAccessible(true);
// 获取Unsafe对象
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 创建对象
User user = (User) unsafe.allocateInstance(User.class);
// 获取属性
Field nameField = user.getClass().getDeclaredField("name");
// 获取属性内存地址偏移量
long nameFieldAddress = unsafe.objectFieldOffset(nameField);
// 设置属性值
unsafe.putObject(user, nameFieldAddress, "think");
// 获取值
Object value = unsafe.getObject(user, nameFieldAddress);
// 打印
System.out.printf("name: %s\n", value);
}
}
4.4 对数组进行读写
Unsafe
提供获取数组第一个元素地址偏移量和元素占用字节大小方法,获取数组地址偏移量进而对元素进行读写操作。
/**
* 返回数组第一个元素地址偏移量
* @param arrayClass 数字Class
* @return 地址偏移量
*/
public native int arrayBaseOffset(Class<?> arrayClass);
// 基本类型可以通过静态属性获取其第一个偏移地址
public static final int ARRAY_BYTE_BASE_OFFSET
= theUnsafe.arrayBaseOffset(byte[].class);
/**
* 返回数组元素占用字节大小
* @param arrayClass 数组Class
* @return 数组元素占用字节大小
*/
public native int arrayIndexScale(Class<?> arrayClass);
// 基本类型可以通过静态属性获取其元素占用字节大小
public static final int ARRAY_BYTE_INDEX_SCALE
= theUnsafe.arrayIndexScale(byte[].class);
示例
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeSample
{
private static Unsafe unsafe;
static {
try {
// 获取Unsafe类theUnsafe属性的Field
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
theUnsafeField.setAccessible(true);
// 获取Unsafe实例
unsafe = (Unsafe) theUnsafeField.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {}
}
public static void main(String[] args)
{
// 数组
int[] intArray = new int[3];
// 获取Class对象
Class<?> intArrayClass = intArray.getClass();
// 获取第一个元素的偏移位置
int intArrayOffset = unsafe.arrayBaseOffset(intArrayClass);
// 获取元素占用字节数
int intArrayByte = unsafe.arrayIndexScale(intArrayClass);
for(int i=0; i<intArray.length; i++) {
// 计算偏移位置
long elementOffset = intArrayOffset + i * intArrayByte;
// 更新数组位置元素值
unsafe.putInt(intArray, elementOffset, i * 5);
}
System.out.print("int array: ");
for(Integer e: intArray) {
System.out.printf("%d\t", e);
}
}
}
五、同步操作
Unsafe提供对线程相关操作,包括内存可见性和线程同步。
5.1 CAS
CAS
(Compare And Swap)是乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。注意,方法是通过native
修饰,数据是内存可见的。
/**
* 对比对象偏移地址数据是否和预期相同,如果相同则修改内存地址数据.
* @param object 类对象
* @param offset 偏移内存地址
* @param expectedValue 期望值
* @param newValue 新值
* @return 修改成功返回True,否则返回False
*/
public final native boolean compareAndSwapObject(Object object, long offset,
Object expectedValue, Object newValue);
/**
* 获取对象内存偏移地址的数据,读取数据是内存可见的,和Java @volatitle修饰一致
* @param object 对象
* @param offset 偏移内存地址
* @return 返回对象偏移内存地址值
*/
public native Object getObjectVolatile(Object object, long offset);
/**
* 对象偏移内存地址数据增加值
* @param object 对象
* @param offset 偏移内存地址
* @param delta 增加值
* @return 返回旧数据
*/
public final int getAndAddInt(Object object, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
/**
* 改变对象或者数组偏移内存地址值
* @param object 对象、数组
* @param offset 属性或者数组的偏移
* @param newValue 新值
* @return 返回旧数据
* @since 1.8
*/
public final int getAndSetInt(Object object, long offset, int newValue) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, newValue));
return v;
}
注意:Unsafe
中任何函数如果标注对基本类型(Int、Float和Boolean等)操作,那么不能自动转换为包装类,否则会抛出NullPointerException
异常。
示例
import lombok.Data;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class UnsafeSample
{
@Data
static class User {
private volatile int age;
}
public static void main(String[] args)
throws NoSuchFieldException, IllegalAccessException,
InstantiationException
{
// 获取Unsafe类theUnsafe属性的Field
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
// 设置访问权限
theUnsafeField.setAccessible(true);
// 获取Unsafe实例
Unsafe unsafe = (Unsafe) theUnsafeField.get(null);
// 创建实例对象
User user = (User) unsafe.allocateInstance(User.class);
// 获取属性Field
Field ageField = user.getClass().getDeclaredField("age");
// 获取属性偏移地址
long ageFieldOffset = unsafe.objectFieldOffset(ageField);
// 设置数值
unsafe.getAndSetInt(user, ageFieldOffset, 17);
// CAS
unsafe.compareAndSwapInt(user, ageFieldOffset, 17, 18);
System.out.println("user = " + user);
}
}
5.2 线程调度
jdk包中J.U.C
底层实现引用到Unsafe
的park
和unpark
操作,也就是阻塞线程。注意,Unsafe实现的锁同样也是重入锁。
/**
* 阻塞当前线程。如果isAbsolute=true,实现ms定时, 否则为ns定时。方法返回的条件:
* 1. 如果time <= 0,则直接返回;
* 2. 如果之前有调用unpark或interrupt方法,则park不会挂起直接返回;
* 3. 如果未调用,则会挂起当前线程,直到到达时间或者收到唤醒信号;
* 4. park未知原因调用出错则直接返回(一般不会出现)
* @param isAbsolute 绝对时间标志。
* @param time 阻塞时间
*/
public native void park(boolean isAbsolute, long time);
/**
* 取消阻塞线程
* @param thread 线程
*/
public native void unpark(Object thread);
/**
* 获取对象锁,
* @param object 锁对象
*/
@Deprecated
public native void monitorEnter(Object object);
/**
* 释放对象锁
* @param object 锁对象
*/
@Deprecated
public native void monitorExit(Object object);
/**
* 尝试获取对象锁
* @param object 锁对象
* @return 成功获取锁对象返回true,否则返回false
*/
@Deprecated
public native boolean tryMonitorEnter(Object object);
5.3 volatile
java JMM
模型,对基本类型(boolean、byte、char、short、int、long、float、double)和引用类型对象都存在可见性问题,下面代码将揭示解决方法。
/**
* 使用volatile的加载语义,从对象指定偏移量处获取变量的引用
* @param object 对象
* @param offset 偏移位置
* @return 返回对象属性引用
*/
public native Object getObjectVolatile(Object object, long offset);
/**
* 使用volatile的存储语义,存储属性引用到对象的指定偏移量处
* @param object 对象
* @param offset 偏移位置
* @param value 引用值
*/
public native void putObjectVolatile(Object object, long offset, Object value);
/**
* 仅仅保证@volatile修饰字段的可见性
* @param object 对象
* @param offset 偏移位置
* @param value 引用值
*/
public native void putOrderedObject(Object object, long offset, Object value);
5.4 内存屏障
Java 8引入内存屏障实现 ,主要用途是避免代码重排序
。
/** 禁止load前后代码操作重排序。*/
public native void loadFence();
/** 禁止store前后代码操作重排序 */
public native void storeFence();
/** 禁止load、store前后代码操作重排序 */
public native void fullFence();
六、系统信息
Unsafe
提供一些方法用于获取系统信息
,比如:
/**
* 获取本地指针所占用的字节大小,值为4/8。
*/
public native int addressSize();
/**
* 获取处理器平均负载,分别代表最近1、5和15分钟的平均值
* @param loadavg 采用接收数组
* @param nelems 采用样本数,取值为[1, 3]
* @return 实际采用样本数,无法获取返回-1
public native int getLoadAverage(double[] loadavg, int nelems);