JVM垃圾回收详解

前言

最近线上出现了JVM 频繁FGC的问题,查询了不少GC相关的资料,作了一些整理翻译。文章比较长能够收藏后慢慢阅读。html

1、什么是垃圾回收?(Garbage Collection)

一个垃圾回收器有一下三个职责java

  • 分配内存
  • 确保有引用的对象可以在内存中保留。
  • 可以在正在执行的代码环境中回收已经死亡对象的内存。

这里提到的有引用是指存活的对象,后面会提到一些算法用来判断对象是否存活。不在有引用的对象将被认为是死亡的,也就是常说的垃圾garbage。找到并释放这些垃圾对象占用的空间的过程就被称做是垃圾回收garbage collection算法

垃圾回收能够解决不少内存分配的问题,但并不意味这所有。 好比:你能够不断地建立对象并保持对它们的引用直到没有可用的内存分配。垃圾回收自己就是一项很是复杂和消耗资源的过程。安全

2、理想的垃圾收集器须要哪些特性?

  1. 垃圾收集器必须是安全和全面的。这就意味着,存活的对象绝对不能被释放,相反垃圾对象在不多的垃圾回收循环里必须被回收。
  2. 垃圾回收必须是高效的,不容许出现正在运行的程序长时间暂停。
  3. 内存碎片整理,垃圾被收集之后内存会存在不少不连续的内存碎片,可能致使大对象没法分配到足够连续的内存。
  4. 扩展性,在多处理器系统、多线程应用中,内存分配和垃圾收集不能成为性能瓶颈。

3、设计选择

在设计一款垃圾收集器时,有一些选择可供选择:bash

  • 串行 vs 并行

串行收集,即便在多cpu环境中也是单线程处理垃圾收集工做。当使用并行收集时,垃圾收集任务就会被分为几子任务由不一样的线程的执行,不只仅是在多CPU环境中使用,在单核的系统中也可使用,只是收集效果可能比使用串行效率还低。因此再单核的环境下尽可能使用串行收集。服务器

  • 并发 vs 暂停(stop-the-word)

并发是指垃圾收集线程和应用线程同时执行,并发和stop-the-word并非互斥的,在一个执行一次垃圾收集的过程当中两种状况均可能存在。例如CMSG1垃圾搜集器。并发式GC会并发执行其垃圾收集任务,可是,可能也会有一些步骤须要以stop-the-world方法执行,致使应用程序暂停。与并发式GC相比,Stop-the-world式的GC更简单.多线程

  • 整理 vs 不整理 vs 复制

这个描述的主要是垃圾被收集之后,对内存碎片的处理方式。并发

整理、不整理,垃圾回收之后是否将存活的对象统一移动到一个地方。整理后的内存空间方便后续的对象分配内存,可是更消耗资源和时间,而不整理效率更高存在内存碎片的风险。oracle

复制,首先将内存分割成两块同样大小的区域,垃圾收集后会将存活的对象拷贝到另外一块不一样的内存区域。这样作的好处是,拷贝后,源内存区域能够做为一块空的、当即可用的区域对待,方便后续的内存分配,可是这种方法的缺点是须要用额外的时间、空间来拷贝对象。jvm

4、对象是否存活?

Jvm要对回收一个对象必须知道这个对象是否存活,便是否有有效的引用?介绍几种判断对象是否死亡的算法。

  1. 引用计数法 给对象添加一个引用计数器,每次引用到它时引用计数器加一,当引用失效时引用计时器减一。当引用计数器为0时即表示当前对象能够被回收。 这个算法实现简单、断定效率也很高,可是没法处理循环引用的问题,即 A 对象引用了 B, B 对象也引用了 A,那么A、B都有引用,他们的应用计数都为一,但实际他们是能够被回收的。

  2. 可达性分析算法 算法规定了一些称为GC Root的根对象,当对象没有引用链到达这些GC Root时就被断定为可回收的对象。

    image

