Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(下)

简介: Java GC详解 - 1. 最全面的理解Java对象结构 - 对象指针 OOPs(下)

1.1.3. 偏向锁,轻量锁,重量锁


我们来编写测试代码:

A a = new A();
System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable());
//偏向锁
synchronized (a) {
    System.out.println("------After Fetched Lock------\n" + ClassLayout.parseInstance(a).toPrintable());
}
System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable());
System.out.println("a.hashcode: " + a.hashCode());
System.out.println("------After call hashcode------\n" + ClassLayout.parseInstance(a).toPrintable());
//由于调用了 hashcode,这里直接升级成为轻量锁
synchronized (a) {
    System.out.println("------After Fetched Lock------\n" + ClassLayout.parseInstance(a).toPrintable());
}
System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable());
//测试重量级锁
Runnable r = () -> {
    synchronized (a) {
        System.out.println("------After " + Thread.currentThread() + " lock is fetched------\n" + ClassLayout.parseInstance(a).toPrintable());
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
};
Thread [] threads = new Thread[2];
for (int i = 0; i < threads.length; i++) {
    threads[i] = new Thread(r);
    threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
    threads[i].join();
}
System.out.println("------After Released Lock------\n" + ClassLayout.parseInstance(a).toPrintable());

输出为(我们这里省略掉我们不关心的输出):

------After Initialization------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
------After Fetched Lock------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 90 8a 5e (00000101 10010000 10001010 01011110) (1586139141)
      4     4        (object header)                           c7 02 00 00 (11000111 00000010 00000000 00000000) (711)
------After Released Lock------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 90 8a 5e (00000101 10010000 10001010 01011110) (1586139141)
      4     4        (object header)                           c7 02 00 00 (11000111 00000010 00000000 00000000) (711)
a.hashcode: 929776179
------After call hashcode------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 33 42 6b (00000001 00110011 01000010 01101011) (1799500545)
      4     4        (object header)                           37 00 00 00 (00110111 00000000 00000000 00000000) (55)
------After Fetched Lock------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           78 f2 0f 53 (01111000 11110010 00001111 01010011) (1393554040)
      4     4        (object header)                           ee 00 00 00 (11101110 00000000 00000000 00000000) (238)
------After Released Lock------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 33 42 6b (00000001 00110011 01000010 01101011) (1799500545)
      4     4        (object header)                           37 00 00 00 (00110111 00000000 00000000 00000000) (55)
------After Thread[Thread-0,5,main] lock is fetched------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342)
      4     4        (object header)                           69 02 00 00 (01101001 00000010 00000000 00000000) (617)
------After Thread[Thread-1,5,main] lock is fetched------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342)
      4     4        (object header)                           69 02 00 00 (01101001 00000010 00000000 00000000) (617)
------After Released Lock------
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           02 3e e0 b0 (00000010 00111110 11100000 10110000) (-1327481342)
      4     4        (object header)                           69 02 00 00 (01101001 00000010 00000000 00000000) (617)

我们这里通过第一字节 8 位的末尾两位判断锁状态:

  • 创建对象后,初始为无锁状态:第一字节为00000101,01 代表处于无锁状态。同时,偏向锁是开启状态,因为00000101,倒数第三位为1,这个根据前面的结构图可以知道是偏向锁标记。
  • 第一次 main 线程获取锁,由于没有争抢,同时启动参数中也没有关闭偏向锁,采用偏向锁:第一字节为00000101,代表处于偏向锁状态,后面保存的是指向线程的指针。
  • 第一次 main 线程释放锁,由于没有其他争抢,保持这个偏向锁状态(Hotspot 取消偏向锁需要全局 safepoint,关于这个相关的分析,可以参考:JVM相关 - SafePoint 与 Stop The World 全解(基于OpenJDK 11版本))。
  • 调用 hashcode,根据之前的源码分析,需要取消偏向锁,同时将 hashcode 写入 header:第一字节为00000001,代表处于无锁状态,偏向锁关闭,倒数第三位为 0。
  • 调用 hashcode 之后 main 线程获取锁,由于偏向锁关闭,直接从轻量锁开始:第一字节为01111000,00 代表轻量锁。后面保存了指向锁记录的指针。
  • 调用 hashcode 之后 main 线程释放锁,释放轻量锁,锁记录会被回收,所以 hashcode 回到 header 保存。
  • 多线程导致对象升级为重量级锁之后:第一字节为 00000010,10 代表重量级锁,由于 monitor 一旦生成一直存在,所以这个对象头会一直保留 monitor 的指针,hashcode 也会保存在 monitor 上。
  • 最后锁释放后,header 没有改变,也是上面说的原因。


