6

承前启后,Java对象内存布局和对象头 - 小高先生o

 6 months ago
source link: https://www.cnblogs.com/GXH980804/p/18025256
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

承前启后,Java对象内存布局和对象头

大家好,我是小高先生。在我之前的一篇文章《并发编程防御装-锁(基础版)》中,我简要介绍了锁的基础知识,并解释了为什么Java中的任何对象都可以作为锁。在那里,我提到了对象头中有一个指向ObjectMonitor的指针,但没有深入探讨Java对象的内存结构。本文将引导大家深入了解Java对象的内存布局以及对象头结构,帮助大家更好地理解Java中的对象和锁,并为之后学习synchronized和锁升级打下基础。

  • new Object()怎么理解?
  • JOL
  • GC分代年龄

new Object()怎么理解?

平时我们开发的时候经常要new一个对象,很少想过这个对象存在哪里以及这个对象是有什么构成的,所以关于new Object的理解可归于两点:

  1. 位置所在(where)

学过JVM的同学都应该知道JVM内存结构,我们new出来的对象就在堆区,大对象在老年代,小对象在新生代。

  1. 构成布局(what)
2013963-20240221150853201-147538962.jpg

1.对齐填充

先讲一下对齐填充,这个比较简单。HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小是8字节的整数倍。对象头已经被设计成8字节的倍数,通常是1倍或2倍,如果实例数据大小不是8字节的整数倍,就需要用对齐填充添加一下,使对象大小为8字节的整数倍。

2.对象头

对象头是对象的另一个重要组成部分,它包含了一些关于对象的元信息。具体来说,对象头包括Mark Word和类元信息(类型指针)。

Mark Word

平时写代码的时候可能会通过hashcode()方法获取对象的哈希码,那哈希码保存在哪里呢?还有JVM中垃圾回收机制里说过,当一个对象在新生代经过15次Minor GC之后还存活下来就会进入老年代,那对象怎么记录GC次数呢?这些信息都记录在Mark Word中,先不需要都知道里面的标记代表什么,后续会讲到,这里只需要知道Mark Word存储了对象的一些重要信息。

存储内容 标志位 状态
对象哈希码、对象分代年龄 01 未锁定
指向锁记录的指针 00 轻量级锁定
指向重量级锁的指针 10 重量级锁定
空,不需要记录信息 11 GC标记
偏向线程ID、偏向时间戳、对象分代年龄 01 可偏向

在64位系统中,Mark Word占8字节,类型指针占8字节,一共16字节。所以当我们new一个对象,还没有实例数据,那这个对象就是16字节。64为系统下,Mark Word就是64bit大小,存储结构如下:

2013963-20240221150917100-202717134.png

比如现在对象是无锁状态,那Mark Word前25bit不用,之后的31bit存hashCode,偏向锁标志位为1,锁标志位为01,其他锁状态会在相关文章中讲解。

Mark Word默认存储hashCode、分代年龄和锁标志位等相关信息,这些信息都是与对象自身定义无关的数据,会根据对象的状态复用自己的存储空间,运行期间数据会随着锁状态而改变

类元信息(类型指针)

Phone ph = new Phone(),创建一个对象,会在JVM方法区存放对象的Klass类元信息,对象头的类型指针就是这个对象指向它的类元信息的指针,JVM通过类型指针确定这个对象是哪个类的实例,比如我创建Animal类,new Animal()实例的类型指针就会指向方法区中的Animal的Klass类元信息。由于Animal里面没有创建属性,所以Animal对象还没有实例数据,大小为16字节。

public class ObjectHeadDemo {
    public static void main(String[] args) {
        Animal a = new Animal();
    }
}
class Animal{

}

我给大家百度了Klass类元信息具体是什么,因为一开始我也不懂。

Klass类元信息是JVM中表示类的元信息的数据结构

JVM中,每个Java类都对应一个Klass实例,是用C++编写的一个类,存储了Java类的所有元信息,包括属性、方法、修饰符等。Klass模型是JVM内部使用的一个概念,是Java类的内部表示。这个模型包含了类的所有信息,如类的完整名称、访问修饰符、父类、实现的接口、属性和方法等。这些信息在类加载的过程中被读取并存储到方法区。

这么看起来,Klass就想是这个类的模板。

3.实例数据

class Animal{
	int id;
    boolean flag = false;
}

