1. 引言
Java 对象需要占用多少内存,这是一个经常被提及的问题。在缺少 sizeof
运算符的情况下,人们不禁想知道代码对其占用空间的影响。在本文中,我们将尝试窥视 Java 对象内部并查看其背后的内容。
2. 探讨更深入的设计与实现方面的问题
Deeper Design and Implementation Questions (DDIQ),在某些章节中,您可能会看到其中包含有关设计/实现问题的更多讨论。这些并不能保证回答所有问题,但他们确实尝试回答最常见的问题。答案基于个人的理解,因此可能是不准确,不完整或两者兼而有之。
3. 方法论考虑
这篇文章针对 Hotspot JVM
,OpenJDK 及其衍生版本的默认 JVM。如果你不知道运行的是哪种 JVM,则很可能就是 Hotspot
。
3.1 工具
为了正确地做到这一点,我们需要工具。当我们分析工具时,重要的是要了解工具可以做什么和不能做什么。
堆转储。转储 Java 堆并检查它可能很诱人。这似乎取决于以下信念:堆转储是运行时堆的低级表示。不幸的是,事实并非如此:它是从实际的 Java 堆重构而来的幻视。如果查看 HPROF 数据格式,您将看到它实际上是多么高级:它没有谈论字段偏移,也没有直接告诉标头任何东西,唯一的安慰就是那里的对象大小,这也是一个谎言。堆转储非常适合检查对象的整个视图及其内部连接,但用来检查对象本身太粗略了。
通过
MXBeans
测量可用或已分配的内存。我们当然可以分配多个对象,并查看它们占用了多少内存。分配足够的对象后,我们可以消除由TLAB 分配
(及其退役),后台线程中的虚假分配等导致的异常值。但是,这并不能使我们在查看对象内部时有任何保真度:我们只能观察到对象的外观大小。这是进行研究的一种好方法,但是您需要正确地制定和测试假设,以得出能够解释每个结果的明智的对象模型。诊断 JVM 标志。因为 JVM 本身负责创建对象,所以可以肯定它知道对象的布局,而且我们“仅”需要从那里得到它。
-XX:+ PrintFieldLayout
可以很容易的实现。不幸的是,该标志仅在debug JVM 版本
下可用。深入对象内部的工具。幸运的是,使用
Class.getDeclaredFields
并询问Unsafe.objectFieldOffset
可以使您知道字段所在的位置。这涉及到多个注意事项:首先,它使用反射功能侵入了大多数类,可能会被禁止使用;第二,Unsafe.objectFieldOffset
不会正式回答偏移量,而是一些 “cookie”,可以将其传递给其他 Unsafe 方法来使用。也就是说,它“通常是有效的”,因此,除非我们做至关重要的事情时才可以使用它侵入代码。某些工具(尤其是JOL
)可以为我们做到这一点。
在本文中,我们将使用 JOL
,因为我们希望看到 Java 对象的更精细的结构。对于我们的需求,使用 JOL-CLI
可执行 jar 包更合适,可以在这里找到:
$ wget https://repo.maven.apache.org/maven2/org/openjdk/jol/jol-cli/0.10/jol-cli-0.10-full.jar -O jol-cli.jar
$ java -jar jol-cli.jar
Usage: jol-cli.jar <mode> [optional arguments]*
Available modes:
internals: Show the object internals: field layout and default contents, object header
...
对于对象目标,我们将尽可能尝试使用各种 JDK 类本身。这样可以轻松验证整个过程,因为您只需要 JOL CLI JAR 和您喜欢的 JDK 安装即可运行测试。在更复杂的情况下,我们将转到 JOL 示例,其中涵盖了此处的一些内容。作为最后的说明手段,我们将使用其它示例类。
JOL 实例中的代码已经演示了大部分情景,可以自己动手运行并查看这些代码。
3.2 JDKs
JDK 8 仍然是世界上部署最广泛的 JDK 版本。因此,我们也会在这里使用它。
$ java -version
openjdk version "1.8.0_252"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_252-b09)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.252-b09, mixed mode)
4. 数据类型及其表示
我们需要从一些基础知识开始。在几乎每个 JOL “内部”运行中,您都会看到以下输出(为简便起见,在以后的调用中将省略此输出):
$ jdk8-64/bin/java -jar jol-cli.jar internals java.lang.Object
...
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
这意味着 Java 引用占用 4 个字节(启用指针压缩),boolean
/ byte
占用 1 个字节,char
/ short
占用 2 个字节,int
/ float
占用 4 个字节,double
/ long
占用 8 个字节。当作为数组元素呈现时,它们占据相同的空间。
为什么这很重要?因为 Java 语言规范没有说明有关数据表示的任何内容,仅说明了这些类型接受的值。原则上,可以为所有原语分配 8 个字节,只要对它们的存储范围超过其规范即可。在当前的 Hotspot 中,除 boolean
外,几乎所有数据类型都与它们的值域完全匹配。例如,指定为支持 int
从 -2147483648
到 2147483647
的值,该值正好适合 4 字节带符号的表示形式。
如上所述,有一个奇怪的地方,那就是 boolean
。原则上,其值域仅包含两个值:true
和 false
,因此可以用 1 位表示。所有 boolean
字段和数组元素仍然占据 1 个完整字节,这有两个原因:Java 内存模型保证了对于单个字段/元素不存在单词撕裂的情况,这对于 1 位布尔字段来说很难做到,并且字段偏移量以字节为单位进行内存寻址,这使得寻址布尔型字段很尴尬。因此,这里每个布尔值占用 1 个字节是一个实际的折衷方案。
5. Mark Word(标记信息)
回到实际的对象结构。让我们从最基本的 java.lang.Object
示例开始。 JOL 将打印此:
$ jdk8-64/java -jar jol-cli.jar internals java.lang.Object
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
Instantiated the sample instance via default constructor.
java.lang.Object object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 00 10 00 00 # (not mark word)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
它显示前 12 个字节是对象头。不幸的是,它没有更详细地解析其内部结构,因此,我们需要深入研究 Hotspot 源代码以弄清楚这一点。在这里,你会注意到对象头包括两部分:mark word
和 class word
。Class word 包含对象类型的信息:它链接到描述该类的本机结构。我们将在下一节中讨论这一部分。其余的元数据保存在 mark word 中。
mark word 有多种用途:
- 存储用于 GC 的元数据(分代年龄和转发数据)
- 存储身份标识 hash code
- 存储锁标志位信息
请注意,每个对象都必须有一个 mark word,因为它处理每个 Java 对象特有的事物。
5.1 存储用于 GC 的转发数据
GC 需要移动对象时,它们至少需要临时记录对象的新位置。mark word 将对此进行编码,以用于 GC 代码,以协调重定位和更新引用工作。这会将 mark word 锁定为与 Java 引用一样宽。GC 转发数据在 mark word 中实现所需要的最小内存量:32 位平台为 4 字节,而 64 位平台为 8 字节。
不幸的是,我们无法显示展示来自 Java 应用程序(而 JOL 是 Java 应用程序)进行 GC 转发的 mark word,因为要么在我们取消阻止 full GC 时就已经消失了。要么并发 GC 障碍阻止我们看到旧对象。
5.2 存储 GC 分代年龄
但是,我们可以演示对象分代年龄位!
$ jdk8-32/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_19_Promotion
# Running 32-bit HotSpot VM.
Fresh object is at d2d6c0f8
*** Move 1, object is at d31104a0
(object header) 09 00 00 00 (00001001 00000000 00000000 00000000)
^^^^
*** Move 2, object is at d3398028
(object header) 11 00 00 00 (00010001 00000000 00000000 00000000)
^^^^
*** Move 3, object is at d3109688
(object header) 19 00 00 00 (00011001 00000000 00000000 00000000)
^^^^
*** Move 4, object is at d43c9250
(object header) 21 00 00 00 (00100001 00000000 00000000 00000000)
^^^^
*** Move 5, object is at d41453f0
(object header) 29 00 00 00 (00101001 00000000 00000000 00000000)
^^^^
*** Move 6, object is at d6350028
(object header) 31 00 00 00 (00110001 00000000 00000000 00000000)
^^^^
*** Move 7, object is at a760b638
(object header) 31 00 00 00 (00110001 00000000 00000000 00000000)
^^^^
请注意,每一步移动都是如何向上计数的。那就是记录的对象年龄。奇怪的是,经过 7 次移动后,它在第 6 次停止。这是由于 InitialTenuringThreshold = 7
的默认设置。如果增加该值,则该对象将经历更多的移动,直到到达老年代为止。默认最多移动 15 次(1111),可以修改参数 -XX:MaxTenuringThreshold
调小。
5.3 身份标识信息
每个 Java 对象都有一个哈希码。如果用户没有为其定义,则使用*身份哈希码*。由于身份哈希码在为给定对象计算后不应更改,因此我们需要将其存储在某个位置。在 Hotspot 中,它直接存储在目标对象的 mark word
中。根据身份哈希码接受的精度,可能需要多达 4 个字节来存储。
DDIQ:当我们也需要存储 GC 转发数据时,该如何工作?
答案很狡猾:当 GC 移动对象时,它实际上处理对象的两个副本,一个在旧位置,一个在新位置。新对象带有所有原始头信息。旧对象仅用于满足 GC 需求,因此我们可以使用 GC 元数据覆盖旧对象头信息。
DDIQ:为什么我们需要存储身份哈希码?这如何影响用户指定的哈希码?
哈希码应该具有两个属性:a) 良好的散列分布,这意味着不同对象的值或多或少是不同的;b) 幂等,意味着具有相同关键对象组件的对象具有相同的哈希码。注意后者暗示如果对象没有更改那些关键对象组件,则其哈希码也不应更改。
对于身份哈希码,无法保证是否存在用于计算哈希码的字段,即使我们有一些字段,也无法得知这些字段的实际稳定性。考虑一下没有字段的
java.lang.Object
:它的哈希码是什么?分配的两个对象几乎是彼此的镜像:它们具有相同的元数据,它们具有相同(即空)的内容。关于它们的唯一区别是分配的地址,但是即使那样,仍然有两个麻烦。第一,地址具有非常低的熵,尤其是像大多数 Java GC 所采用的那样,是来自 bump-ptr 分配器,因此它的分布不均。其次,GC 会移动对象,因此地址不是幂等的。从性能的角度来看,返回恒定值也是行不通的。因此,当前的实现从内部 PRNG(“良好分布”)计算身份哈希码,并为每个对象存储它(“幂等”)。
可以通过相关的 JOLSample_15_IdentityHashCode 清楚地看到由身份哈希码引起的 mark word 变化。使用 64 位 VM 运行它:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_15_IdentityHashCode
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_15_IdentityHashCode$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) (1)
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8 4 (object header) 97 ef 00 f8 (10010111 11101111 00000000 11111000) (-134156393)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
hashCode: 2f333739
**** After identityHashCode()
org.openjdk.jol.samples.JOLSample_15_IdentityHashCode$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 39 37 33 (00000001 00111001 00110111 00110011) (859257089)
4 4 (object header) 2f 00 00 00 (00101111 00000000 00000000 00000000) (47)
8 4 (object header) 97 ef 00 f8 (10010111 11101111 00000000 11111000) (-134156393)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
请注意,哈希码值为 2f333739
。计算机内部为小端字节序,低位字节在前,高位字节在后,可以使用代码 ByteOrder.nativeOrder()
查看系统机器字节序。你现在可以在对象头中找到它的十六进制:01 39 37 33 2f
。 01
是 mark word 标记,其余是用 little-endian 小端字节序编写的身份哈希码。而且,我们还有 3 个字节的备用空间!
5.4 锁信息
Java 同步采用了复杂的状态机。由于每个 Java 对象都可以使用 synchronized
,因此锁定状态应与任何 Java 对象相关联。mark word 维护了大部分状态。
这些锁定转换的不同部分可以在对象头中看到。例如,当 Java 锁定偏向特定线程时,我们需要在相关对象附近记录有关该偏向锁 (Biased Lock) 的信息。这由相关的 JOLSample_13_BiasedLocking 示例演示:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_13_BiasedLocking
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_13_BiasedLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) # 无锁
4 4 (object header) 00 00 00 00
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
org.openjdk.jol.samples.JOLSample_13_BiasedLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b0 00 80 (00000101 11010000 00000000 01111011) # 偏向锁
4 4 (object header) b8 7f 00 00 # 偏向锁
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
org.openjdk.jol.samples.JOLSample_13_BiasedLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 b0 00 80 (00000101 11010000 00000000 01111011) # 偏向锁
4 4 (object header) b8 7f 00 00 # 偏向锁
8 4 (object header) c0 07 08 00
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
偏向锁指向线程的本地指针:b0 00 80 b8 7f
。
JEP 374 提案废除和禁用偏向锁,该提案在 JDK 15 及后续版本已经生效。
偏置锁定是 HotSpot 虚拟机中使用的一种优化技术,用于减少无竞争锁定的开销。它旨在避免在获取监视器时执行比较和交换原子操作,方法是假设监视器一直归给定线程所有,直到不同的线程尝试获取它。监视器的初始锁定使监视器偏向该线程,从而避免在对同一对象的后续同步操作中需要原子指令。当许多线程对以单线程方式使用的对象执行许多同步操作时,与常规锁定技术相比,偏置锁历来会导致显着的性能改进。
过去看到的性能提升今天远没有那么明显。许多受益于偏向锁定的应用程序是使用早期 Java 集合 API 的较旧的遗留应用程序,这些 API 在每次访问时使用同步(例如,
Hashtable
和Vector
)。较新的应用程序通常使用非同步集合(例如,HashMap
和ArrayList
),在 Java 1.2 中引入用于单线程场景,或者在 Java 5 中引入用于多线程场景的更高性能的并发数据结构。这意味着如果更新代码以使用这些较新的类,由于不必要的同步而受益于偏向锁定的应用程序可能会看到性能改进。此外,围绕线程池队列和工作线程构建的应用程序通常在禁用偏置锁定的情况下性能更好。偏向锁定带来了在争用情况下需要昂贵的撤销操作的成本。因此,受益于它的应用程序只有那些表现出大量无竞争同步操作的应用程序,如上面提到的那些。偏向锁定在同步子系统中引入了大量复杂的代码,并且对其他 HotSpot 组件也有侵入性。这种复杂性是理解代码各个部分的障碍,也是在同步子系统内进行重大设计更改的障碍。为此,我们希望禁用、弃用并最终删除对偏向锁定的支持。
没有偏向的情况下锁定时,会发生 JOLSample_14_FatLocking 如下锁升级情况:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_14_FatLocking
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 (00000001 00000000 00000000 00000000) # 无锁
4 4 (object header) 00 00 00 00
8 4 (object header) 5a ef 00 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** Before the lock
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 58 29 ad 04 (01011000 00101001 10101101 00000100) # 轻量级锁
4 4 (object header) 00 70 00 00
8 4 (object header) 5a ef 00 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a c1 80 e1 (01001010 11000001 10000000 11100001) # 重量级锁
4 4 (object header) f6 7f 00 00
8 4 (object header) 5a ef 00 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 4a c1 80 e1 (01001010 11000001 10000000 11100001) # 重量级锁
4 4 (object header) f6 7f 00 00
8 4 (object header) 5a ef 00 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After System.gc()
org.openjdk.jol.samples.JOLSample_14_FatLocking$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 09 00 00 00 (00001001 00000000 00000000 00000000) # 锁释放
4 4 (object header) 00 00 00 00
8 4 (object header) 5a ef 00 f8
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
各种情况下锁结构如下:
在这里,我们看到了锁的通常升级过程:首先对象未记录任何锁信息,然后被其他线程获取,并升级为轻量级锁,然后主线程争用它,锁发生膨胀,升级为重量级锁,然后在线程释放锁后,锁定信息仍保持为重量级锁。最后,在 GC 时的安全点后,对象恢复为无锁状态。
5.5 观察:身份 Hashcode 禁用偏向锁
但是,如果我们需要在偏向锁定生效时存储身份哈希码,该怎么办?很简单:身份哈希码优先,并且对该对象/类的偏向锁定被禁用。可以从相关示例 JOLSample_26_IHC_BL_Conflict 中看到:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
**** Fresh object
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 00 00 00 (00000101 00000000 00000000 00000000) # No lock
4 4 (object header) 00 00 00 00 (00000000 00000000 00000000 00000000)
8 4 (object header) 5a ef 00 f8 (01011010 11101111 00000000 11111000)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e0 80 c7 (00000101 11100000 10000000 11000111) # Biased lock
4 4 (object header) f7 7f 00 00 (11110111 01111111 00000000 00000000) # Biased lock
8 4 (object header) 5a ef 00 f8 (01011010 11101111 00000000 11111000)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 05 e0 80 c7 (00000101 11100000 10000000 11000111) # Biased lock
4 4 (object header) f7 7f 00 00 (11110111 01111111 00000000 00000000) # Biased lock
8 4 (object header) 5a ef 00 f8 (01011010 11101111 00000000 11111000)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
hashCode: 4cc77c2e
**** After the hashcode
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 2e 7c c7 (00000001 00101110 01111100 11000111) # Hashcode
4 4 (object header) 4c 00 00 00 (01001100 00000000 00000000 00000000) # Hashcode
8 4 (object header) 5a ef 00 f8 (01011010 11101111 00000000 11111000)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** With the second lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 80 59 9e 02 (10000000 01011001 10011110 00000010) # Lightweight lock
4 4 (object header) 00 70 00 00 (00000000 01110000 00000000 00000000) # Lightweight lock
8 4 (object header) 5a ef 00 f8 (01011010 11101111 00000000 11111000)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
**** After the second lock
org.openjdk.jol.samples.JOLSample_26_IHC_BL_Conflict$A object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 2e 7c c7 (00000001 00101110 01111100 11000111) # Hashcode
4 4 (object header) 4c 00 00 00 (01001100 00000000 00000000 00000000) # Hashcode
8 4 (object header) 5a ef 00 f8 (01011010 11101111 00000000 11111000)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
刚开始对象通过偏向锁定的宽限期后为无锁状态,可偏向(锁标记为 …101)。第一次进入代码同步块时,偏向锁通过 CAS 操作将线程 ID 置换到 mark word 中。在释放锁后,偏向锁仍然会保留在对象头中,以便于减少单个线程未竞争情况下时锁定和解锁的 CAS 操作。
一旦我们调用了对象的 hashcode 方法,将触发 identity hashcode
的计算(我们的对象没有重写 Object.hashcode
),偏向锁被撤销为无锁,线程 ID 置换为 hashcode。
第二次执行到方法对象同步时,由于存放了 hashcode,这时检查到为不可偏向的无锁标志,虚拟机会将当前的 mark word
备份到当前线程方法的栈桢的 Lock Record
中,并通过 CAS
操作将 Lock Record
指针更新到对象的 mark word
中,如果 CAS 操作成功,那么该线程就获取了该对象上的锁,并且对象的 mark word 锁标记变为 00,表示该对象处于轻量级锁状态。在代码退出同步块时,轻量级锁释放,再通过 CAS 操作把对象当前的 mark word 和线程中复制的替换回来。
6. Class Word
从机器的角度来看,每个对象只是一堆字节。在某些情况下,我们想知道在运行时处理的对象的类型是什么。需要的非详尽清单:
- 运行时类型检查
- 确定对象的大小
- 找出虚拟/接口调用的目标。
Class word 也可以被压缩。即使类指针不是 Java 堆引用,它们仍然可以享受类似的优化。
6.1 运行时类型检查
Java 是一门类型安全的语言,所以在很多地方都需要运行时类型检查。Class word 携带有关我们拥有的对象的实际类型的数据,这使编译器可以发出运行时类型检查。这些运行时检查的效率取决于元数据类型的形状。
如果元数据以简单的形式编码,则编译器甚至可以直接在代码流中内联那些检查。在 Hotspot 中,Class word 持有指向 VM Klass
实例的本机指针(Klass Pointer),该实例包含大量元信息,包括它继承的超类的类型,实现的接口等,它还带有 Java 镜像 - java.lang.Class
的关联实例。这种间接方式允许将 java.lang.Class
实例视为常规对象,并在 GC 期间不更新每个 class word 的情况下移动它们:java.lang.Class
可以移动,而 Klass
始终保持在同一位置。
6.2 确定对象大小
确定对象大小采用相似的方法。与运行时类型检查无法静态地知道对象的类型相比,分配确实或多或少地精确地知道了分配对象的大小:它由使用的构造函数的类型,使用的数组初始化器等定义。因此,在这些情况下,不需要通过 class word 进行访问。
但是,本机代码中有一些情况(最著名的是垃圾收集器)想要使用以下代码遍历可解析堆:
HeapWord* cur = heap_start;
while (cur < heap_used) {
object o = (object)cur;
do_object(o);
cur = cur + o->size();
}
为此,本机代码需要提前知道当前(未确定类型)object 的大小。因此,对于本机代码,如何安排类元数据非常重要。在 Hotspot 中,我们可以遍历 class word 访问布局助手,这将为我们提供有关对象大小的信息。
6.3 找出虚方法/接口调用的目标
当运行时需要在对象实例上调用虚拟/接口方法时,它需要确定目标方法在哪里。虽然大多数时间可以优化,但在某些情况下,我们需要进行实际的调度。该调度的性能还取决于类元数据的距离,因此不能忽略这一点。
6.4 观察:压缩引用影响对象头文件
与根据 JVM 位数观察 mark word 大小类似,我们也可以期待压缩引用模式会影响对象大小,即使不涉及引用字段。为了证明这一点,让我们在两个堆大小上使用 java.lang.Integer,小 (1 GB) 和大 (64 GB)。默认情况下,这些堆大小将分别打开和关闭压缩引用。这意味着默认情况下压缩类指针也是打开或关闭的。
$ jdk8-64/bin/java -Xmx1g -jar jol-cli.jar internals java.lang.Integer
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
Instantiated the sample instance via public java.lang.Integer(int)
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) de 21 00 20 # Class word
12 4 int Integer.value 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
$ jdk8-64/bin/java -Xmx64g -jar jol-cli.jar internals java.lang.Integer
# Running 64-bit HotSpot VM.
Instantiated the sample instance via public java.lang.Integer(int)
java.lang.Integer object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 40 69 25 ad # Class word
12 4 (object header) e5 7f 00 00 # (uncompressed)
16 4 int Integer.value 0
20 4 (loss due to the next object alignment)
Instance size: 24 bytes # AHHHHHHH....
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
这里,在具有 1 GB 堆的 VM 中,对象头占用 8(mark word)+ 4(class word)= 12 个字节,而 64G VM 头占用 8(mark word)和 + 8(class word)= 16 个字节。如果没有字段,由于对象对齐方式为 8,两者都会四舍五入到 16 个字节。 但是,由于有一个 int 字段,在 64 GB 的情况下,我们需要将其分配超过 16 个字节,因此需要另外 8 个字节,总共占用 24 个字节。
7. 对象头:数组长度
数组带有另一小部分元数据:数组长度。由于数组元素类型仅编码对象类型,因此我们需要将数组长度存储在其他位置。
可以通过相关的 JOLSample_25_ArrayAlignment 查看:
$ jdk8-64/bin/java -cp jol-samples.jar org.openjdk.jol.samples.JOLSample_25_ArrayAlignment
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
[J object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) d8 0c 00 00 # Class word
12 4 (object header) 00 00 00 00 # Array length
16 0 long [J.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
...
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 00 00 00 00 # Array length
16 0 byte [B.<elements> N/A
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 01 00 00 00 # Array length
16 1 byte [B.<elements> N/A
17 7 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 02 00 00 00 # Array length
16 2 byte [B.<elements> N/A
18 6 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 6 bytes external = 6 bytes total
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 03 00 00 00 # Array length
16 3 byte [B.<elements> N/A
19 5 (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 5 bytes external = 5 bytes total
...
[B object internals:
OFFSET SIZE TYPE DESCRIPTION VALUE
0 4 (object header) 01 00 00 00 # Mark word
4 4 (object header) 00 00 00 00 # Mark word
8 4 (object header) 68 07 00 00 # Class word
12 4 (object header) 08 00 00 00 # Array length
16 8 byte [B.<elements> N/A
Instance size: 24 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
偏移 +12 处有一个插槽,用于承载数组长度。当我们分配 0…8 个元素的 byte[] 数组时,该插槽不断变化。将 arraylength 与数组实例一起携带有助于计算对象遍历器的实际大小,并且还可以进行有效的范围检查,以使数组长度非常接近。