java头的信息分析

首先为什么我要去研究java的对象头呢? 这里截取一张hotspot的源码当中的注释

这张图换成可读的表格如下

|--------------------------------------------------------------------------------------------------------------|
| Object Header ( bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word ( bits) | Klass Word ( bits) |
|--------------------------------------------------------------------------------------------------------------|
| unused: | identity_hashcode: | unused: | age: | biased_lock: | lock: | OOP to metadata object | 无锁
|----------------------------------------------------------------------|--------|------------------------------|
| thread: | epoch: | unused: | age: | biased_lock: | lock: | OOP to metadata object | 偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_lock_record: | lock: | OOP to metadata object | 轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_heavyweight_monitor: | lock: | OOP to metadata object | 重量锁
|----------------------------------------------------------------------|--------|------------------------------|
| | lock: | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------|

意思是java的对象头在对象的不同状态下会有不同的表现形式,主要有三种状态,无锁状态、加锁状态、gc标记状态。

那么我可以理解java当中的取锁其实可以理解是给对象上锁,也就是改变对象头的状态,如果上锁成功则进入同步代码块。

但是java当中的锁有分为很多种,从上图可以看出大体分为偏向锁、轻量锁、重量锁三种锁状态。

这三种锁的效率 完全不同、关于效率的分析会在下文分析,我们只有合理的设计代码,才能合理的利用锁、那么这三种锁的原理是什么? 所以我们需要先研究这个对象头。

java对象的布局以及对象头的布局

使用JOL来分析java的对象布局,添加依赖

    <dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.8</version>
</dependency>

测试类

public class B {

}
public class JOLExample1 {
static B b = new B();
public static void main(String[] args) {
//jvm的信息
out.println(VM.current().details());
out.println(ClassLayout.parseInstance(b).toPrintable());
}
}

看下结果

分析结果1:整个对象一共16B,其中对象头(Object header)12B,还有4B是对齐的字节(因为在64位虚拟机上对象的大小必 须是8的倍数),

由于这个对象里面没有任何字段,故而对象的实例数据为0B?

两个问题

1、什么叫做对象的实例数据呢?

2、那么对象头里面的12B到底存的是什么呢?

首先要明白什么对象的实例数据很简单,我们可以在B当中添加一个boolean的字段,大家都知道boolean字段占 1B,然后再看结果

整个对象的大小还是没有改变一共16B,其中对象头(Object header)12B,boolean字段flag(对象的实例数据)占 1B、剩下的3B就是对齐字节。

由此我们可以认为一个对象的布局大体分为三个部分分别是:对象头(Object header)、 对象的实例数据和字节对齐

接下来讨论第二个问题,对象头为什么是12B?这个12B当中分别存储的是什么呢?(不同位数的VM对象头的长度不一 样,这里指的是64bit的vm)

首先引用openjdk文档当中对对象头的解释

上述引用中提到一个java对象头包含了2个word,并且好包含了堆对象的布局、类型、GC状态、同步状态和标识哈 希码,具体怎么包含的呢?又是哪两个word呢?

mark word为第一个word根据文档可以知他里面包含了锁的信息,hashcode,gc信息等等,第二个word是什么 呢?

klass word为对象头的第二个word主要指向对象的元数据。

假设我们理解一个对象头主要上图两部分组成(数组对象除外,数组对象的对象头还包含一个数组长度),

那么 一个java的对象头多大呢?我们从JVM的源码注释中得知到一个mark word一个是64bit,那么klass的长度是多少呢?

所以我们需要想办法来获得java对象头的详细信息,验证一下他的大小,验证一下里面包含的信息是否正确。

根据上述利用JOL打印的对象头信息可以知道一个对象头是12B,其中8B是mark word 那么剩下的4B就是klass word了,和锁相关的就是mark word了,

那么接下来重点分析mark word里面信息 在无锁的情况下markword当中的前56bit存的是对象的hashcode,那么来验证一下

先上代码:手动计算HashCode

public class HashUtil {
public static void countHash(Object object) throws NoSuchFieldException, IllegalAccessException {
// 手动计算HashCode
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(null);
long hashCode = 0;
for (long index = 7; index > 0; index--) {
// 取Mark Word中的每一个Byte进行计算
hashCode |= (unsafe.getByte(object, index) & 0xFF) << ((index - 1) * 8);
}
String code = Long.toHexString(hashCode);
System.out.println("util-----------0x"+code);
}
}
public class JOLExample2 {
public static void main(String[] args) throws Exception {
B b = new B();
out.println("befor hash");
//没有计算HASHCODE之前的对象头
out.println(ClassLayout.parseInstance(b).toPrintable());
//JVM 计算的hashcode
out.println("jvm------------0x"+Integer.toHexString(b.hashCode()));
HashUtil.countHash(b);
//当计算完hashcode之后,我们可以查看对象头的信息变化
out.println("after hash");
out.println(ClassLayout.parseInstance(b).toPrintable()); }
}

分析结果3:

1-----上面没有进行hashcode之前的对象头信息,可以看到的56bit没有值,打印完hashcode之后就有值了,为什 么是1-7B,不是0-6B呢?因为是小端存储

其中两行是我们通过hashcode方法打印的结果,第一行是我根据1-7B的信息计算出来的 hashcode,所以可以确定java对象头当中的mark work里面的后七个字节存储的是hashcode信息,

那么第一个字节当中的八位分别存的 就是分带年龄、偏向锁信息,和对象状态,这个8bit分别表示的信息如下图(其实上图也有信息),这个图会随着对象状态改变而改变, 下图是无锁状态下

关于对象状态一共分为五种状态,分别是无锁、偏向锁、轻量锁、重量锁、GC标记,

那么2bit,如何能表示五种状 态(2bit最多只能表示4中状态分别是:00,01,10,11),

jvm做的比较好的是把偏向锁和无锁状态表示为同一个状态,然 后根据图中偏向锁的标识再去标识是无锁还是偏向锁状态。

什么意思呢?写个代码分析一下,在写代码之前我们先记得 无锁状态下的信息00000001,然后写一个偏向锁的例子看看结果

public static void main(String[] args) throws Exception {
//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
B b = new B();
out.println("befor lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
synchronized (b){
out.println("lock ing");
out.println(ClassLayout.parseInstance(b).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
}

上面这个程序只有一个线程去调用sync方法,故而讲道理应该是偏向锁,但是此时却是轻量级锁

而且你会发现最后输出的结果(第一个字节)依 然是00000001和无锁的时候一模一样,其实这是因为虚拟机在启动的时候对于偏向锁有延迟,

比如把上述代码当中加上 睡眠5秒的代码,结果就会不一样了,

public static void main(String[] args) throws Exception {
//-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
Thread.sleep(5000);
B b = new B();
out.println("befor lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
synchronized (b){
out.println("lock ing");
out.println(ClassLayout.parseInstance(b).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(b).toPrintable());
}

结果变成00000101.当然为了方便测试我们也可以直接通过JVM的参数来禁用延迟

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

结果是和睡眠5秒一样的.

想想为什么偏向锁会延迟?因为启动程序的时候,jvm会有很多操作,包括gc等等,jvm刚运行时存在大量的同步方法,很多都不是偏向锁,

而偏向锁升级为轻/重量级锁的很费时间和资源,因此jvm会延迟4秒左右再开启偏向锁.

那么为什么同步之前就是偏向锁呢?我猜想是jvm的原因,目前还不清楚.

需要注意的after lock,退出同步后依然保持了偏向信息

然后看下轻量级锁的对象头

static A a;
public static void main(String[] args) throws Exception {
a = new A();
out.println("befre lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}

看结果:

关于重量锁首先看对象头

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());//无锁---gc
} public static void sync() throws InterruptedException {
synchronized (a){
System.out.println("t1 main lock");
out.println(ClassLayout.parseInstance(a).toPrintable());//重量锁
}
}

看结果

 由上述实验可总结下图:

性能对比偏向锁和轻量级锁:

public class A {
int i=0; public synchronized void parse(){
i++; }
//JOLExample6.countDownLatch.countDown();
}

执行1000000000L次++操作

public class JOLExample4 {
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)); }
}

此时根据上面的测试可知是轻量级锁,看下结果

大概16秒

然后我们让偏向锁启动无延时,在启动一次

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

再看下结果

只需要2秒,速度提升了很多

再看下重量级锁的时间

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)); }