1.2. 类型字压缩指针与 JVM 最大内存


压缩指针这个属性默认是打开的,可以通过-XX:-UseCompressedOops关闭。

首先说一下为何需要压缩指针呢?32 位的存储,可以描述多大的内存呢?假设每一个1代表1字节,那么可以描述 0~2^32-1 这 2^32 字节也就是 4 GB 的内存。


image.png


但是呢,Java 默认是 8 字节对齐的内存,也就是一个对象占用的空间,必须是 8 字节的整数倍,不足的话会填充到 8 字节的整数倍。也就是其实描述内存的时候,不用从 0 开始描述到 8(就是根本不需要定位到之间的1,2,3,4,5,6,7)因为对象起止肯定都是 8 的整数倍。所以,2^32 字节如果一个1代表8字节的话,那么最多可以描述 2^32 * 8 字节也就是 32 GB 的内存。


image.png


这就是压缩指针的原理。如果配置最大堆内存超过 32 GB(当 JVM 是 8 字节对齐),那么压缩指针会失效。 但是,这个 32 GB 是和字节对齐大小相关的,也就是-XX:ObjectAlignmentInBytes配置的大小(默认为8字节,也就是 Java 默认是 8 字节对齐)-XX:ObjectAlignmentInBytes可以设置为 8 的整数倍,最大 128。也就是如果配置-XX:ObjectAlignmentInBytes为 24,那么配置最大堆内存超过 96 GB 压缩指针才会失效。

编写程序测试下:

A a = new A();
System.out.println("------After Initialization------\n" + ClassLayout.parseInstance(a).toPrintable());

首先,以启动参数:-XX:ObjectAlignmentInBytes=8 -Xmx16g执行:

------After Initialization------
com.hashjang.jdk.TestObjectAlign$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4        (alignment/padding gap)                  
     16     8   long A.d                                       0
Instance size: 24 bytes
Space losses: 4 bytes internal + 0 bytes external = 4 bytes total

可以看到类型字大小为 4 字节48 72 06 00 (01001000 01110010 00000110 00000000) (422472),压缩指针生效。

首先,以启动参数:-XX:ObjectAlignmentInBytes=8 -Xmx32g执行:

------After Initialization------
com.hashjang.jdk.TestObjectAlign$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           a0 5b c6 00 (10100000 01011011 11000110 00000000) (12999584)
     12     4        (object header)                           b4 02 00 00 (10110100 00000010 00000000 00000000) (692)
     16     8   long A.d                                       0
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total


可以看到类型字大小为 8 字节,压缩指针失效:

a0 5b c6 00 (10100000 01011011 11000110 00000000) (12999584)
b4 02 00 00 (10110100 00000010 00000000 00000000) (692)


修改对齐大小为 16 字节,也就是以-XX:ObjectAlignmentInBytes=16 -Xmx32g执行:

------After Initialization------
com.hashjang.jdk.TestObjectAlign$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           48 72 06 00 (01001000 01110010 00000110 00000000) (422472)
     12     4        (alignment/padding gap)                  
     16     8   long A.d                                       0
     24     8        (loss due to the next object alignment)
Instance size: 32 bytes
Space losses: 4 bytes internal + 8 bytes external = 12 bytes total

可以看到类型字大小为 4 字节48 72 06 00 (01001000 01110010 00000110 00000000) (422472),压缩指针生效。