5、分代收集算法

当使用称为分代收集的技术时,内存将被分为不一样的几代,即,会将对象按其年龄分别存储在不一样的对象池中。例如,目前最普遍使用的是分代是将对象分为年轻代对象和老年代对象。

在分代内存管理中,使用不一样算法对不一样代的对象执行垃圾收集的工做,每种算法都是基于对某代对象的特性进行优化的。考虑到应用程序能够是用包括Java在内的不一样的程序语言编写,分代垃圾收集使用了称为 弱代理论(weak generational hypothesis)的方法,具体描述以下:

大多数分配了内存的对象并不会存活太长时间,在处于年轻代时就会死掉; 不多有对象会从老年代变成年轻代。 年轻代对象的垃圾收集相对频繁一些,同时会也更有效率,更快一些,由于年轻代对象所占用的内存一般较小,也比较容易肯定哪些对象是已经没法再被引用的。

当某些对象通过几回年轻代垃圾收集后依然存活,则这些对象会被 提高(promoted)到老年代。典型状况下,老年代所占用的内存会比年轻代大,并且还会随时渐渐慢慢增大。这样的结果是,对老年代的垃圾收集就不能频繁进行,并且执行时间也会长不少。

image

选择年轻代的垃圾收集算法时会更看重执行速度,由于年轻代的垃圾收集工做会频繁执行。另外一方面,管理老年代的算法则更注重空间效率,由于老年代会占用堆中的大部分空间,这要求算法必需要处理好垃圾收集的工做,尽可能下降堆中的垃圾内存的密度。

6、HotSpot 分代收集

主要介绍几种常见的垃圾收集器串行收集器(Serial Collector)并行垃圾收集器(Parallel Collector)并行整理收集器(Parallel Compacting Collector)并发标记清理垃圾收集器(Concurrent Mark-Sweep,CMS)Garbage-First (G1) 图中有连线的表示能够组合使用。

6.1 HotSpot中的代的划分

在Java HotSpot虚拟机中,内存被分为3代:年轻代、老年代和永生代(java8已经取消永久代)。大多数对象最初都是分配在年轻代内存中的,年轻代中对象通过几回垃圾收集后还存活的,会被转到老年代。一些体积比较大的对象在建立的时候可能就会在老年代中。 在年轻代中包含三个分区,一个 Eden区和两个 Survivor区(FROM、TO),如图所示。大部分对象最初是分配在Eden区中的(可是,如前面所述,一些较大的对象可能会直接分配在老年代中)。Survivor始终保持一个区域为空,当通过必定次数(-XX:MaxTenuringThreshold=n来指定默认值为15)的年轻代GC后依然存活的对象能够被晋升到老年代。

6.2 垃圾收集分类

当年轻代被填满时,开始执行年轻代的垃圾收集(minor collection)。当老年代被填满时,也会执行老年代垃圾收集(full GCmajor collection),通常来讲,年轻代GC会先执行,执行屡次young GC 会触发FGC,固然这不是绝对的,由于大对象会直接分配到老年代,当老年代的分配的内存不足时就可能触发频繁的FGC。目前除了CMS收集器外,在执行FGC的时候都会对整个堆进行垃圾收集。

6.3 串行收集器(Serial Collector)

使用串行收集器,年轻代和老年代的垃圾收集工做会串行完成(在单一CPU系统上),这时是stop-the-world模式的。即,当执行垃圾收集工做时,应用程序必须中止运行。

6.3.1 使用串行收集器的年轻代垃圾收集

图3展现了使用串行收集器的年轻代垃圾收集的执行过程。EdenSurvivor FROM区存活的对象会被拷贝到初始为空的另外一个Survivor区(图中标识为To的区)中,这其中,那些体积过大以致于Survivor区装不下的对象会被直接拷贝到老年代中。相对于已经被拷贝到To区的对象,源Survivor区(图中标识为From的区)中的存活对象仍然比较年轻,而被拷贝到老年代中对象则相对年纪大一些。