看下结果,大概31秒,

可以看出三种锁的消耗是差距很大的,这也是1.5以后synchronized优化的意义

需要注意的是如果对象已经计算了hashcode就不能偏向了

static A a;
public static void main(String[] args) throws Exception {
Thread.sleep(5000);
a= new A();
a.hashCode();
out.println("befor lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
synchronized (a){
out.println("lock ing");
out.println(ClassLayout.parseInstance(a).toPrintable());
}
out.println("after lock");
out.println(ClassLayout.parseInstance(a).toPrintable());
}

看下结果

java对象头信息和三种锁的性能对比的更多相关文章

  1. C#实例化对象的三种方式及性能对比

    前言 做项目过程中有个需求要实例化两万个对象并添加到List中,这个过程大概需要1min才能加载完(传参较多),于是开启了代码优化之旅,再此记录. 首先想到的是可能实例化比较耗时,于是开始对每种实例化 ...

  2. Dynamics CRM2016 查询数据的三种方式的性能对比

    之前写过一个博客,对非声明验证方式下连接组织服务的两种方式的性能进行了对比,但当时只是对比了实例化组织服务的时间,并没有对查询数据的时间进行对比,那有朋友也在我的博客中留言了反映了查询的时间问题,一直 ...

  3. Java中数组转为List三种情况的优劣对比,常犯的类型转换错误原因解析

    一.最常见方式(未必最佳)通过 Arrays.asList(strArray) 方式,将数组转换List后,不能对List增删,只能查改,否则抛异常. 关键代码:List list = Arrays. ...

  4. 015-线程同步-synchronized几种加锁方式、Java对象头和Monitor、Mutex Lock、JDK1.6对synchronized锁的优化实现

    一.synchronized概述基本使用 为确保共享变量不会出现并发问题,通常会对修改共享变量的代码块用synchronized加锁,确保同一时刻只有一个线程在修改共享变量,从而避免并发问题. syn ...

  5. 并发王者课-青铜5:一探究竟-如何从synchronized理解Java对象头中的锁

    在前面的文章<青铜4:synchronized用法初体验>中,我们已经提到锁的概念,并指出synchronized是锁机制的一种实现.可是,这么说未免太过抽象,你可能无法直观地理解锁究竟是 ...

  6. WPF中实现PropertyGrid(用于展示对象的详细信息)的三种方式

    原文:WPF中实现PropertyGrid(用于展示对象的详细信息)的三种方式 由于WPF中没有提供PropertyGrid控件,有些业务需要此类的控件.这篇文章介绍在WPF中实现PropertyGr ...

  7. 探究java对象头

    探究java对象头 研究java对象头,我这里先截取Hotspot中关于对象头的描述,本文研究基于64-bit HotSpot VM 文件路径 openjdk-jdk8u-jdk8u\hotspot\ ...

  8. Java中创建线程的三种方式以及区别

    在java中如果要创建线程的话,一般有3种方法: 继承Thread类: 实现Runnable接口: 使用Callable和Future创建线程. 1. 继承Thread类 继承Thread类的话,必须 ...

  9. 盘一盘 synchronized (一)—— 从打印Java对象头说起

    Java对象头的组成 Java对象的对象头由 mark word 和  klass pointer 两部分组成, mark word存储了同步状态.标识.hashcode.GC状态等等. klass  ...

随机推荐

  1. sweep line-The Skyline Problem

    2020-01-10 17:51:05 问题描述: 问题求解: 本题是经典的sweep line问题. 对于sweep line问题我们需要考虑的只有两点: 1. 延水平方向 / 时间方向 :时间队列 ...

  2. Building Applications with Force.com and VisualForce (DEV401) (二四):JavaScript in Visualforce

    Dev401-025:Visualforce Pages: JavaScript in Visualforce Module Objectives1.Describe the use of AJAX ...

  3. NOI ONLINE 提高组 序列 根据性质建图

    题目链接 https://www.luogu.com.cn/problem/P6185 题意 应该不难懂,跳过 分析 说实话第一眼看到这题的时候我有点懵,真不知道怎么做,不过一看数据,还好还好,暴力能 ...

  4. 【Pytest03】全网最全最新的Pytest框架fixture应用篇(1)

    fixtrue修饰器标记的方法通常用于在其他函数.模块.类或者整个工程调用时会优先执行,通常会被用于完成预置处理和重复操作.例如:登录,执行SQL等操作. 完整方法如下:fixture(scope=' ...

  5. ios shell打包脚本 gym

    #! /bin/bash project_path=$() project_config=Release output_path=~/Desktop build_scheme=YKTicketsApp ...

  6. 一款基于SVM算法的分布式法律助手

    一. 项目简介 与 使用说明 体验网站(适配手机端): http://www.zhuchangwu.com 项目基于 Spring Cloud .Vue 构建,平台针对需要维权的用户而设计,主要提供如 ...

  7. session分布式处理

    session分布式处理 标签(空格分隔): 分布式 1. Session复制 在支持Session复制的Web服务器上, 通过修改服务器配置, 可以实现将Session同步到其它Web服务器上, 达 ...

  8. (C#、JavaScript)面向对象的程序设计

    面向对象(OOP)的理解 喜欢程序的朋友们,大家应该都听过一句话"万物皆对象",感觉老牛X了. 面向对象的程序设计,它是围绕真实世界来设计程序的. 面向对象三要素:封装.继承.多态 ...

  9. wsl中配置SML环境

    配置SML/NJ #安装 sudo apt install smlnj #但是wsl不支持32位程序,所以需要下面配置 sudo dpkg --add-architecture i386 sudo a ...

  10. X-Admin&ABP框架开发-版本管理

    多租户系统中,针对于不同租户开放不同功能,或是按照不同功能进行收费管理,需要从宿主本身去管理租户的版本信息,如同酒店人员对不同房间收取不同费用,依据房间内部设施,房间大小等设置不同收费标准.Abp系统 ...