如果创建了一个Animal对象,大小就是对象头(16字节)+ int(4字节)+ boolean(1字节)= 21字节,需要有对齐填充,21 + 3 = 24字节。

有关Java对象布局的理论知识已经学完了,那能不能从代码层面验证一下对象的结构呢。

JOL(Java Object Layout)是一个专门用于分析Java虚拟机(JVM)中对象内存布局的工具箱。它能够提供非常精确的关于对象如何分布在JVM内存中的信息。

1.先在POM文件中导入坐标

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

2.通过指令查看JVM详细细节

public class JOLDemo {
    public static void main(String[] args) {
        //VM的详细细节
        System.out.println(VM.current().details());
        //获取当前JVM实例的对象对齐方式
        System.out.println(VM.current().objectAlignment());
       
    }
}
class Customer{
    int id;
    boolean flag = false;
}
2013963-20240221151018590-1460644276.png

Object alignment指的是对象在内存中的对齐方式。8 bytes表示对象的大小必须是8的倍数,即对象的起始地址必须是8的整数倍。

我们创建一个Object类,看看这个类具体的结构是什么样的,通过ClassLayout.parseInstance(o).toPrintable()来查看。

public class JOLDemo {
    public static void main(String[] args) {
        Object o = new Object();
        System.out.println(ClassLayout.parseInstance(o).toPrintable());
    }
}
2013963-20240221151603206-265505770.png
参数 作用
OFFSET 偏移量,也就是这个字段起始位置
SIZE 字节大小
TYPE Class中定义的类型
DESCRIPTION 类型描述
VALUE 是TYPE在内存中的值

我们这里会发现类型指针只占了4字节,之前说的是占8字节,这是为什么?这个我们稍后讨论。

另外一个细节会发现Instance size为16字节,因为对象头不够8的倍数,要补齐,也就是new一个对象默认大小为16字节。刚刚是创建了一个Object对象,现在再创建一个自己写的类看看结果,并且类中没有实例数据。结果表明自己写的类和Object类new出来的对象内存布局是一样的。

public class JOLDemo {
    public static void main(String[] args) {
        Object o = new Object();
        //System.out.println(ClassLayout.parseInstance(o).toPrintable());
        
        Customer c = new Customer();
        System.out.println(ClassLayout.parseInstance(c).toPrintable());
    }
}
class Customer{
	//只有对象头,没有实例数据
}
2013963-20240221151702111-6251358.png

另外一种情况是类中有实例数据,看看结果,这次除了对象头以外多了实例数据,然后也是用对齐填充补到8的倍数。

public class JOLDemo {
    public static void main(String[] args) {
        Object o = new Object();
        //System.out.println(ClassLayout.parseInstance(o).toPrintable());

        Customer c = new Customer();
        System.out.println(ClassLayout.parseInstance(c).toPrintable());
    }
}
class Customer{
    int id;
    String name;
}
2013963-20240221151802227-1383214273.png

现在来研究研究为什么类型指针是4而不是8,这是因为压缩指针的影响。使用java -XX:+PrintCommandLineFlags -version指令,启动Java程序的时候把所有参数打印出来。-XX:+UseCompressedClassPointers这个参数就表示使用了压缩指针,默认开启,结果就导致类型指针是4字节。

2013963-20240221151849040-1068800154.png

试试不开启指针压缩是什么样,正常工作中是不会用到的,这里就是跟大家尝试一下,看看结果而已。

2013963-20240221151952820-608570024.png
2013963-20240221152035656-957066138.png

GC分代年龄

GC年龄在Mark Word中用4bit存储,最大为15,也就是JVM参数-XX:MaxTenuringThreshold = 15。我们也可以验证一下,调整参数改成16试试,运行时是有异常的。

2013963-20240221152059247-1423133541.png
2013963-20240221152150724-195082079.png

本文和朋友们一起学习Java对象内存布局的知识,对象由对象头、实例数据和对齐填充组成。对象头包括Mark Word和类型指针,Mark Word记录一些对象的关键信息,主要包括锁的状态,类型指针是可以确定这个对象是哪个类的实例。JVM要求对象大小是8字节的整数倍,所以当对象头和实例数据大小不是8字节整数倍的时候,就需要对齐填充帮忙补齐。

东西不多但是挺重要的,可以更好地理解之前讲的synchronized底层分析,也可以对未来要学的锁升级做好准备。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK