JVM三.垃圾收集器

2018-08-08  本文已影响0人  stoneyang94

博主最近复习深入理解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)

2. 主动式中断(Voluntary Suspension)

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

安全区域(Safe Region)

产生背景

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

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

安全区域定义

工作原理

执行函数在进入安全区域时设置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

相关概念

并发和并行

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

吞吐量

收集器

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收集器使用两个参数控制吞吐量:

直观上,只要最大的垃圾收集停顿时间越小,吞吐量是越高的,但是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个步骤:

其执行过程如下:

CMS

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

CMS的优点

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

CMS的缺点

7. G1收集器

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

G1

G1收集器特点

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

G1工作过程

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

内存分配

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

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

1. 对象优先在Eden分配

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)

老年代GC(Major GC / Full GC)

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)概述

上一篇下一篇

猜你喜欢

热点阅读