前言
Unsafe类是在sun.misc包下,不属于Java标准。但是很多Java的基础类库,包括一些被广泛使用的高性能开发库都是基于Unsafe类开发的,比如Netty、Hadoop、Kafka等。
使用Unsafe可用来直接访问系统内存资源并进行自主管理,Unsafe类在提升Java运行效率,增强Java语言底层操作能力方面起了很大的作用。
Unsafe可认为是Java中留下的后门,提供了一些低层次操作,如直接内存访问、线程调度等。
官方并不建议使用Unsafe。
类的使用
获取Unsafe对象
遗憾的是,Unsafe对象不能直接通过new Unsafe()
或调用Unsafe.getUnsafe()
获取,原因如下:
*不能直接new Unsafe()
,原因是Unsafe
被设计成单例模式,构造方法是私有的;
*不能通过调用Unsafe.getUnsafe()获取,因为
getUnsafe
被设计成只能从引导类加载器(bootstrap class loader)加载,从getUnsafe
的源码中也可以看出来,如下:
@CallerSensitive
public static Unsafe getUnsafe() {
//得到调用该方法的Class对象
Class cc = Reflection.getCallerClass();
//判断调用该方法的类是否是引导类加载器(bootstrap class loader)
//如果不是的话,比如由AppClassLoader调用该方法,则抛出SecurityException异常
if (cc.getClassLoader() != null)
throw new SecurityException("Unsafe");
//返回单例对象
return theUnsafe;
}
虽然我们不能通过以上方法得到Unsafe对象,但得Unsafe类中有个私有的静态全局属性theUnsafe(Unsafe实例对象)
,通过反射,可以获取到该成员属性theUnsafe对应的Field对象,并将其设置为可访问,从而得到theUnsafe具体对象,如下代码:
package concurrency;
import java.lang.reflect.Field;
import sun.misc.Unsafe;
import sun.reflect.Reflection;
public class Test {
public static void main(String[] args) throws NoSuchFieldException,
SecurityException, IllegalArgumentException, IllegalAccessException {
// 通过反射得到theUnsafe对应的Field对象
Field field = Unsafe.class.getDeclaredField("theUnsafe");
// 设置该Field为可访问
field.setAccessible(true);
// 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
Unsafe unsafe = (Unsafe) field.get(null);
System.out.println(unsafe);
}
}
Unsafe类源码分析
Unsafe的大部分API都是native的方法,通过调用JNI的代码实现的(JNI:Java Native Interface为JAVA本地调用),主要包括以下几类:
1)Class相关。主要提供Class和它的静态字段的操作方法。
2)Object相关。主要提供Object和它的字段的操作方法。
3)Arrray相关。主要提供数组及其中元素的操作方法。
4)并发相关。主要提供低级别同步原语,如CAS、线程调度、volatile、内存屏障等。
5)Memory相关。提供了直接内存访问方法(绕过Java堆直接操作本地内存),可做到像C一样自由利用系统内存资源。
6)系统相关。主要返回某些低级别的内存信息,如地址大小、内存页大小。
一个类,方便后续举例
public class VO
{
public int a = 0;
public long b = 0;
public static String c= "123";
public static Object d= null;
public static int e = 100;
}
1.Class相关
//静态属性的偏移量,用于在对应的Class对象中读写静态属性
public native long staticFieldOffset(Field f);
public native Object staticFieldBase(Field f);
//判断是否需要初始化一个类
public native boolean shouldBeInitialized(Class<?> c);
//确保类被初始化
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);
获取静态字段的属性值
VO.e = 1024;
Field sField = VO.class.getDeclaredField("e");
Object base = unsafe.staticFieldBase(sField);
long offset = unsafe.staticFieldOffset(sField);
System.out.println(unsafe.getInt(base, offset));//1024
2.Object相关
//获得对象的字段偏移量
public native long objectFieldOffset(Field f);
//获得给定对象地址偏移量的int值
public native int getInt(Object o, long offset);
//设置给定对象地址偏移量的int值
public native void putInt(Object o, long offset, int x);
//创建对象,但并不会调用其构造方法。如果类未被初始化,将初始化类。
public native Object allocateInstance(Class<?> cls)
throws InstantiationException;
读取对象实例字段的值
//获取实例字段的属性值
VO vo = new VO();
vo.a = 10000;
long aoffset = unsafe.objectFieldOffset(VO.class.getDeclaredField("a"));
int va = unsafe.getInt(vo, aoffset);
System.out.println("va="+va);
3.数组相关
/**
* Report the offset of the first element in the storage allocation of a
* given array class. If {@link #arrayIndexScale} returns a non-zero value
* for the same class, you may use that scale factor, together with this
* base offset, to form new offsets to access elements of arrays of the
* given class.
*
* @see #getInt(Object, long)
* @see #putInt(Object, long, int)
*/
//返回数组中第一个元素的偏移地址
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)
*/
//返回数组中每一个元素占用的大小
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);
获取数组的头部大小和元素大小
// 数组第一个元素的偏移地址,即数组头占用的字节数
int[] intarr = new int[0];
System.out.println(unsafe.arrayBaseOffset(intarr.getClass()));
// 数组中每个元素占用的大小
System.out.println(unsafe.arrayIndexScale(intarr.getClass()));
通过arrayBaseOffset和arrayIndexScale可定位数组中每个元素在内存中的位置。
4.并发相关
4.1CAS相关
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);
从Java 8开始,Unsafe中提供了以下方法:
//增加
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;
4.2线程调度相关
//取消阻塞线程
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);
//尝试获取对象锁,返回true或false表示是否获取成功
public native boolean tryMonitorEnter(Object o);
4.3volatile相关读写
Java中的基本类型(boolean、byte、char、short、int、long、float、double)及对象引用类型都有以下方法。
//从对象的指定偏移量处获取变量的引用,使用volatile的加载语义
//相当于getObject(Object, long)的volatile版本
public native Object getObjectVolatile(Object o, long offset);
//存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义
//相当于putObject(Object, long, Object)的volatile版本
public native void putObjectVolatile(Object o, long offset, Object x);
/**
* Version of {@link #putObjectVolatile(Object, long, Object)}
* that does not guarantee immediate visibility of the store to
* other threads. This method is generally only useful if the
* underlying field is a Java volatile (or if an array cell, one
* that is otherwise only accessed using volatile accesses).
*/
public native void putOrderedObject(Object o, long offset, Object x);
/** Ordered/Lazy version of {@link #putIntVolatile(Object, long, int)} */
public native void putOrderedInt(Object o, long offset, int x);
/** Ordered/Lazy version of {@link #putLongVolatile(Object, long, long)} */
public native void putOrderedLong(Object o, long offset, long x);
4.4内存屏障相关
Java 8引入 ,用于定义内存屏障,避免代码重排序。
//内存屏障,禁止load操作重排序,即屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序,即屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();
5.直接内存访问(非堆内存)
allocateMemory所分配的内存需要手动free(不被GC回收)
//(boolean、byte、char、short、int、long、float、double)都有以下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);
6.系统相关
//返回指针的大小。返回值为4或8。
public native int addressSize();
/** The value of {@code addressSize()} */
public static final int ADDRESS_SIZE = theUnsafe.addressSize();
//内存页的大小。
public native int pageSize();
知识扩展
我们注意到上面有一个方法
- stateOffset=unsafe.objectFieldOffset(field) 从方法名上可以这样理解:获取object对象的属性Field的偏移量。
要理解这个偏移量,需要先了解java的内存模型
Java内存模型
Java对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),简单的理解:
- 对象头,对象是什么?
- 实例数据,对象里有什么?
- 对齐填充,不关键,目的是补齐位数达到8的倍数。
举个简单的例子,如下类:
class VO {
public int a = 0;
public int b = 0;
}
VO vo=new VO();的时候,Java内存中就开辟了一块地址,包含一个固定长度的对象头(假设是16字节,不同位数机器/对象头是否压缩都会影响对象头长度)+实例数据(4字节的a+4字节的b)+padding。
这里直接说结论,我们上面说的偏移量就是在这里体现,如上面a属性的偏移量就是16,b属性的偏移量就是20。
在unsafe类里面,我们发现一个方法unsafe.getInt(object, offset);
通过unsafe.getInt(vo, 16) 就可以得到vo.a的值。是不是联想到反射了?其实java的反射底层就是用的UNSAFE
进一步思考
如何知道一个类里面每个属性的偏移量?只根据偏移量,java怎么知道读取到哪里为止是这个属性的值?
查看属性偏移量,推荐一个工具类jol:http://openjdk.java.net/projects/code-tools/jol/
用jol可以很方便的查看java的内存布局情况,结合一下代码讲解
public class VO {
public int a = 0;
public long b = 0;
public String c= "123";
public Object d= null;
public int e = 100;
public static int f= 0;
public static String g= "";
public Object h= null;
public boolean i;
}
public static void main(String[] args) throws Exception {
System.out.println(VM.current().details());
System.out.println(ClassLayout.parseClass(VO.class).toPrintable());
System.out.println("=================");
Unsafe unsafe = getUnsafeInstance();
VO vo = new VO();
vo.a=2;
vo.b=3;
vo.d=new HashMap<>();
long aoffset = unsafe.objectFieldOffset(VO.class.getDeclaredField("a"));
System.out.println("aoffset="+aoffset);
// 获取a的值
int va = unsafe.getInt(vo, aoffset);
System.out.println("va="+va);
}
public static Unsafe getUnsafeInstance() throws Exception {
// 通过反射获取rt.jar下的Unsafe类
Field theUnsafeInstance = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeInstance.setAccessible(true);
// return (Unsafe) theUnsafeInstance.get(null);是等价的
return (Unsafe) theUnsafeInstance.get(Unsafe.class);
}
在我本地机器测试结果如下:
# Running 64-bit HotSpot VM.
# Using compressed oop with 0-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
com.ha.net.nsp.product.VO object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 12 (object header) N/A
12 4 int VO.a N/A
16 8 long VO.b N/A
24 4 int VO.e N/A
28 1 boolean VO.i N/A
29 3 (alignment/padding gap)
32 4 java.lang.String VO.c N/A
36 4 java.lang.Object VO.d N/A
40 4 java.lang.Object VO.h N/A
44 4 (loss due to the next object alignment)
Instance size: 48 bytes
Space losses: 3 bytes internal + 4 bytes external = 7 bytes total
=================
aoffset=12
va=2
在结果中,我们发现:
- 1、我本地的虚拟机环境是64位并且开启了compressed压缩,对象都是8字节对齐
- 2、VO类的内存布局包含12字节的对象头,4字节的int数据,8字节的long数据,其他String和Object是4字节,最后还有4字节的对齐。
- 3、VO类属性的内存布局跟属性声明的顺序不一致。
- 4、VO类的static属性不在VO的内存布局中,因为他是属于class类。
- 5、通过VO类就可以确定一个对象占用的字节数,这个占用空间在编译阶段就已经确定(注:此占用空间并不是对象的真实占用空间,)。
- 6、如上,通过偏移量12就可以读取到此处存放的值是2。
引申出新的问题:
1、这里的对象头为什么是12字节?对象头里都具体包含什么?
答:正常情况下,对象头在32位系统内占用一个机器码也就是8个字节,64位系统也是占用一个机器码16个字节。但是在我本地环境是开启了reference(指针)压缩,所以只有12个字节。
2、这里的String和Object为什么都是4字节?
答:因为String或者Object类型,在内存布局中,都是reference类型,所以他的大小跟是否启动压缩有关。未启动压缩的时候,32位机器的reference类型是4个字节,64位是8个字节,但是如果启动压缩后,64位机器的reference类型就变成4字节。
3、Java怎么知道应该从偏移量12读取到偏移量16呢,而不是读取到偏移量18或者20?
答:这里我猜测,虚拟机在编译阶段,就已经保留了一个VO类的偏移量数组,那12后的偏移量就是16,所以Java直到读到16为止。
注:一般使用时:
public class Test implements java.io.Serializable {
//序列化相关
private static final long serialVersionUID = 6214790243416807050L;
// JDK里使用的一个工具类对象,提供一些不安全的操作的方法,一般不会在自己的程序中使用该类
//在这里主要用到其中的objectFieldOffset、putOrderedInt、compareAndSwapInt方法
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value成员属性的内存地址相对于对象内存地址的偏移量
private static final long valueOffset;
static {
try {
//初始化valueOffset,通过unsafe.objectFieldOffset方法获取成员属性value内存地址相对于对象内存地址的偏移量
valueOffset = unsafe.objectFieldOffset(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//int的值,设为volatile,保证线程之间的可见性
private volatile int value;
......
}