初探 Java 对象头

本贴最后更新于 1702 天前,其中的信息可能已经时异事殊

gwr8hdzgncY.jpg

写在前面

在 Java 程序的运行过程中,会被创建出许许多多的对象,这些对象可能是我们自己写的一个类,也可能是 JDK 自带的一些类被我们创建了对象。那么,我们创建出来的对象里面都装了些什么呢?

说到装载的东西,我们首先想到的就是里面会装载我们给对象自定义的一些属性,比如下面这个类:

public class Student {
    private String name;
}

我们在创建了这个对象之后,这个存储在堆上的对象就会肯定会包含一个变量 name,拿出了这个对象属性,里面还有什么东西呢?

经过初步资料查询之后,关于新创建的对象,会保存一下数据:

  • Java 对象头
  • 对象属性
  • 对齐数据填充

什么是数据对齐

要知道什么是数据对齐,就要先看到一个对象在 Java 中到底是长成什么样子的,这里我们我们引用一个 Maven 依赖 jol(Java Object Layout)

查看 Java 对象头

image20200308204353475.png

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

当引入这个以来之后,就可以看到 Java 对象再内存中到底长什么样子,下面来看一下用法:

先看一下我们的测试对象

public class Student {
    private String name;
}

然后测试代码如下

import com.gouzhong1223.domain.Student;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * @Author : Gouzhong
 * @Blog : www.gouzhong1223.com
 * @Description : 查看 Java 对象信息
 * @Date : create by Gouzhong in 2020-03-02 4:31 下午
 * @Email : gouzhong1223@gmail.com
 * @Version : 1.0.0
 */
public class Demo1 {
    public static void main(String[] args) {
      	// 创建新对象
        Student student = new Student();
      	// 查看虚拟机信息
        System.out.println(VM.current().details());
      	// 查看对象信息
        System.out.println(ClassLayout.parseInstance(student).toPrintable());
    }
}

运行上面的 main 方法,就会得到一下结果:

image20200308205023692.png

红线以上就是我们的虚拟机参数信息,显然我们现在使用的是 64 位的 HotSpot 虚拟机。红线以下就是我们的对象信息,这里可以看到 Object Header 是占用了 12 个 Byte,我们的实例数据 name 占用了 4 个 Byte,因为字符串在 Java 中是占用 4 个 Byte 的。但是这里我们没有看到关于数据填充的东西啊,难道 Oracle 的文档是错误的?

为什么没有看到数据对齐?

不急,这里我们把刚才的测试对象升级一下:

public class Student {
    private String name;
    private String addr;
}

按照刚才的逻辑,我们新增了一个字符串字段之后,应该在 16Byte 的基础上增加 4Byte 也就是总共占用 20Byte,那么事实真的是这样么?

我们再来运行一下刚才那个 main 方法,看一下得到的结果有什么变化

image20200308210154536.png

运行出来的结果居然是 24 Byte,除了新增的 4 Byte,还有一段这样的描述:

20     4                    (loss due to the next object alignment)

意思就是说,虚拟机刚我们在这个对象上补充了 4Byte 的空位置。由于操作系统以及 CPU 的限制,我们的程序在运行以及使用的过程中,内存中单个文件的大小最好是 8Byte 的整数倍,按照以往逻辑,我们这个对象在内存中应该是占有 20Byte 的大小,但是这对于我们的操作系统是非常不变的,所以 JVM 就会自动的将对象大小扩容到 8Byte 的整数倍,也就是 24Byte。

这就是数据填充,具体填充多少,这个也是不固定的。

除了数据填充以及对象实例字段,最上面的就是对象头信息。

什么是 Java 对象头

在上面的运行测试中,我们查看了两个不同的对象信息,发现对象头都是占用 12Byte,那这 12 个 Byte 中储存了什么呢?

HotSpot 文档对于对象头的定义

在 HotSpot 的文档中,可以看到,Java 对于对象头是这样定义的:

image20200308202756107.png

object header

Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

上面的意思就是,在 Java 对象头中,储存了堆对象的布局,类型,GC 状态,同步状态和标识哈希码的基本信息,

