在看CAS中经常会遇到unsafe.compareAndSwapInt(this, stateOffset, expect, update);很久很久以前看着就当眼熟;现在再看,结果对这个偏移量完全未知,于是有了这篇文章
最近在翻ReentrantLock源码的时候,看到AQS(AbstractQueuedSynchronizer.java)里面有一段代码
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
这就是经典的CAS的算法,这里包含两个陌生的东西,unsafe,stateOffset。
private static final Unsafe unsafe = Unsafe.getUnsafe();
stateOffset = unsafe.objectFieldOffset
(AbstractQueuedSynchronizer.class.getDeclaredField("state"));
又发现stateOffset是跟AQS里面的state字段相关
private volatile int state;
然后我们又发现state是volatitle类型的,当然这是实现LOCK必备的。
这个stateOffset是什么,值是多少,由stateOffset能得到什么?由CAS的算法我们知道需要跟原值进行对比,所以大胆推测通过stateOffset可以得到state字段的值。
另外还有一个东西很让人好奇,UNSAFE是什么,能做什么?
带着这两个问题,查了不少资料,这里我希望尽量能用白话的方式说明一下。 UNSAFE,顾名思义是不安全的,他的不安全是因为他的权限很大,可以调用操作系统底层直接操作内存空间,所以一般不允许使用。 可参考:java对象的内存布局(二):利用sun.misc.Unsafe获取类字段的偏移地址和读取字段的值 我们注意到上面有一个方法
要理解这个偏移量,需要先了解java的内存模型
此文章值得认真阅读几遍: java对象在内存中的结构(HotSpot虚拟机)
Java对象在内存中存储的布局可以分为三块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),简单的理解:
image.png
举个简单的例子,如下类:
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的内存布局情况,结合一下代码讲解
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
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、这里的对象头为什么是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为止。
更多内存布局问题请参考: java对象的内存布局(一):计算java对象占用的内存空间以及java object layout工具的使用 Java对象内存结构 JVM内存堆布局图解分析
说到回收算法,再参考下这篇也更能理解对象的创建和回收: 垃圾回收机制中,引用计数法是如何维护所有对象引用的?
unsafe中有一个park方法,与线程挂起有关,预留到以后研究