在年轻代垃圾收集完成后,Eden区和From区会被清空,只有To区会继续持有存活的对象。此时,From区和To区在逻辑上交换,To区变成From区,原From区变成To区,如图4所示。

6.3.2 使用串行收集器的老年代垃圾收集

对于串行收集器,老年代和永生代会在进行垃圾收集时使用标记-清理-整理(Mark-Sweep-Compact)算法。在标记阶段,收集器会标识哪些对象是live状态的。清理阶段会跨代清理,标识垃圾对象。而后,收集器执行整理(sliding compaction),将存活对象移动到老年代内存空间的起始部分(永生代中状况于此相似),这样在老年代内存空间的尾部会产生一个大的连续空间。如图5所示。这种整理可使用碰撞指针完成。

6.3.3 何时使用串行垃圾收集器

大多数运行在客户机上的应用程序会选择使用并行垃圾收集器,由于这些应用程序对低暂停时间并无较高的要求。对于当今的硬件来讲,串行垃圾收集器已经能够有效的管理许多具备64M堆的重要应用程序,而且执行一次完整垃圾收集也不会超过半秒钟。

6.3.4 选择串行垃圾收集器

在J2SE 5.0的发行版中,在非服务器类使用的机器上,默认选择的是串行垃圾收集器。在其余类型使用的机器上,能够经过添加参数 -XX:+UseSerialGC来显式的使用串行垃圾收集器。

6.4 并行垃圾收集器(Parallel Collector)

当前,不少的Java应用程序都跑在具备较大物理内存和多CPU的机器上。并行垃圾收集器,也称为吞吐量垃圾收集器,被用于垃圾收集工做。该收集器能够充分的利用多CPU的特色,避免一个CPU执行垃圾收集,其余CPU空闲的状态发生。

6.4.1 使用并行垃圾收集器的年轻代垃圾收集

这里,对年轻代的并行垃圾收集使用的串行垃圾收集算法的并行版本。它仍然会stop-the-world,拷贝对象,但执行垃圾收集时是使用多CPU并行进行的,减小了垃圾收集的时间损耗,提升了应用程序的吞吐量。图6展现了串行垃圾收集器和并行垃圾收集器对年轻代进行垃圾收集时的区别。

6.4.2 使用并行垃圾收集器的老年代垃圾收集

老年代中的并行垃圾收集使用了与串行垃圾收集器相同的串行 标记-清理-整理(mark-sweep-compact)算法。

6.4.3 何时使用并行垃圾收集器

当应用程序运行在具备多个CPU上,对暂停时间没有特别高的要求时,使用并行垃圾收集器会有较好的效果,由于虽不频繁,但可能时间会很长的老年代垃圾收集仍然会发生。例如,那些执行批量处理、订单处理、工资支付、科学计算的应用程序更适合使用并行垃圾收集。

可能你会想用并行整理垃圾收集器(会在下一节介绍)来替代并行收集器,由于前者对全部代执行垃圾收集,然后者指对年轻代执行垃圾收集。

6.4.4 选择并行垃圾收集器

在J2SE 5.0的发行版中,若应用程序是运行在服务器类的机器上,则会默认使用并行垃圾收集器。在其余机器上,能够经过 -XX:+UseParallelGC参数来显式启用并行垃圾收集器。

6.6 并行整理整理收集器(Parallel Compacting Collector)

并行整理垃圾收集器是在J2SE 5.0 update 6中被引入的,其与并行垃圾收集器的区别在于,并行整理垃圾收集器使用了新的算法对老年代进行垃圾收集。注意,最终,并行整理垃圾收集器会取代并行垃圾收集器。

4.6.1 使用并行整理垃圾收集器的年轻代垃圾收集

年轻代中,并行整理垃圾收集器使用了与并行垃圾收集器相同的垃圾收集算法。