这些信息都是储存在一个 Mark Word 里面的,那什么是 Mark Word 呢?

关于 Mark Word

Mark Word

The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

每个对象标头的第一个单词。通常,一组位域包括同步状态和标识哈希码。也可以是指向与同步相关的信息的指针(具有特征性的低位编码)。在 GC 期间,可能包含 GC 状态位。

打开 HotSpot 的源码,我们可以看到在虚拟机中是如何定义 Mark Word 的

image20200308211716431.png

可以看到,在 32 位的虚拟机和 64 位的虚拟机中,Mark Word 的大小是不一样的,在网上能够看到写 Mark Word 的文章,基本上都是基于 32 位虚拟机来写的,但是现在已经是 2020 年了,我相信已经没有人会用 32 位的电脑了吧,哈哈哈哈。

言归正传,正是因为 32 位的虚拟机和 64 位虚拟机的差异,所以才导致我们在照着代码敲的过程中会出现和博客不一样的结果。

对象的状态

Java 对象有 5 种状态

  1. 新创建(无状态)
  2. 偏向锁
  3. 轻量级所
  4. 重量级锁
  5. GC 标记

对应上面 C++ 源码的注释,不难看到,在一个对象是无状态的时候,对应的是下面这种情况:

unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)

HashCode 真的存在么?

回顾一下我们刚刚运行的程序,那个就是一个无状态的对象啊,为什么没有看到 HashCode 呢?难道这个 HotSpot 的源码是假的?不急不急,在探究这个问题之前,我们向来想一想,HashCode 真的存在么??

答案是:不存在!!!!!!

我们来看一下 HashCode 这个方法:

image20200308212631767.png

从上面可以看出,HashCode 是一个 native 方法,也就是说这个其实是 C++ 来帮我们实现的,如果我们没有调用这个方法,C++ 怎么知道你要计算 HashCode,所以,如果我们没有为某个对象的 hashCode 方法之前,对象是没有 HashCode 的。

现在来改进一下刚刚那个测试,将代码更改为一下代码:

import com.gouzhong1223.domain.Student;
import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

/**
 * @Author : Gouzhong
 * @Blog : www.gouzhong1223.com
 * @Description : 查看 Java 对象头
 * @Date : create by Gouzhong in 2020-03-02 4:31 下午
 * @Email : gouzhong1223@gmail.com
 * @Since : JDK 1.8
 * @ProjectName : ObjectHeader
 * @Version : 1.0.0
 */
public class Demo1 {
    public static void main(String[] args) {
        Student student = new Student();
        System.out.println(student.hashCode());
        System.out.println(VM.current().details());
        System.out.println(ClassLayout.parseInstance(student).toPrintable());
    }
}

在输出对象信息之前,先调用一下这个对象的 hashCode 方法,然后我们再来运行就会得到不一样的结果image20200308213057502.png

什么是大端存储

好啦,现在就可以看到这个地方是不为 0 的了,但是为什么这一串数据看起来怪怪的啊?难道是机器出 Bug 了??? No No No,解释这个之前,这里又要引入一个新的概念:

在 Intel X86 架构的处理器中,我们的计算机是采用大端存储模式的,什么是大端存储模式呢?

意思就是说,高字节,保存在低地址中,低字节保存在高地址中,我的这一台电脑就是 intel 的 CPU,所以我们在分析对象头的时候,**应该倒着看!**倒着看那就好理解多了,除去那 25 个未使用的,剩下的就是 HashCode,GC 分代年龄,锁状态,锁标记。

image20200308214256954.png

那么问题又来了,为什么除了一个锁标记,还要有一个锁状态呢?

很简单,我们看一下,这个所标记占有 2 个 bit,两个 bit 能够表示的状态,只有 4 种

  1. 00
  2. 01
  3. 10
  4. 11

但是我们上面说了,对象有五种状态,这个显然是不能够完全表示的,所以就有一个锁状态来辅助标记

比如说新创建的时候就是 0 00

偏向锁的时候就是 1 00

这样就能多表示一种状态了。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3186 引用 • 8212 回帖
  • 对象头
    1 引用

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...