概述:以最常用的虚拟机HotSpot和最常用的内存区域Java堆为例,深入探讨HotSpot虚拟机在Java堆中对象分配、布局和访问的全过程。
1 对象的创建
- 类加载检查:
- 当JVM遇到一条
new
指令时,首先检查常量池中是否有这个类的符号引用,并且检查这个类是否已经被加载、解析和初始化过。如果没有,先执行类加载过程。
- 当JVM遇到一条
- 内存分配:
- 类加载完成后,JVM为对象分配内存。
- 对象所需内存的大小在类加载后便完全确定。
- 内存分配方法取决于Java堆的情况:
- 指针碰撞(Bump The Pointer):如果堆内存是规整的,分配仅需移动指针(该指针分隔开了占用内存与空闲内存)。
- 空闲列表(Free List):如果堆内存是不规整的,JVM则需要维护一个记录可用内存块的列表。
- java堆是否规整由所采用的垃圾收集器是否带有空间压缩整理(Compact)的能力决定。
- 类加载完成后,JVM为对象分配内存。
- 内存分配中的线程安全:
- 由于对象频繁创建,内存分配需要处理线程安全。解决方案包括:
- 使用CAS(Compare And Swap)配上失败重试的方式保证更新操作的原子性。
- 使用本地线程分配缓冲(Thread Local Allocation Buffer,TLAB),即每个线程预分配一小块内存,只有分配新的缓冲区时才需要同步锁定。
- 由于对象频繁创建,内存分配需要处理线程安全。解决方案包括:
- 内存初始化:
- 分配的内存(除对象头外)需初始化为零值,确保字段默认值正确,如使用TLAB则可以将此步骤提前至分配缓冲区时执行。
- 对象头设置:
- 设置对象的元数据信息,如类的类型、哈希码、GC信息等。
- 执行
<init>
方法:- JVM视角上对象已创建,但从Java程序的视角来看,对象的构造函数(即Class文件中的()方法)还需执行,完成字段初始化和其他资源的设置。
2 对象的内存布局
三个主要部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
-
对象头(Object Header):
-
Mark Word:存储对象自身的运行时数据,如哈希码、GC分代年龄、锁状态标志等。这部分数据的长度取决于系统是32位还是64位。Mark Word有着动态定义的数据结构,会根据对象的状态(如是否被锁定)而有所不同。
-
类型指针:指向类元数据的指针,用于确定对象是哪个类的实例。查找对象的元数据信息并不一定要经过对象本身(见2.2.3)。
-
数组长度:如果对象是数组,那么对象头还会包含数组长度信息,因为当数组的长度不固定时,无法通过类的元数据推断出数组的大小。
-
-
实例数据(Instance Data):
- 存储对象的实际有效信息,包括类中定义的各种字段内容,无论是继承的还是在类中直接定义的。
- 实例数据的存储顺序受到JVM分配策略和字段在Java源码中的定义顺序的影响
- 相同宽度的字段总是被分配到一起存放,在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。
-
对齐填充(Padding):
- 不是必须的,只用于填充,使得对象的总大小为8字节的整数倍。
- 原因:HotSpot虚拟机要求对象的起始地址必须是8字节的整数倍,而对象头已被精心设计为8字节的倍数(1倍或者 2倍),因此,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。
- 不是必须的,只用于填充,使得对象的总大小为8字节的整数倍。
3 对象的访问定位
Java程序会通过栈上的reference数据来操作堆上的具体对象,而具体的访问方式并没有在虚拟机规范中详细定义,主流的实现有句柄和直接指针两种。
-
使用句柄访问:
-
Java堆可能划分出一部分空间作为句柄池。
-
引用(reference)中存储的是对应句柄的地址。
-
句柄包含了对象实例数据和类型数据的指针。因此,访问对象需要两步:首先通过引用找到句柄,然后通过句柄找到实际的对象数据。
-
句柄访问的优势在于,当对象被移动(比如在GC过程中)时,只需要修改句柄中的对象实例指针,而引用本身不需要改变,这为对象移动提供了便利。
-
-
使用直接指针访问:
- 引用(reference)直接指向对象在堆中的地址。
- 访问对象时,不需要像使用句柄那样额外的定位句柄,从而提高了访问速度。
- 直接指针访问的优势在于访问速度更快,因为节省了一次指针定位的时间。由于在Java中对象访问非常频繁,这种方式可以显著节省执行成本
- HotSpot虚拟机主要采用这种方式进行对象访问,尽管在某些情况(如使用特定的垃圾收集器)可能会有变化(见第3章)。
-
对象或句柄中的类型数据指针与Class对象区别:
- 对象或句柄中的类型指针:这是为了JVM自身的运行时需求而存在的。这些指针直接指向方法区(或Java 8及更高版本中的元空间Metaspace)中的类元数据。
- 堆内存中的
Class
对象:这是供程序员在Java代码中使用的,主要用于反射操作。通过Class
对象,程序员可以获取类的信息(如类名、方法、字段等),创建类的实例,甚至在运行时修改类的结构。Class
对象本身也包含指向方法区中类元数据的引用,从而使得Java程序能够反射地访问和操作类的信息