4.6.2 使用并行整理垃圾收集器的老年代垃圾收集

当使用并行整理垃圾收集时,老年代和永生代会使用stop-the-world的方式执行垃圾收集,大多数的并行模式都会使用移动整理(sliding compaction)。垃圾收集分为三个阶段。首先,将每个代从逻辑上分为固定大小的区域。

在 标记阶段(mark phase),应用程序代码能够直接到达的live对象的初始集合会被划分到各个垃圾收集线程中,而后,全部的live对象会被并行标记。若一个对象被标记为live,则会更新该对象所在的区域中与该对象的大小和位置相关的数据。

在 总结阶段(summary phase)会对区域,而非单独的对象进行操做。因为以前的垃圾收集执行了整理,每一代的左侧部分的对象密度会较高,包含了大部分live对象。这些对象密度较高的区域被恢复为可用后,就不值得再花时间去整理了。因此,在总结阶段要作的第一件事是从最左端对象开始检查每一个区域的live对象密度,直到找到了一个恢复其本区域和恢复其右侧的空间的开销都比较小时中止。找到的区域的左侧全部区域被称为dense prefix,不会再有对象被移动到这些区域里了。这个区域后侧的区域会被整理,清除全部已死的空间(清理垃圾对象占用的空间)。总结阶段会计算并保存每一个整理后的区域中对象的新地址。注意,在当前实现中,总结阶段是串行的;固然总结阶段也能够实现为并行的,但相对于性能总结阶段的并行不及标记整理阶段来得重要。

在 整理阶段(compaction phase),垃圾收集线程使用总结阶段收集到的数据决定哪些区域课余填充数据,而后各个线程独立的将数据拷贝到这些区域中。这样就产生了一个底端对象密度大,连一端是一个很大的空区域块的堆。

4.6.3 何时使用并行整理垃圾收集器

相对于并行垃圾收集器,使用并行整理垃圾收集器对那些运行在多CPU的应用程序更有好处。此外,老年代垃圾收集的并行操做能够减小应用程序的暂停时间,对于那些对暂停时间有较高要求的应用程序来讲,并行整理垃圾程序比并行垃圾收集更加适用。并行整理垃圾收集程序可能并不适用于那些与其余不少应用程序并存于一台机器的应用程序上,这种状况下,没有一个应用程序能够独占全部的CPU。在这样的机器上,须要考虑减小执行垃圾收集的线程数(使用-XX:ParallelGCThreads=n命令行选项),或者使用另外一种垃圾收集器。

4.6.4 选择并行整理垃圾收集选项

若你想使用并行整理垃圾收集器,你必须显式指定-XX:+UseParallelOldGC命令行选项。

4.7 并发标记清理(Concurrent Mark-Sweep,CMS)垃圾收集器

对于不少应用程序来讲,点到点的吞吐量并不如快速响应来的重要。典型状况下,年轻代的垃圾收集并不会引发较长时间的暂停。可是,老年代的垃圾收集,虽不频繁,却可能引发长时间的暂停,特别是使用了较大的堆的时候。为了应付这种状况,HotSpot JVM使用了CMS垃圾收集器,也称为低延迟(low-latency)垃圾收集器。

4.7.1 使用CMS垃圾收集器的年轻代垃圾收集

CMS垃圾收集器只对老年代进行收集,年轻代实际默认使用ParNewGC(一种年轻代的并行垃圾收集器)收集。

4.7.2 使用CMS垃圾收集器的老年代垃圾收集

大部分老年代的垃圾收集使用了CMS垃圾收集器,垃圾收集工做是与应用程序的执行并发进行的。

过程 描述
初始标记 标记老年代的存活对象,也可能包括年轻代的存活对象。暂停应用线程stop-the world
并发标记 和应用程序一块儿执行,标记应用程序运行过程当中产生的存活的对象。
重标记 标记因为应用程序更新致使遗漏的对象,暂停应用线程stop-the world
并发清理 清理没有被标记的对象,不会进行内存整理,可能致使内存碎片问题。
复位 清理数据等待下一次收集执行。

