java对象头信息和三种锁的性能对比

简介:

java对象头信息和三种锁的性能对比

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

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

Object Header (128 bits)
Mark Word (64 bits) Klass Word (64 bits)
unused:25 identity_hashcode:31 unused:1 age:4 biased_lock:1 lock:2 OOP to metadata object 无锁
thread:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 OOP to metadata object 偏向锁
ptr_to_lock_record:62 lock:2 OOP to metadata object 轻量锁
ptr_to_heavyweight_monitor:62 lock:2 OOP to metadata object 重量锁
lock:2 OOP to metadata object GC

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

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

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

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

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

    

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

看下结果

原文地址https://www.cnblogs.com/lusaisai/p/12748869.html

相关文章
|
2月前
|
缓存 算法 Java
Java 实现的局域网管控软件的性能调优
局域网管控软件在企业网络管理中至关重要,但随着网络规模扩大和功能需求增加,其性能可能受影响。文章分析了数据处理效率低下、网络通信延迟和资源占用过高等性能瓶颈,并提出了使用缓存、优化算法、NIO库及合理管理线程池等调优措施,最终通过性能测试验证了优化效果,显著提升了软件性能。
40 1
|
1月前
|
XML Java 数据库连接
性能提升秘籍:如何高效使用Java连接池管理数据库连接
在Java应用中,数据库连接管理至关重要。随着访问量增加,频繁创建和关闭连接会影响性能。为此,Java连接池技术应运而生,如HikariCP。本文通过代码示例介绍如何引入HikariCP依赖、配置连接池参数及使用连接池高效管理数据库连接,提升系统性能。
57 5
|
1月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
20天前
|
SQL Java
使用java在未知表字段情况下通过sql查询信息
使用java在未知表字段情况下通过sql查询信息
34 8
|
1月前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
29天前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
45 3
|
1月前
|
人工智能 监控 数据可视化
Java智慧工地信息管理平台源码 智慧工地信息化解决方案SaaS源码 支持二次开发
智慧工地系统是依托物联网、互联网、AI、可视化建立的大数据管理平台,是一种全新的管理模式,能够实现劳务管理、安全施工、绿色施工的智能化和互联网化。围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管理需求,满足工地多角色、多视角的有效监管,实现工程建设管理的降本增效,为监管平台提供数据支撑。
37 3
|
2月前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
50 17
|
1月前
|
Java 编译器 开发者
Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面
本文探讨了Java异常处理的最佳实践,涵盖理解异常类体系、选择合适的异常类型、提供详细异常信息、合理使用try-catch和finally语句、使用try-with-resources、记录异常信息等方面,帮助开发者提高代码质量和程序的健壮性。
53 2
|
1月前
|
Java 数据库连接 数据库
优化之路:Java连接池技术助力数据库性能飞跃
在Java应用开发中,数据库操作常成为性能瓶颈。频繁的数据库连接建立和断开增加了系统开销,导致性能下降。本文通过问题解答形式,深入探讨Java连接池技术如何通过复用数据库连接,显著减少连接开销,提升系统性能。文章详细介绍了连接池的优势、选择标准、使用方法及优化策略,帮助开发者实现数据库性能的飞跃。
31 4