一个对象实例包含三个部分,对象头,对象的实例数据,对齐字节。
这里截取一张hotspot的源码当中的注释
也许这样看有些抽象,这里来把它转换成表格会看得更清晰些
可以看出,整个对象头一共有128位,Mark Word有64位,Klass Word有64位,但是Klass Word因为指针压缩的原因被压缩为32位,使用对象头一共有96位,而且现在我们更需要关注的是前64位。至于Klass Word指向了方法区的模板类,字节码。
而且对象头还不是一成不变的,就表格可以看出,对象的状态会改变对象头的数值,这里我们分为5个状态,分别是无锁、偏向锁、轻量锁、重量锁和被gc标记的对象。
这里我会通过证明来说明各种状态对象的对象头信息。
首先是证明的工具,这里需要使用JOL来查看对象头信息,下面是maven添加依赖。
<dependency>
<groupId>org.openjdk.jolgroupId>
<artifactId>jol‐coreartifactId>
<version>0.9version>
dependency>
首先创建一个空的类
public class A {
//没有任何字段
}
查看对象头
package com.luban.layout;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;
import static java.lang.System.out;
public class JOLExample1 {
public static void main(String[] args) throws Exception {
out.println(VM.current().details());
out.println(ClassLayout.parseClass(A.class).toPrintable());
}
}
运行结果
# 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.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 82 22 01 20 (10000010 00100010 00000001 00100000) (536945282)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
整个对象一共16B,其中对象头(Object header)12B,还有4B是对齐的字节(因为在64位虚拟机上对象的大小必须是8的倍数),由于这个对象里面没有任何字段,故而对象的实例数据为0B。
那么对象头里到底是什么呢?在openjdk的官网上可以找到一些解释,网址如下:
http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html
上述引用中提到一个java对象头包含了2个word,并且好包含了堆对象的布局、类型、GC状态、同步状态和标识哈希码。
mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,klass word为对象头的第二个word主要指向对象的元数据。
通过hotspot的注解,我们先假设无锁的情况下mark word的前56位保存的是hashcode,后面8位第一位是无意义,后4位是gc的分代年龄,倒数第三位是偏向锁标志,最后两位是对象状态。
首先要明白,hashcode必须经过计算才能得出,如果对象不经过hashcode计算是不存在这个值的,所以上面列出的头信息都是0。
下面是经过hashcode计算过后的对象头信息测试
public static void main(String[] args) throws Exception {
A a= new A();
out.println("befor hash");
//没有计算HASHCODE之前的对象头
out.println(ClassLayout.parseInstance(a).toPrintable());
//JVM 计算的hashcode
out.println("jvm‐‐‐‐‐‐‐‐‐‐‐‐"+Integer.toHexString(a.hashCode()));
HashUtil.countHash(a);
//当计算完hashcode之后,我们可以查看对象头的信息变化
out.println("after hash");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
这个HashUtil是一个把输出的对象头信息的2进制转换成16进制并处理的工具类
输出的结果
befor hash
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 1 boolean A.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
jvm‐‐‐‐‐‐‐‐‐‐‐‐0x6ae40994
util‐‐‐‐‐‐‐‐‐‐‐0x6ae40994
after hash
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 94 09 e4 (00000001 10010100 00001001 11100100) (‐469134335)
4 4 (object header) 6a 00 00 00 (01101010 00000000 00000000 00000000) (106)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 1 boolean A.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
其中jvm‐‐‐‐‐‐‐‐‐‐‐‐0x6ae40994
是计算出来的hashcode值,但是我们通过计算貌似前56位二进制数并不等于这个16进制数。这是因为我的电脑是用的小端存储。
简单来说最低地址存放的最低字节,一个用十六进制表示的32位数据:12345678H,存放在存储字长是32位的存储单元中,按低字节到高字节的存储顺序为0x78、0x56、0x34和0x12。整个存储字从低字节到高字节读出的结果就是:78563412H。
所以要正确得读出对象头的hashcode必须倒着读。
这样的话真正存储的信息了,而且也和hashcode一模一样,基本可以证明了无锁的情况下对象头前56为存储的就是hashcode,也基本证明了hotpost的注解的正确性。
至于最后8位第一位无意义所以是0,后4位因为对象并没有被gc标记,所以也是0000,倒数第三位是0无偏向,最后两位是01,说明01就是无锁的状态位。
在输出偏向锁对象的对象头信息之前我们必须得搞清楚一件事情,那就是jvm为了方便自身的一些线程的运行,在jvm启动后的一段时间内是没有启动偏向锁的,或者锁是延时了偏向锁的。
至于为什么要延时偏向锁,因为jvm自己知道自己的线程多数都不是轻量级锁,所以就直接延时了偏向锁,节省了上锁的时间。
package com.luban.layout;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample2 {
static A a;
public static void main(String[] args) throws Exception {
//Thread.sleep(5000);
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("我也不知道要打印什么");
}
}
}
如果直接这样上锁,打印出来的依然是无锁的情况
befre lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 1 boolean A.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
我也不知道要打印什么
after lock
com.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12 1 boolean A.flag false
13 3 (loss due to the next object alignment)
这时候就需要把程序自己延时几秒(这里是4秒以上)或者把设置jvm直接启动偏向锁
‐XX:BiasedLockingStartupDelay=0
可以通过这样设置jvm再启动程序
这样打印出来的就不一样了,结果就是两次打印都如下所示,对象一开始就是偏向锁的状态
可以看到偏向锁的状态位依然是01,但是偏向位为1,至于前面4位依然是分代年龄和一位无意义位,但是前56位存放的就再也不是hashcode了,通过hotspot的注释可以理解为前54位当前线程的ID,后两位是Epoch偏向时间戳。
下面这段代码就展示了计算了hashcode之后对象的对象头发生的变化
package com.luban.layout;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample8 {
static A a;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
a = new A();
a.hashCode();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1= new Thread(){
public void run() {
synchronized (a){
System.out.println("lock ed");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
};
t1.start();
}
}
这里想要查看轻量级锁的对象的对象头信息只需要不设置jvm不等待并加个锁就可以了
package com.luban.layout;
import org.openjdk.jol.info.ClassLayout;
import static java.lang.System.out;
public class JOLExample5 {
static A a;
public static void main(String[] args) throws Exception {
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
public static void sync() throws InterruptedException {
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
打印结果
可以看出首先是无锁,然后进入同步代码里就变成了轻量锁,而且轻量锁的状态位是00,前62位是ptr_to_lock_record,hashcode存在这当中
想要查看重量级锁的对象的对象头信息只要让线程出现资源竞争就可以了。
public class JOLExample6 {
static A a;
public static void main(String[] args) throws Exception {
//Thread.sleep(5000);
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1= new Thread(){
public void run() {
synchronized (a){
try {
Thread.sleep(5000);
System.out.println("t1 release");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(1000);
out.println("t1 lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
sync();
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
System.gc();
out.println("after gc()");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("t1 main lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
打印结果
可以看出重量级锁的状态位是10,前62位是ptr_to_monitorObject,hashcode存在这当中
还有需要注意的是如果调用wait方法则立刻变成重量锁
public class JOLExample7 {
static A a;
public static void main(String[] args) throws Exception {
//Thread.sleep(5000);
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
Thread t1= new Thread(){
public void run() {
synchronized (a){
try {
synchronized (a) {
System.out.println("before wait");
out.println(ClassLayout.parseInstance(a).toPrintable());
a.wait();
System.out.println(" after wait");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
t1.start();
Thread.sleep(5000);
synchronized (a) {
a.notifyAll();
}
}
public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("t1 main lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
}
}
因为状态位为2位2进制数,所以只剩下11来标识这种状态了
为了看一看是不是有对象的实例数据,我们在对象中直接创建一个boolean字段并赋值
public class A {
//占一个字节的boolean字段
boolean flag =false;
}
依旧是最开始的那段程序
public class JOLExample1 {
public static void main(String[] args) throws Exception {
out.println(VM.current().details());
out.println(ClassLayout.parseClass(A.class).toPrintable());
}
}
打印结果
# 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.luban.layout.A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 82 22 01 20 (10000010 00100010 00000001 00100000) (536945282)
12 1 boolean A.flag false
13 3 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 3 bytes external = 3 bytes total
可以看到第12个字节就是对象里的boolean字段,然后后面3个字节就是对齐字节,这是因为64位系统存储的数据大小必须是8字节的倍数,所以必须填充到16个字节。
public class A {
int i;
public synchronized void parse(){
i++;
}
}
可以通过添加‐XX:BiasedLockingStartupDelay=20000 ‐XX:BiasedLockingStartupDelay=0
来改变是偏向锁还是轻量锁。
//‐XX:BiasedLockingStartupDelay=20000 ‐XX:BiasedLockingStartupDelay=0
public class JOLExample3 {
public static void main(String[] args) throws Exception {
A a = new A();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for(int i=0;i<1000000000L;i++){
a.parse();
}
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end ‐ start));
}
}
public class JOLExample4 {
static CountDownLatch countDownLatch = new CountDownLatch(1000000000);
public static void main(String[] args) throws Exception {
final A a = new A();
long start = System.currentTimeMillis();
//调用同步方法1000000000L 来计算1000000000L的++,对比偏向锁和轻量级锁的性能
//如果不出意外,结果灰常明显
for(int i=0;i<2;i++){
new Thread(){
@Override
public void run() {
while (countDownLatch.getCount() > 0) {
a.parse();
}
}
}.start();
}
countDownLatch.await();
long end = System.currentTimeMillis();
System.out.println(String.format("%sms", end ‐ start));
}
}
可以非常明显看出各种锁之间的差异,偏向锁的性能最优,而重量级锁的性能最差,虽然这种对比还是有很多偏差,但大致也可以了解各种锁的性能差异。
注意锁的标志和锁的释放没有关系。