图7展现了使用串行化的标记清理垃圾收集器和使用CMS垃圾收集器对老年代进行垃圾收集的区别。

不进行内存空间整理节省了时间,可是可用空间再也不是连续的了,垃圾收集也不能简单的使用指针指向下一次可用来为对象分配内存的地址了。相反,这种状况下,须要使用可用空间列表。即,会建立一个指向未分配区域的列表,每次为对象分配内存时,会从列表中找到一个合适大小的内存区域来为新对象分配内存。这样作的结果是,老年代上的内存的分配比简单实用碰撞指针分配内存消耗大。这也会增长年轻代垃圾收集的额外负担,由于老年代中的大部分对象是在新生代垃圾收集的时候重新生代提高为老年代的。

使用CMS垃圾收集器的另外一个缺点是它所须要的对空间比其余垃圾收集器大。在标记阶段,应用程序能够继续运行,能够继续分配内存,潜在的可能会持续的增大老年代的内存使用。此外,尽管垃圾收集器保证会在标记阶段标记出全部的live对象,可是在此阶段中,某些对象可能会变成垃圾对象,这些对象不会被回收,直到下一次垃圾收集执行。这些对象成为 浮动垃圾对象(floating garbage)。

最后,因为没有使用整理,会形成内存碎片的产生。为了解决这个问题,CMS垃圾收集器会跟踪经常使用对象的大小,预估可能的内存须要,可能会差分或合并内存块来知足须要。

与其余的垃圾收集器不一样,当老年代被填满后,CMS垃圾收集器并不会对老年代进行垃圾收集。相反,它会在老年代被填满以前就执行垃圾收集工做。不然这就与串行或并行垃圾收集器同样会形成应用程序长时间地暂停。为了不这种状况,CMS垃圾收集器会基于统计数字来来定执行垃圾收集工做的时间,这个统计数字涵盖了前几回垃圾收集的执行时间和老年代中新增内存分配的速率。当老年代中内存占用率超过了称为初始占用率的阀值后,会启动CMS垃圾收集器进行垃圾收集。初始占用率能够经过命令行选项-XX:CMSInitiatingOccupancyFraction=n进行设置,其中n是老年代占用率的百分比的值,默认为68。

整体来看,与平行垃圾收集器相比,CMS减小了执行老年代垃圾收集时应用暂停的时间,但却增长了新生代垃圾收集时应用暂停的时间、下降了吞吐量并且须要占用更大的堆空间。

7、G1 收集器

G1最为新一代的垃圾回收器,设计之初就是为了取代CMS的。具有如下优势:

  • 并发执行垃圾收集
  • 很短的时间进行内存整理
  • GC的暂停时间可控
  • 不须要牺牲吞吐量
  • 不须要占用额外的java堆空间 什么须要使用G1收集器呢?
  • 频繁的FGC
  • 对象分配率或提高的速率差别很大。
  • 没法接受过长的GC暂停和内存整理时间

G1收集器和以前垃圾收集器拥有彻底不一样的内存结构,虽然从逻辑上也存在年轻代、老年代,可是物理空间上不在连续而是散列在内存中的一个个regions。内存空间分割成不少个相互独立的空间,被乘称做regions。当jvm启动时regins的大小就被肯定了。jvm会建立大概2000个regions,每一个region的大小在1M~32M之间。内存结构以下图:

7.1 使用G1进行年轻代收集

当年轻代GC被触发时,Eden中存活的对象将会被复制或者移动evacuated到幸存区的regions,在幸存复制的次数到达阈值的存活对象将会晋升到老年区。这个过程也是一个Stop-the-world 暂停。eden和survivor的大小将会在下一次年轻代GC前从新计算。

总而言之,G1在年轻代的手机行为包括如下几点:

