Java对象在内存中是如何表示的

date
Jul 20, 2020
slug
Java对象在内存中是如何表示的
status
Published
tags
Java
summary
JVM 规范中没有限制对象在内存中是怎么表示的,具体的设计方式取决于各 JVM 实现,我们就以最常见的 HotSpot 为例来说明。 HotSpot JVM 是用一种叫做 Ordinary Object Pointer(OOP)的数据结构来表示指向对象的指针。在 OOP 中,最重要的是 mark word 和klass word:
type
Post

背景知识

JVM 规范中没有限制对象在内存中是怎么表示的,具体的设计方式取决于各 JVM 实现,我们就以最常见的 HotSpot 为例来说明。
HotSpot JVM 是用一种叫做 Ordinary Object Pointer(OOP)的数据结构来表示指向对象的指针。在 OOP 中,最重要的是 mark word 和klass word:
  1. mark word 中包含了唯一标识一个对象的hashcode,关于锁的信息,GC的信息。mark word占用4字节(32位架构中,64位架构中是8字节),其中关于锁的信息,在对象使用偏向锁和普通锁的时候,存储方式是完全不同的。
  1. klass word则存储了一些编程语言层面的类相关的信息,比如类名,修饰符,父类信息等等。通常有4字节大小。
刚才说了,OOP中最重要的是mark word 和klass word,因为不管指向的是个普通对象,还是一个array,这两部分都是存在的。
除此之外,对于array来说,另外还要存array的长度;根据整个对象头的大小不同,还可能有一些用于对齐的填充字节。

调试环境

相关代码可以在这里找到
想直观的观察到具体的内存结构,我们需要使用JOL这个工具,添加jol-core这个依赖就可以了。

GET HANDS DIRTY

下面我们来看看一个普通对象在JVM中到底是怎么表示的,执行这段代码
可以看到以下输出
意思也挺直白,告诉我们运行环境是64位的HotSpot虚拟机,对齐长度是8字节。我们重点看后面两行,表示的是各类型占用的内存空间。按照顺序,后面两行意思就是: 1. 对象的引用是4字节、boolean和byte占用1字节、short和char占用2字节、int和float占用4字节、long和double占用8字节 2. 如果我们把对象放到array里,作为array 的元素的话,也占用这么多空间

Class Layout

我们有这么一个类
把它的class layout打印出来看看
可以看到,对象头占了12字节,state属性占了4字节,总共对象占了16字节;这个对象头的12字节,刚好印证了mark word的8字节+klass word的4字节。因为我们这里打印的是class 的layout,所以后面的VALUE显示的都是N/A。

对象默认的hash code

Object提供的默认方法之一就是hashCode(),当我们没有重写这个方法时,默认的native实现返回的就是mark word中的identity hashcode。
hashcode应该在对象的整个生命周期内都不会发生变化,所以HotSpot把它存在了mark word里。让我们看一看
为了方便阅读,我加了一点注释,其中0~4,4~8字节的内容是mark word,8~12字节是klass word,接下来int属性占了4字节。这个实例整个占用了16字节。值得注意的是,mark word的前4字节现在值是1,后4字节现在值是0;我们开始说过了,identity hashcode是存在mark word中的,这显然有问题。
其实这个hashcode是延迟计算的,用到的时候才会计算它的值,如果我们尝试调用对象的hashCode()方法,就能发现mark word的内容变了。
上面结果中的ae 04 6a 70就是hashcode,由于JVM是采用小端法存储(低位地址存储低位字节)多字节信息的,所以我们应该按照70 6a 04 ae的顺序理解这段内容,转换成10进制的话,正是上面打印的1885996206

对齐

回到最开始,在我们打印VM.current().details()的时候,有这么一句输出:
说的就是对齐的信息,上面是说对象是按8字节来对齐的。我们上面的例子中,都有这么一句
这是因为前面的例子中,OOP的大小都是16字节,正好是8的倍数,所以不用对齐。让我们看看需要对齐的例子
可以看到,因为我们的AlignmentSimpleInt类中state变成了long,占用8个字节,整个OOP应该占用12+8=20字节才对。由于这个原因,出现了4个字节的alignment/padding gap,把整个OOP的占用内存变成了24字节。就是因为要按8字节来对齐,所以需要填充4个字节,使整个OOP的大小是8的倍数。

属性重排

当一个类有多个属性的时候,JVM可能会出于要降低填充字节数的原因(解约内存避免浪费)而把属性进行重排,我们在代码中声明的顺序,不一定是OOP中表示的顺序,比如我们有这么一个类
可以看到,OOP中的顺序,跟我们声明的顺序不同,因为按照我们声明的顺序,显然要用到更多的空间来存储。如果正好这个类在应用中要成千上万次的实例化,那么属性重排避免的空间浪费就很可观了。

关于锁

开头提到,mark word中也有锁相关的信息,我们可以简单的看一下
注意mark word的第一个字节,值是0x01,如果我们执行一些锁相关的操作
可以看到第一个字节的值变成了0x60其他位的模式也发生了变化。

年龄和分代

我们知道GC中会利用分代来提高效率,有一些对象会在一段时间后进入老年代,要达到这个目的,JVM就必须知道每个对象存活了多久。这个信息也是存在mark word中的。在运行时,一个对象的地址发生变化了,很有可能就是因为minor GC发生了。所以我们通过大量的创建对象,来促使JVM进行minor GC。来观察mark word的变化
输出的内容有点多,我只把mark word的前4字节截取出来就是这样的
可以看到,每次地址发生变化,这个部分的值就+1,其实这中间记录的就是对象的年龄。
Java对象在内存中是如何表示的
JVM 规范中没有限制对象在内存中是怎么表示的,具体的设计方式取决于各 JVM 实现,我们就以最常见的 HotSpot 为例来说明。
HotSpot JVM 是用一种叫做 Ordinary Object Pointer(OOP)的数据结构来表示指向对象的指针。在 OOP 中,最重要的是 mark word 和klass word:
JVM 规范中没有限制对象在内存中是怎么表示的,具体的设计方式取决于各 JVM 实现,我们就以最常见的 HotSpot 为例来说明。
HotSpot JVM 是用一种叫做 Ordinary Object Pointer(OOP)的数据结构来表示指向对象的指针。在 OOP 中,最重要的是 mark word 和klass word:
JVM 规范中没有限制对象在内存中是怎么表示的,具体的设计方式取决于各 JVM 实现,我们就以最常见的 HotSpot 为例来说明。
HotSpot JVM 是用一种叫做 Ordinary Object Pointer(OOP)的数据结构来表示指向对象的指针。在 OOP 中,最重要的是 mark word 和klass word:
JVM 规范中没有限制对象在内存中是怎么表示的,具体的设计方式取决于各 JVM 实现,我们就以最常见的 HotSpot 为例来说明。
HotSpot JVM 是用一种叫做 Ordinary Object Pointer(OOP)的数据结构来表示指向对象的指针。在 OOP 中,最重要的是 mark word 和klass word:

© oddcc 2020 - 2024