JVM三.垃圾收集器

博主最近复习深入理解JVM一书,整理归纳,以形成系统认识和方便日后复习。
本文主要介绍

  1. 可达性分析法实现
  2. 垃圾收集器
  3. 内存分配与回收策略

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人想出来。

一. HotSpot的算法实现

本节主要介绍JVM如何发起内存回收。具体如何进行内存回收则和垃圾回收器有关。

HotSpot虚拟机上实现对象存活判断算法和垃圾收集算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。

可达性分析具体流程

可达性分析对执行时间敏感
可达性分析中会发生:Stop The World 和枚举根节点。

1. Stop The World

可达性分析对执行时间的敏感体现在GC停顿上,在整个分析期间,整个执行系统看起来就像冻结在某个时间点上,不能出现在分析过程中对象引用关系还在不断变化的情况。这导致GC进行时,必须停顿所有Java执行线程(Stop The World)。即使在号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也必须要停顿。
至于为什么在GC时要发生STW,有一个很适合的比喻:你妈妈在给你打扫房间的时候,肯定让你在老实呆着,否则她一边打扫,你一边扔纸屑,这房间还能打扫完?

2. 枚举根节点---借助OopMap

简言之就是列举出所有“GC Roots”。在可达性分析中,通过GC Roots 节点找引用链判断对象在链情况。
而可以作为GC Roots的节点主要是全局性引用(常量、类变量)与执行上下文(栈帧中的本地变量表)中,现在很多应用仅方法区就有数百兆。如果要逐个检查GC Roots节点,那必然会消耗很多时间。
目前主流Java虚拟机使用的都是准确式GC,所以在执行系统停顿下来后,并不需要一个不漏的检查所有执行上下文和全局引用的位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为OopMap(Ordinary Object Pointer,普通对象指针)的数据结构来达到这个目的的。在类加载完成时,HotSpot就把对象内具体偏移量上是什么类型的数据计算出来,在JIT(Just-In-Time Compiler)编译过程中,也会在特定的位置(Safepoint)记录下栈和寄存器中哪些位置是引用。这样,GC在扫描时就可以直接得知这些信息。

安全点(Safepoint)

OopMap缺点

  1. 在OopMap的协助下,HotSpot可以快速且准确地完成GC Roots枚举,但是可能会导致引用关系变化
  2. OopMap内容变化的指令非常多,如果为每一条指令都生成对应的OopMap,将会需要大量的额外空间,这样GC的空间成本将会变得很高。

用安全点弥补OopMap缺点

HotSpot没有为每条指令都生成OopMap,只是在特定的位置记录了这些信息,这些位置称为安全点(Sapfepoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点时才能暂停

  • 安全点的选定
  1. 宏观准则
    安全点的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过分增大运行时的负荷。
  2. 具体标准
    安全点的选定基本上是以程序是否具有让程序长时间执行的特性为标准选定的,长时间执行的最明显特性就是指令序列复用,如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生Safepoint。

线程怎么到安全点

如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都跑到最近的安全点上再停顿下来,有两张方案可供选择:

1. 抢先式中断(Preemptive Suspension)

  • 不需要线程的执行代码主动去配合,在GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点
  • 现在几乎没有虚拟机实现采用抢先式中断来暂停线程从而响应GC事件

2. 主动式中断(Voluntary Suspension)

当GC需要中断线程的时候,不直接对线程进行操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起(VM将内存页设置为不可读,线程会产生自陷异常,在预先注册异常处理器中暂停线程实现等待),轮询标志的地方和安全点是重合的。

安全区域(Safe Region)

产生背景

Safepoint机制保证了程序执行时,在不太长时间内就会遇到可进入GC的Safepoint;但是当程序不执行(没有CPU分配时间)的时候(如线程出于Sleep状态或者Block状态),这时线程无法响应JVM的中断请求,走到安全的地方中断挂起,JVM也不可能等待线程重新分配CPU时间。

对于这种情况,就需要安全区域(Safe Region)来解决。

安全区域定义

  • 安全区域是指在一段代码片中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的
  • 可以将Safe Region看作被扩展了的Safepoint

工作原理

执行函数在进入安全区域时设置ready flag。在它离开安全区域以前,它先检查GC是否完成了枚举(或者收集),并且不再需要执行函数呆在阻塞状态。如果是真,它就向前执行,离开安全区域; 否则,它就像安全点一样阻塞他自己。

二. 垃圾收集器---how

垃圾收集算法是方法论,而垃圾收集器是具体的实现。

垃圾收集器
收集器 串行、并行 or 并发 新生代 / 老年代 算法 目标 适用场景
Serial 串行 新生代 复制算法 响应速度优先 单 CPU 环境下的 Client 模式
Serial Old 串行 老年代 标记-整理 响应速度优先 单 CPU 环境下的 Client 模式、CMS 的后备预案
ParNew 并行 新生代 复制算法 响应速度优先 多 CPU 环境时在 Server 模式下与 CMS 配合
Parallel Scavenge 并行 新生代 复制算法 吞吐量优先 在后台运算而不需要太多交互的任务
Parallel Old 并行 老年代 标记-整理 吞吐量优先 在后台运算而不需要太多交互的任务
CMS 并发 老年代 标记-清除 响应速度优先 集中在互联网站或 B/S 系统服务端上的 Java 应用
G1 并发 both 标记-整理 + 复制算法 响应速度优先 面向服务端应用,将来替换 CMS

相关概念

并发和并行

这两个名词都是并发编程中的概念,在谈论垃圾收集器的上下文语境中,它们可以解释如下。

  • 并行(Parallel):指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态
  • 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另一个CPU上

吞吐量

  • 吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即
    吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)
  • eg:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%