一、内存被分割成相互独立的大小相等的regions。 二、年轻代散列在整个内存空间中,这样作的好处是当须要从新分配年轻代大小时会很是方便。 三、stop-the-word 暂停全部线程。 四、实际上也是并行回收算法,多线程并行收集。 五、存活的对象将被复制到新的 survivor或老年代 regions。

7.2 使用G1进行老年代收集

过程 描述
初始标记 stop-the world 一般伴随在年轻代GC后面,标记有被老年代对象关联的幸存区 regions
扫描根 Regions 和应用线程并发执行,扫描幸存区regions
并发标记 并发标记整个堆存活的对象
重标记 完成整个堆的存活对象标记,使用snapshot-at-the-beginning (SATB)算法标记存活对象,该算法比CMS中使用的更快。stop-the-word
并行清理 并行清理死亡的的对象,返回空的regoins到可用列表。
复制 复制存活的对象到新的regions,This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)].

最佳实践

一、不要指定年轻代大小 -Xmn,G1每次垃圾收集结束后都会重新计算并设置年轻代的大小,将会影响全局的暂停时间 二、响应时间配置 -XX:MaxGCPauseMillis=<N> 三、如何解决清理 or 复制失败问题,经过增长-XX:G1ReservePercent=n配置预留空间的大小,防止Evacuation Failure,默认值是10.也可使用-XX:ConcGCThreads=n增长并发标记的线程数来解决

8、相关命令

选择垃圾回收

-XX:+UseSerialGC            串行垃圾收集器
-XX:+UseParallelGC          并行垃圾收集器
-XX:+UseParallelOldGC       并行整理垃圾收集器
-XX:+UseConcMarkSweepGC	    并发标记清理(CMS)垃圾收集年轻代默认使用-XX:+ParNewGC
-XX:+UserG1GC
复制代码

查看垃圾收集日志

-XX:+PrintGC                
-XX:+PrintGCDetails     
-XX:+PrintGCTimeStamps      
复制代码

对大小配置:

-Xmsn                           堆最小值
-Xmxn                           堆最大值
-Xmn                            年轻代大小
-XX:NewRatio=n	                年清代比例 Client_JVM=2 Server_JVM=8     
-XX:SurvivorRatio=n	            幸存去比例
-XX:MaxPermSize=n               依赖于不一样平台的实现永生代的最大值(java 8 之后启用)。
复制代码

G1可用的配置

-XX:+UseG1GC	Use the Garbage First (G1) Collector
-XX:MaxGCPauseMillis=n	Sets a target for the maximum GC pause time. This is a soft goal, and the JVM will make its best effort to achieve it.
-XX:InitiatingHeapOccupancyPercent=n	Percentage of the (entire) heap occupancy to start a concurrent GC cycle. It is used by GCs that trigger a concurrent GC cycle based on the occupancy of the entire heap, not just one of the generations (e.g., G1). A value of 0 denotes 'do constant GC cycles'. The default value is 45.
-XX:NewRatio=n	Ratio of new/old generation sizes. The default value is 2.
-XX:SurvivorRatio=n	Ratio of eden/survivor space size. The default value is 8.
-XX:MaxTenuringThreshold=n	Maximum value for tenuring threshold. The default value is 15.
-XX:ParallelGCThreads=n	Sets the number of threads used during parallel phases of the garbage collectors. The default value varies with the platform on which the JVM is running.
-XX:ConcGCThreads=n	Number of threads concurrent garbage collectors will use. The default value varies with the platform on which the JVM is running.
-XX:G1ReservePercent=n	Sets the amount of heap that is reserved as a false ceiling to reduce the possibility of promotion failure. The default value is 10.
-XX:G1HeapRegionSize=n
复制代码

参考:

Getting Started with the G1 Garbage Collector

Memory Management in the Java HotSpot™ Virtual Machine

相关文章
相关标签/搜索