概述
HotSpot 是在 JIT 之后的一款 java 虚拟机的开源实现,sun 从 JDK 1.3.1 开始使用。 它主要使用 C 实现的,相对于 JIT,性能有大幅提高。 HotSpot 将部分代码直接编译为本地可执行代码,从而显著提升了性能。
对象的创建
内存分配
java 中,最经常发生的就是对象的创建,那么,虚拟机在 java 对象创建过程中发生了什么呢? 虚拟机遇到一条 new 指令时,首先检查这个指令的参数是否能在常量池中定位到一个类的符号引用,并检查这个符号引用代表的类是否已经被加载、解析和初始化过,如果没有,则执行相应的类加载流程。 接下来,虚拟机为新生对象分配内存,对象所需内存的大小在类加载完成后可以完全确定,因此可以将一块确定大小的内存从 java 堆中划分出来。 在堆中的内存分配有两种方式: 1. 指针碰撞 — 对于在规整的空间中分配内存,只需要将指针向空闲空间挪动一段与对象大小相等的距离,通常在使用 Serial、ParNew 等带 Compact 过程的收集器时采用 2. 空闲列表 — 对于已使用空间和空闲空间交错的情况,指针碰撞就无法使用了,这个时候 jvm 必须维护一个空闲列表,保存每段空闲空间的首地址和长度,分配时 jvm 从列表中查找到足够大的一块空闲空间划分给对象,并更新列表,通常,使用 CMS 这种基于 Mark-Sweep 算法的收集器时采用
原子性
由于 java 是线程模型,所以需要考虑频繁的对象创建的线程安全问题。 有两种方案解决这个问题: 1. 对分配内存的空间进行同步处理,如原子性的比较交换操作配合失败重试 2. 每个线程在 java 堆中预先分配一小块独立的内存,称为本地线程分配缓冲 — TLAB,只有在分配新的 TLAB 时,才需要同步锁定
初始化
jvm 在为对象分配空间后需要将分配的空间初始化为零值,并且根据对象头中的对象信息如哈希码、GC 分代年龄等信息对对象进行必要的设置。 随后,一般来说执行 new 指令之后会接着执行 init 方法,将对象初始化成程序所希望得到的样子。 一个真正可用的对象就完全诞生了。
对象的内存布局
在 HotSpot 虚拟机中,对象在内存中分为三个部分:对象头、实例数据、对齐填充。
对象头 — Header
对象头包含两部分信息: 1. 用于存储对象运行时数据,如哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称之为“Mark Word” 2. 类型指针,即对象指向他的类元数据的指针,jvm 通过这个指针来确定这个对象是哪个类的实例
实例数据 — Instance Data
实例数据部分是对象存储的有效信息,也就是程序中定义的各种类型字段内容。
对齐填充 — Padding
这部分数据不是必须存在的,也没有特别含义,因为 HotSpot 是8字节对齐的,因此需要通过对其填充补全8字节。
对象的访问
在 java 栈中,维护了一个本地变量表,当需要访问一个变量时,jvm 就会在本地变量表中查找到变量的类型信息,如果是一个 reference 类型的变量,jvm 就需要去加载相应的对象。 下面的两图分别展示了通过句柄访问对象和通过指针访问对象的存储模式:
使用句柄最大的好处是 reference 中存储的是稳定的句柄地址,在对象移动、垃圾收集等工作中,只需要更新指针,而不需要改变 java 栈中的 reference 的指向(句柄地址不变) 而显然指针访问方式的优势就是速度更快,节省了一次指针定位的开销,对于频繁的对象操作,这类开销累积起来是十分可观的,因此 HotSpot 采用了指针直接访问对象的方式进行对象访问和操作。