总结


  1. 对象指针包括标记字与类型字。
  2. 标记字中保存了:对象默认哈希值(如果没有覆盖默认的 hashcode() 方法,则哈希值在 hashcode() 方法被调用之后,会被记录到标记字之中)、对象的形状(是否是数组)、锁状态(偏向锁等锁信息,值得一提的是偏向锁在 Java 15 中废弃:Disable and Deprecate Biased Locking)、数组长度(如果标记显示这个对象是数组,描述了数组的长度)。标记字的实现仅仅包含一个uintptr_t类型,所以,在 32 位和 64 位虚拟机上面,大小分别是 4 字节和 8 字节。可以参考源码: markWord.hpp
  3. 类型字中保存了:指向对象实现的 Class 的指针。类型字默认是被压缩的,压缩指针指的就是这里。
  4. 默认哈希值计算,对于偏向锁是否生效,是有影响的。就是默认哈希值与偏向锁不能共存。
  5. 默认哈希值有缓存:无锁缓存在标记字;轻量锁缓存在锁记录,标记字中有指针指向锁记录,轻量锁释放后,锁记录中的哈希值复制到标记字中;重量锁缓存在 monitor 对象,标记字中有指针指向 monitor 对象,释放后,哈希值依然缓存在 monitor 对象中。
  6. 默认哈希值计算,需要考虑异步 monitor 降级的情况,这是 Java 15 中的新特性:Async Monitor Deflation
  7. 分代年龄在每次 Young GC 复制之后 +1,最大是 -XX:MaxTenuringThreshold=n配置的值,大于这个值就进入老年代了。
  8. 压缩指针是否启用和 Java 对齐字节大小(-XX:ObjectAlignmentInBytes,默认是 8,也就是 8 字节对齐)还有最大堆栈大小相关。
相关文章
|
7天前
|
传感器 监控 Java
Java代码结构解析:类、方法、主函数(1分钟解剖室)
### Java代码结构简介 掌握Java代码结构如同拥有程序世界的建筑蓝图,类、方法和主函数构成“黄金三角”。类是独立的容器,承载成员变量和方法;方法实现特定功能,参数控制输入环境;主函数是程序入口。常见错误包括类名与文件名不匹配、忘记static修饰符和花括号未闭合。通过实战案例学习电商系统、游戏角色控制和物联网设备监控,理解类的作用、方法类型和主函数任务,避免典型错误,逐步提升编程能力。 **脑图速记法**:类如太空站,方法即舱段;main是发射台,static不能换;文件名对仗,括号要成双;参数是坐标,void不返航。
27 5
|
1月前
|
存储 Java
Java中判断一个对象是否是空内容
在 Java 中,不同类型的对象其“空内容”的定义和判断方式各异。对于基本数据类型的包装类,空指对象引用为 null;字符串的空包括 null、长度为 0 或仅含空白字符,可通过 length() 和 trim() 判断;集合类通过 isEmpty() 方法检查是否无元素;数组的空则指引用为 null 或长度为 0。
|
2月前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
2月前
|
安全 Java
Object取值转java对象
通过本文的介绍,我们了解了几种将 `Object`类型转换为Java对象的方法,包括强制类型转换、使用 `instanceof`检查类型和泛型方法等。此外,还探讨了在集合、反射和序列化等常见场景中的应用。掌握这些方法和技巧,有助于编写更健壮和类型安全的Java代码。
54 17
|
2月前
|
Java
java代码优化:判断内聚到实体对象中和构造上下文对象传递参数
通过两个常见的java后端实例场景探讨代码优化,代码不是优化出来的,而是设计出来的,我们永远不可能有专门的时间去做代码优化,优化和设计在平时
38 15
|
4月前
|
安全 Java 编译器
Java对象一定分配在堆上吗?
本文探讨了Java对象的内存分配问题,重点介绍了JVM的逃逸分析技术及其优化策略。逃逸分析能判断对象是否会在作用域外被访问,从而决定对象是否需要分配到堆上。文章详细讲解了栈上分配、标量替换和同步消除三种优化策略,并通过示例代码说明了这些技术的应用场景。
Java对象一定分配在堆上吗?
|
4月前
|
JSON Java 程序员
Java|如何用一个统一结构接收成员名称不固定的数据
本文介绍了一种 Java 中如何用一个统一结构接收成员名称不固定的数据的方法。
62 3
|
4月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
290 4
|
5月前
|
Java API
Java 对象释放与 finalize 方法
关于 Java 对象释放的疑惑解答,以及 finalize 方法的相关知识。
91 17
|
4月前
|
存储 安全 Java
Java编程中的对象序列化与反序列化
【10月更文挑战第22天】在Java的世界里,对象序列化和反序列化是数据持久化和网络传输的关键技术。本文将带你了解如何在Java中实现对象的序列化与反序列化,并探讨其背后的原理。通过实际代码示例,我们将一步步展示如何将复杂数据结构转换为字节流,以及如何将这些字节流还原为Java对象。文章还将讨论在使用序列化时应注意的安全性问题,以确保你的应用程序既高效又安全。

热门文章

最新文章