收集器

1. Serial收集器---Client模式下首选

Serial收集器是最基本、发展历史最悠久的收集器。它是一种单线程垃圾收集器,这就意味着在其进行垃圾收集的时候需要暂停其他的线程,也就是之前提到的”Stop the world“。虽然这个过程是在用户不可见的情况下把用户正常的线程全部停掉,听起来有点狠,这点是很难让人接受的。Serial、Serial Old收集器的工作示意图如下:

Serial

尽管有以上不能让人接受的地方,但是Serial收集器还是有其优点的:简单而高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得较高的收集效率。

到目前为止,Serial收集器依然是Client模式下的默认的新生代垃圾收集器。

2. ParNew收集器---Server模式下首选

ParNew收集器是Serial收集器的多线程版本,ParNew收集器的工作示意图如下:

ParNew

ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。除去性能因素,很重要的原因是除了Serial收集器外,目前只有它能与CMS收集器配合工作

但是,在单CPU环境中,ParNew收集器绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。然而,随着可以使用的CPU的数量的增加,它对于GC时系统资源的有效利用还是很有好处的。

3. Parallel Scavenge收集器---吞吐量优先,可自适应

新生代,使用复制算法,并行的多线程。

与ParNew收集器相比,很多相似之处,但是Parallel Scavenge收集器更关注可控制的吞吐量。吞吐量越大,垃圾收集的时间越短,则用户代码则可以充分利用CPU资源,尽快完成程序的运算任务。

Parallel Scavenge收集器使用两个参数控制吞吐量:

  • XX:MaxGCPauseMillis 控制最大的垃圾收集停顿时间
  • XX:GCRatio 直接设置吞吐量的大小。

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是GC停顿时间的缩短是以牺牲吞吐量和新生代空间作为代价的。比如原来10秒收集一次,每次停顿100毫秒,现在变成5秒收集一次,每次停顿70毫秒。停顿时间下降的同时,吞吐量也下降了。

除此之外,Parallel Scavenge收集器还可以设置参数-XX:+UseAdaptiveSizePocily来动态调整停顿时间或者最大的吞吐量,这种方式称为GC自适应调节策略,这点是ParNew收集器所没有的。

4. Serial Old收集器

Serial Old收集器是Serial收集器的老年代版本,也是一个单线程收集器,采用“标记-整理算法”进行回收。其运行过程与Serial收集器一样。

Serial Old收集器的主要意义也是在于给Client模式下的虚拟机使用。

如果在Server模式下,那么它主要还有两大用途:

  1. 用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
  2. 用途就是作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure时使用。

5. Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法进行垃圾回收。其通常与Parallel Scavenge收集器配合使用,“吞吐量优先”收集器是这个组合的特点,在注重吞吐量和CPU资源敏感的场合,都可以使用这个组合。

Parallel Old

6. CMS(Concurrent Mark Sweep)收集器---停顿时间

CMS收集器(Concurrent Mark Sweep)的目标就是获取最短回收停顿时间。在注重服务器的响应速度,希望停顿时间最短,则CMS收集器是比较好的选择。

整个执行过程分为以下4个步骤:

  • 初始标记
    标记GC Roots能够关联到的对象,速度很快,需要停顿。
  • 并发标记
    执行GC Roots Tracing的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记
    修正并发标记期间因用户程序执行而导致标记发生变动使得标记错误的记录,需要停顿。
  • 并发清除
    不需要停顿。

