Java是一门面向对象的编程语言,对象无时无刻不在被创建出来,首先介绍以下对象的内存布局,Hotspot虚拟机对象的内存布局包含三部分
- 对象头(Header)
- 实例数据(Instance Data)
- 对齐填充(Padding)
实例数据位对象包含的字段信息,对齐填充位非必须内容,这里不做详细分析。
在HotspotVM中对象的创建过程概括以下几个步骤:
- 类加载(加载、链接、初始化)
- 内存分配(根据不同的垃圾收集器)
- 初始化(设置零值)
- 设置对象头(设置对象头部信息,了解对象内存分布)
-
类加载过程
关于类加载过程,可以参考类加载篇。
内存分配
简单来说内存分配就是把Java堆(heap)中的一大块空间,划分出来一块大小明确的空间。关于内存划分不仅要考虑空间分配,还要考虑分配过程中的并发问题。接下来介绍两种问题在Java虚拟机中的处理方案:
内存分配空间划分问题
根据堆空间的剩余内存是否规整在划分的时候可以分为指针碰撞和空闲列表两种方式。空间划分是否规整又要取决于使用的垃圾回收算法,而具体的垃圾回收算法要看不同的虚拟机实现,对于HotspotVM,不同版本使用的垃圾收集器原理也有所不同,Serial、ParNew等带压缩整理过程的收集器时,虚拟机采用的分配算法是指针碰撞,既简单又高效;而当使用CMS这种基于清除(Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。下面是两种分配方式在处理方案上的不同点:
- 指针碰撞(Bump The Pointer)
假设Java的堆内存空间划分绝对规整,使用过的放在一块,未使用的划分在另一块,以指针作为临界点,那内存分配就是将指针向空闲空间方向移动与待分配对象大小的距离。 - 空闲列表(Free List)
如果Java的堆已使用空间和未使用空间交错在一起,那就需要维护一个未使用的列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。
- 内存分配并发问题
除了空间分配问题外,Java虚拟机还要解决并发问题,对象创建在虚拟机中是比较频繁的一个动作,在并发场景下修改指针指向的位置是不安全的,同一块内存区域可能同时被两个线程操作,指针还未来得及修改,又在原来指针基础上分配。处理这种并发问题一般来说有两种解决方案。
- 分配内存使用同步处理:虚拟机使用CAS配合失败重试方案保证分配动作操作的原子性
使用线程TLAB分配:(即Thread Local Allocation Buffer)每个线程在Java堆中预先分配一部分内存空间,当线程进行对象创建时预先使用缓冲区,缓冲区使用完毕再进行同步分配。Java中可以使用-Xss为JVM启动的每个线程分配的内存大小,JDK1.4中是256K,JDK1.5+中是1M,使用-XX:+/-UseTLAB指定线程是否开启缓冲区预分配。
初始化零值
Java虚拟机完成内存分配后,需要将分配的内存空间进行初始化零值(不包含对象头)操作,如果使用了TLAB,也可以在TLAB中顺便进行,这一步骤可以保证Java对象实例中的字段即使没有初始值也可以使用,此时程序访问到的字段初始值即为字段所对应数据类型的零值。比如String类型零值为’’,int类型零值为0等。
设置对象头
在对象字段中零值设置完成后需要对字段进行其他信息的设置,如对象属于哪个类的实例,如何找到类的元数据信息,对象的hash码(对象的hash码一般会预留到调用对象的hashcode方法时生成),以及对象的GC分代年龄信息等,这些信息都存储在对象头中(Object Header),此外对象头中还会包含偏向锁等信息。
对象头主要包含对象自身运行时数据,主要包含mark word
,klass pointer
两部分,mark word
在32位和64位的虚拟机实现中长度分别为32位和64位。mark word
,存储了同步状态、标识、hashcode、GC状态等;klass pointer
,存储了对象的类型指针,以此来判断是哪个类的实例。
对象头存储内容字节占用情况如下:
锁状态 | 32bit | ||||
---|---|---|---|---|---|
25bit | 4bit | 1bit | 2bit | ||
23bit | 2bit | 偏向模式 | 标志位 | ||
未锁定 | 对象哈希码 | 分代年龄 | 0 | 01 | |
轻量级锁 | 指向调用栈中锁记录的指针 | 00 | |||
重量级锁 (锁膨胀) |
指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 | |||
可偏向 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
在32位虚拟机中,对象未被同步锁定的状态,空间使用主要为25bit存储哈希码,4bit存储对象分代年龄,2bit用来存储锁标志位,1bit固定为0,对象存储格式如下
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
在64位虚拟机中,JVM会默认使用选项 +UseCompressedOops 开启指针压缩,将指针压缩至32位,可以降低内存使用量50%,对象存储内容如下:
|--------------------------------------------------------------------------------------------------------------|
| Object Header (128 bits) |
|--------------------------------------------------------------------------------------------------------------|
| Mark Word (64 bits) | Klass Word (64 bits) |
|--------------------------------------------------------------------------------------------------------------|
| unused:25 | identity_hashcode:31 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 无锁
|----------------------------------------------------------------------|--------|------------------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | lock:2 | OOP to metadata object | 偏向锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_lock_record:62 | lock:2 | OOP to metadata object | 轻量锁
|----------------------------------------------------------------------|--------|------------------------------|
| ptr_to_heavyweight_monitor:62 | lock:2 | OOP to metadata object | 重量锁
|----------------------------------------------------------------------|--------|------------------------------|
| | lock:2 | OOP to metadata object | GC
|--------------------------------------------------------------------------------------------------------------|
关于 Mark Word和Klass Word各部分含义:
- lock: 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
- biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。
- age:Java GC标记位对象年龄。
- identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。
- thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向线程Monitor的指针。
对于锁状态位,不同标志对应锁为:
biased_lock | lock | 状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
执行构造方法
对象头设置完成后,从Java虚拟机的角度来说一个新的对象已经产生,从Java程序触发,对象的创建刚刚开始,接下来开始执行new关键字所触发的构造函数调用。即Class文件中的
// class version 52.0 (52)
// access flags 0x21
public class com/starsray/test/TestNew {
// compiled from: TestNew.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 3 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/starsray/test/TestNew; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 6 L0
NEW com/starsray/test/TestNew
DUP
INVOKESPECIAL com/starsray/test/TestNew.<init> ()V
POP
L1
LINENUMBER 7 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 2
MAXLOCALS = 1
}
JOL使用
JOL全称Java Object Layout,Java对象布局,可以查看一个对象包含的数据信息,打破你对Java对象的想象,以数据说话。JOL使用起来也很简单。
引入maven依赖
<!-- JOL依赖 -->
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
代码示例:
/**
* TestJOL
*
* @author starsray
* @since 2021-11-25
*/
public class TestJOL {
public static void main(String[] args) {
TestJOL jol = new TestJOL();
System.out.println(ClassLayout.parseInstance(jol).toPrintable());
}
}
输出结果:
jol.TestJOL 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) 05 c1 00 f8 (00000101 11000001 00000000 11111000) (-134168315)
12 4 (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
- 第一行内容和锁状态内容对应
unused:1 | age:4 | biased_lock:1 | lock:2
0 0000 0 01 代表A对象正处于无锁状态 - 第三行中表示的是被指针压缩为32位的klass pointer
- 第四行则代表了对象的对齐字段 为了凑齐64位的对象,对齐字段占用了4个字节,32bit
- 第三行与第四行之间展示的是对象的属性值,这里没有属性,展示为空。