JVM HotSpot
本文最后更新于:2024年3月18日 凌晨
JVM HotSpot
HotSpot 虚拟机对象
对象的内存布局
-
在 HotSpot 虚拟机中,对象的内存布局分为以下 3 块区域:
-
对象头(Header)
对象头记录了对象在运行过程中所需要使用的一些数据:
- Hash值。
- GC 分代年龄。
- 锁状态标志。
- 线程持有的锁。
- 偏向线程 ID
- 偏向时间戳。
对象头可能包含类型指针,通过该指针能确定对象属于哪个类,如果对象是一个数组,那么对象头还会包括数组长度。
-
实例数据(Instance Data)
实例数据部分就是成员变量的值,其中包括父类成员变量和本类成员变量。
-
对齐填充(Padding)
- 用于确保对象的总长度为 8 字节的整数倍。
- HotSpot VM 的自动内存管理系统要求对象的大小必须是 8 字节的整数倍,而对象头部分正好是 8 字节的倍数(1 倍或 2 倍),因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。
- 对齐填充并不是必然存在,也没有特别的含义,它仅仅起着占位符的作用。
-
对象的创建过程
-
类加载检查:虚拟机在解析
.class
文件时,若遇到一条 new 指令,首先它会去检查常量池中是否有这个类的符号引用,并且检查这个符号引用所代表的类是否已被加载,解析和初始化过,如果没有,那么必须先执行相应的类加载过程。 -
为新生对象分配内存:对象所需内存的大小在类加载完成后便可完全确定,接下来从堆中划分一块对应大小的内存空间给新的对象,分配堆中内存有两种方式:
- 指针碰撞
如果 Java 堆中内存绝对规整(说明采用的是标记-复制算法或标记整理法),空闲内存和已使用内存中间放着一个指针作为分界点指示器,那么分配内存时只需要把指针向空闲内存挪动一段与对象大小一样的距离,这种分配方式称为"指针碰撞” - 空闲列表
如果 Java 堆中内存并不规整,已使用的内存和空闲内存交错(说明采用的是标记-清除法,有碎片),此时没法简单进行指针碰撞,VM 必须维护一个列表,记录其中哪些内存块空闲可用,分配之时从空闲列表中找到一块足够大的内存空间划分给对象实例,这种方式称为"空闲列表”
- 指针碰撞
-
初始化:分配完内存后,为对象中的成员变量赋上初始值,设置对象头信息,调用对象的构造函数方法进行初始化。
对象的访问方式
所有对象的存储空间都是在堆中分配的,但是这个对象的引用却是在堆栈中分配的,也就是说在建立一个对象时两个地方都分配内存,在堆中分配的内存实际建立这个对象,而在堆栈中分配的内存只是一个指向这个堆对象的指针(引用)而已,那么根据引用存放的地址类型的不同,对象有不同的访问方式。
句柄访问方式
- 堆中需要有一块叫做"句柄池”的内存空间,句柄中包含了对象实例数据与类型数据各自的具体地址信息。
- 引用类型的变量存放的是该对象的句柄地址(reference),访问对象时,首先需要通过引用类型的变量找到该对象的句柄,然后根据句柄中对象的地址找到对象。
直接指针访问方式
- 引用类型的变量直接存放对象的地址,从而不需要句柄池,通过引用能够直接访问对象,但对象所在的内存空间需要额外的策略存储对象所属的类信息的地址。
- 需要说明的是,HotSpot 采用第二种方式,即直接指针方式来访问对象,只需要一次寻址操作,所以在性能上比句柄访问方式快一倍,但像上面所说,它需要额外的策略来存储对象在方法区中类信息的地址。
HotSpot 垃圾收集器
HotSpot 虚拟机提供了多种垃圾收集器,每种收集器都有各自的特点,虽然我们要对各个收集器进行比较,但并非为了挑选出一个最好的收集器,我们选择的只是对具体应用最合适的收集器。
年轻代垃圾收集器
Serial 垃圾收集器(单线程)
- 只开启一条 GC 线程进行垃圾回收,并且在垃圾收集过程中停止一切用户线程(Stop The World)
- 一般客户端应用所需内存较小,不会创建太多对象,而且堆内存不大,因此垃圾收集器回收时间短,即使在这段时间停止一切用户线程,也不会感觉明显卡顿,因此 Serial 垃圾收集器适合客户端使用。
- 由于 Serial 收集器只使用一条 GC 线程,避免了线程切换的开销,从而简单高效。
ParNew 垃圾收集器(多线程)
- ParNew 是 Serial 的多线程版本,由多条 GC 线程并行地进行垃圾清理,但清理过程依然需要 Stop The World
- ParNew 追求"低停顿时间”,与 Serial 唯一区别就是使用了多线程进行垃圾收集,在多 CPU 环境下性能比 Serial 会有一定程度的提升,但线程切换需要额外的开销,因此在单 CPU 环境中表现不如 Serial
Parallel Scavenge 垃圾收集器(多线程)
- Parallel Scavenge 和 ParNew 一样,都是多线程,年轻代垃圾收集器,但是两者有巨大的不同点:
- Parallel Scavenge:追求 CPU 吞吐量,能够在较短时间内完成指定任务,因此适合没有交互的后台计算。
- ParNew:追求降低用户停顿时间,适合交互式应用。
吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
- 追求高吞吐量,可以通过减少 GC 执行实际工作的时间,然而,仅仅偶尔运行 GC 意味着每当 GC 运行时将有许多工作要做,因为在此期间积累在堆中的对象数量很高,单个 GC 需要花更多的时间来完成,从而导致更高的暂停时间,而考虑到低暂停时间,最好频繁运行 GC 以便更快速完成,反过来又导致吞吐量下降。
- 通过参数
-XX:GCTimeRadio
设置垃圾回收时间占总 CPU 时间的百分比。 - 通过参数
-XX:MaxGCPauseMillis
设置垃圾处理过程最久停顿时间。 - 通过命令
-XX:+UseAdaptiveSizePolicy
开启自适应策略,我们只要设置好堆的大小和 MaxGCPauseMillis 或 GCTimeRadio,收集器会自动调整年轻代的大小,Eden 和 Survivor 的比例,对象进入老年代的年龄,以最大程度上接近我们设置的 MaxGCPauseMillis 或 GCTimeRadio
老年代垃圾收集器
Serial Old 垃圾收集器(单线程)
- Serial Old 收集器是 Serial 的老年代版本,都是单线程收集器,只启用一条 GC 线程,都适合客户端应用,它们唯一的区别就是:Serial Old 工作在老年代,使用"标记-整理”算法,Serial 工作在年轻代,使用"标记-复制”算法。
Parallel Old 垃圾收集器(多线程)
- Parallel Old 收集器是 Parallel Scavenge 的老年代版本,追求 CPU 吞吐量。
CMS 垃圾收集器
- CMS(Concurrent Mark Sweep,并发标记清除)收集器是以获取最短回收停顿时间为目标的收集器(追求低停顿),它在垃圾收集时使得用户线程和 GC 线程并发执行,因此在垃圾收集过程中用户也不会感到明显的卡顿。
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用多条标记线程,与用户线程并发执行,此过程进行可达性分析,标记出所有废弃对象,速度很慢。
- 重新标记:Stop The World,使用多条标记线程并发执行,将刚才并发标记过程中新出现的废弃对象标记出来。
- 并发清除:只使用一条 GC 线程,与用户线程并发执行,清除刚才标记的对象,这个过程非常耗时。
- 并发标记与并发清除过程耗时最长,且可以与用户线程一起工作,因此,总体上说,CMS 收集器的内存回收过程是与用户线程一起并发执行的。
- 缺点
- 吞吐量低。
- 无法处理浮动垃圾,导致频繁 Full GC
- 使用"标记-清除”算法产生碎片空间。
- 对于产生碎片空间的问题,可以通过开启
-XX:+UseCMSCompactAtFullCollection
,在每次 Full GC 完成后都会进行一次内存压缩整理,将零散在各处的对象整理到一块,设置参数-XX:CMSFullGCsBeforeCompaction
告诉 CMS,经过了 N 次 Full GC 之后再进行一次内存整理。
G1 通用垃圾收集器
-
G1 是一款面向服务端应用的垃圾收集器,它没有年轻代和老年代的概念,而是将堆划分为一块块独立的 Region,当要进行垃圾收集时,首先估计每个 Region 中垃圾的数量,每次都从垃圾回收价值最大的 Region 开始回收,因此可以获得最大的回收效率。
-
从整体上看,G1 是基于"标记-整理”算法实现的收集器,从局部(两个 Region 之间)上看是基于"标记-复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
-
可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。
-
如果不计算维护 Remembered Set 的操作,G1 收集器的工作过程分为以下几个步骤:
- 初始标记:Stop The World,仅使用一条初始标记线程对所有与 GC Roots 直接关联的对象进行标记。
- 并发标记:使用一条标记线程与用户线程并发执行,此过程进行可达性分析,速度很慢。
- 最终标记:Stop The World,使用多条标记线程并发执行。
- 筛选回收:回收废弃对象,此时也要 Stop The World,并使用多条筛选回收线程并发执行。
注意
一个对象和它内部所引用的对象可能不在同一个 Region 中,那么当垃圾回收时,是否需要扫描整个堆内存才能完整地进行一次可达性分析?并不!每个 Region 都有一个 Remembered Set,用于记录本区域中所有对象引用的对象所在的区域,进行可达性分析时,只要在 GC Roots 中再加上 Remembered Set 即可防止对整个堆内存进行遍历。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!