其执行过程如下:

CMS

由上图可知,整个过程中耗时最长的并发标记并发清除过程收集器线程都可以与用户线程一起工作,因此,总体上CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS的优点

CMS的优点很明显:并发收集、低停顿。由于进行垃圾收集的时间主要耗在并发标记与并发清除这两个过程,虽然初始标记和重新标记仍然需要暂停用户线程,但是从总体上看,这部分占用的时间相比其他两个步骤很小,所以可以认为是低停顿的。

CMS的缺点

  • 1)对CPU资源太敏感
    CMS 默认启动的回收线程数是(CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。
  • 2)无法处理浮动垃圾
    由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。
    也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。
    可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。
  • 3)空间碎片过多
    清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

7. G1收集器

G1(Garbage-First)收集器是现今收集器技术的最新成果之一,之前一直处于实验阶段,直到jdk7u4之后,才正式作为商用的收集器。
整个执行过程如下:

G1

G1收集器特点

  • 并行与并发
  • 分代收集(仍然保留了分代的概念)
  • 空间整合(整体上属于“标记-整理”算法,不会导致空间碎片)
  • 可预测的停顿(比CMS更先进的地方在于能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒)

此外,G1收集器将Java堆划分为多个大小相等的Region(独立区域),新生代与老年代都是一部分Region的集合,G1的收集范围则是这一个个Region(化整为零)。

G1工作过程

  • 1.初始标记(Initial Marking)
    仅仅只是标记一下GC Roots能够直接关联的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段的用户程序并发运行的时候,能在正确可用的Region中创建对象,这个阶段需要暂停线程。
  • 2.并发标记(Concurrent Marking)
    从GC Roots进行可达性分析,找出存活的对象,这个阶段食欲用户线程并发执行的
  • 3.最终标记(Final Marking)
    修正在并发标记阶段因为用户程序的并发执行而导致标记产生变动的那一部分记录,这部分记录被保存在Remembered Set Logs中,最终标记阶段再把Logs中的记录合并到Remembered Set中,这个阶段是并行执行的,仍然需要暂停用户线程
  • 4.筛选回收(Live Data Counting and Evacuation)
    首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间制定回收计划

三. 内存分配策略---when

内存分配

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

对象的内存分配,往大方向讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。

1. 对象优先在Eden分配

  • 大多数情况下,对象在新生代Eden区分配。
  • 当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC

2. 大对象直接进入老年代

所谓的大对象就是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串和数组,经常产生大对象容易导致额外的GC操作。

JVM中提供了一个-XX:PretenureSizeThreshold参数(这个参数只对Serial和ParNew这两个新生代垃圾收集器有效),令大于这个参数的对象直接在老年代中分配,这样做的目的是避免在Eden区和两个Survivor区之间发生大量的内存拷贝。

3. 长期存活的对象将进入老年代

JVM给了每个对象一个“年龄计数器”,所谓的年龄计数器就是指,这个对象熬过第一次GC,并且进入了Survivor区中,那么就将这个对象的年龄设为1,之后,每熬过一次GC,年龄+1,当这个值到达一个阀值(默认15)时,这个对象就会被移到老年代中。

可通过-XX:MaxTenuringThreshold来设置阀值

4. 动态对象年龄判定

为了更好的适应不同程序的内存状况,JVM也不是要去一个对象必须达到MaxTenuringThreshold设置的年龄阀值才能进入老年代。

如果Survivor中的对象满足同年龄(比如N)对象所占空间达到了Survivor总空间的一半的时候,那么年龄大于或者等于N的对象都可以进入老年代,无需等待阀值

5. 空间分配担保

  1. 在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间
    1.1 如果这个条件成立,那么Minor GC可以确保是安全的
    1.2 如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败
    1.2.1 如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
    • 如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的
    • 如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

四. 内存回收策略---when

新生代GC(Minor GC)

  • 发生在新生代的垃圾收集动作
  • 因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快

老年代GC(Major GC / Full GC)

  • 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)
  • Major GC的速度一般会比Minor GC慢10倍以上

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。

可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。

2. 老年代空间不足

老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。

4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

参考文章
周志明,深入理解Java虚拟机:JVM高级特性与最佳实践,机械工业出版社
JVM学习笔记(三)垃圾收集器与内存分配策略
【深入理解JVM】:垃圾收集(GC)概述

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容