垃圾收集器G1和CMS ,以及老年代和新生代的比例设置

 

首先

1.G1是包括年轻代和年老代的GC

2.CMS是年老代GC

3.二者在某些时候都需要FullGC(serial old GC)的辅助

 

###CMS收集器:
CMS(ConCurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,从Mark-Sweep上可以看出,CMS收集器(以下简称CMS)是基于“标记-清除”算法实现的。主要应用于B/S模式的服务端(希望系统停顿时间尽可能短,尤其重视响应时间)
CMS的运作过程可以分为4个步骤

**1. 初始标记(CMS initial mark) **

**2. 并发标记(CMS concurrent mark) **

3. 重新标记(CMS remark)

4. 并发清除(CMS concurrent sweep)

  1. 详细说明:初始标记(CMS initial Mark)是需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能关联到的对象,速度很快。

  2. 并发标记阶段就是进行GC Roots Tracing 的过程。

  3. 重新标记阶段:因为并发标记阶段用户程序继续执行,导致原先标记产生变动,所以需要对原先标记的记录进行修正。从这个作用可以看出,重新标记阶段和初始标记阶段同样需要“Stop The World”。这个阶段的停顿时间一般比初始标记阶段稍长一点,但远比并发标记的时间要短。

  4. 并发清除阶段:从前面的语义解释就可以看出,该阶段用户线程同垃圾清除线程同时执行(这里可以看出由于清除阶段用户线程还在运行, 自然就会产生新的垃圾(称为“浮动垃圾”), 因为新产生的垃圾在垃圾标记阶段之后,所以这部分新产生的垃圾CMS无法在本次收集过程中处理掉它们,只能留到下次GC时再清理)。
    这里写图片描述
    ####CMS收集器的缺点

    1. 对CPU资源很敏感:CMS默认启动的回收线程数量是(CPU的个数+3)/4,如果CPU的个数在4个以上,
      收集器会占用不少于25%的CPU资源,CPU个数越多,收集器会占用不少于25%的CPU资源,
      CPU个数越多,CPU资源占比越小。但是当CPU不足4(例如2个)的时候,CMS收集器占用一半的运算能力去执行收集器线程。本来CPU负载就比较大,就会导致用户程序的执行效率下降的很明显。

    2. 无法处理浮动垃圾:由于最后的垃圾清除阶段是并发进行的,伴随着程序的运行产生的新的垃圾,在本次收集过程中无法处理掉,指的下次GC时再清理。
      而且还需要留足够的内存空间给用户线程使用,不能像其他收集器那样等到老年代几乎被填满了再进行收集,需要预留一部分空间提供并发收集时用户线程使用。

    3. 产生大量的空间碎片。CMS是基于“标记-清除”算法实现的收集器。因为这种算法在收集结束后有大量的空间碎片,当碎片过多时,会给大对象的内存
      分配带来很大麻烦,往往在老年代中还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象导致不得不提前触发一次Full GC。

###G1(Garbage First)收集器
面向服务端应用的垃圾收集器。G1名称由来:G1收集器,Java堆的内存布局是将整个Java堆分为多个大小相等的独立区域(Region),也保留了新生代
和老年代的概念。但是新生代和老年代不再是物理隔离的,它们都是一部分Region的集合。G1跟踪各个Region里面的垃圾堆积的价值大小(也就是回收获得的空间大小以及回收需要的时间的经验值),在后台维护一个优先列表,每次根据允许的的收集时间,优先回收价值最大的Region。

G1的运作过程大致划分为几个步骤

1. 初始标记(Initial Marking)

2. 并发标记(Concurrent Marking)

3. 最终标记(Final Marking)

4. 筛选回收(Live Data Counting and Evacuation)

  1. 详细说明第一步:初始标记(Initial Marking),同CMS收集器很相似,仅仅是标记一下GC Root能关联到的对象,这阶段需要停顿线程,但耗时很短。

  2. 并发标记(Concurrent Marking):是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时比较长,但是可与用户程序并发执行。

  3. 最终标记(Final Marking):为了修正在并发标记期间因为用户程序执行而导致标记产生变动的那一部分的标记记录。所以这个阶段同初始标记阶段一样需要"Stop The World"。可以并行执行(即最终标记线程同时执行)。

  4. 筛选回收(Live Data Counting Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这个阶段也是"Stop The World"的。(这个阶段也可以做到同用户线程一起并发执行,因为只回收一部分Region时间是用户可控制的,停顿用户线程可以大幅提高收集效率。所以选择了“Stop The World”)。
    这里写图片描述
    ####G1收集器的特点:

    1. 并行与并发:G1能充分利用多CPU,多核的硬件优势来缩短Stop—The—World停顿的时间,部分其它收集器原本需要停顿用户线程执行的GC动作,
      G1依然可以通过并发的方式让用户线程继续执行。

    2. 分代收集:同其它收集器一样,分代概念依然在G1中保留。G1可以不需要其它收集器配合就能独立管理整个GC堆,而且采用了不同的方式处理
      新创建的对象,已经存活一段时间的对象,熬过多次GC的旧对象,来获得更好的收集结果。

    3. 空间整合:使用算法从整体上来看是基于标记——整理实现的。局部来看,是基于“复制”算法实现的。所以G1在运作期间不会产生内存空间碎片,
      收集后能提供规整的可用内存。这有利于程序长时间运行。

    4. 可预测的停顿:G1同CMS都追求低停顿时间。但是G1还能建立可预测的停顿时间模型(通过有计划的避免在整个Java堆中进行全区域的垃圾收集,
      而是将整个Java堆分为多个大小相等的独立区域(Region),也保留了新生代和老年代的概念。但是新生代和老年代不再是物理隔离的,它们都是
      优先列表,每次根据允许的的收集时间,优先回收价值最大的Region。),能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集
      的时间上不超过N毫秒,这几乎是实时Java的垃圾收集器的特征。

 

总结

   随着JVM的发展,ORACLE官方推出的JDK11又有了新算法ZGC,它对内存碎片的整理更加优化,回收暂停时间也更加缩短,具体细节本人还没有深入研究,后面有机会可以写文章专门介绍它。

最后,我把目前主流JDK使用到的JVM垃圾收集器采用的算法做下简单总结,方便大家对比参考,

  1. 新生代垃圾收集器
  • Serial-复制算法:Serial收集器是新生代单线程收集器,优点是简单高效,算是最基本、发展历史最悠久的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。
  • ParNew收集器-复制算法:ParNew收集器是新生代并行收集器,其实就是Serial收集器的多线程版本。
  • Parallel Scavenge(并行回收)-复制算法:
    Parallel Scavenge收集器是新生代并行收集器,追求高吞吐量,高效利用 CPU。该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)。停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可用高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。

 

  2. 老年代垃圾收集器

  • Serial Old-标记整理算法:Serial Old是Serial收集器的老年代版本,它同样是一个单线程(串行)收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用
  • Parallel Old-标记整理算法:Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。
  • CMS:标记清除算法
  • G1:标记整理算法

 

 

CMS和G1的区别

 

CMS:以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现

过程:

1、初始标记:独占PUC,仅标记GCroots能直接关联的对象

2、并发标记:可以和用户线程并行执行,标记所有可达对象

3、重新标记:独占CPU(STW),对并发标记阶段用户线程运行产生的垃圾对象进行标记修正

4、并发清理:可以和用户线程并行执行,清理垃圾

优点:

并发,低停顿

缺点:

1、对CPU非常敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢

2、无法处理浮动垃圾:在最后一步并发清理过程中,用户县城执行也会产生垃圾,但是这部分垃圾是在标记之后,所以只有等到下一次gc的时候清理掉,这部分垃圾叫浮动垃圾

3、CMS使用“标记-清理”法会产生大量的空间碎片,当碎片过多,将会给大对象空间的分配带来很大的麻烦,往往会出现老年代还有很大的空间但无法找到足够大的连续空间来分配当前对象,不得不提前触发一次FullGC,为了解决这个问题CMS提供了一个开关参数,用于在CMS顶不住,要进行FullGC时开启内存碎片的合并整理过程,但是内存整理的过程是无法并发的,空间碎片没有了但是停顿时间变长了

CMS 出现FullGC的原因:

1、年轻带晋升到老年带没有足够的连续空间,很有可能是内存碎片导致的

2、在并发过程中JVM觉得在并发过程结束之前堆就会满,需要提前触发FullGC

 

G1:是一款面向服务端应用的垃圾收集器

特点:

1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。

2、分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。

3、空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。

4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。

 

 

与其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和来年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。

 

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:

1、初始标记(Initial Making)

2、并发标记(Concurrent Marking)

3、最终标记(Final Marking)

4、筛选回收(Live Data Counting and Evacuation)

看上去跟CMS收集器的运作过程有几分相似,不过确实也这样。初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。而最终标记阶段需要吧Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。下图为G1收集器运行示意图:

 

 

JVM老年代和新生代的比例

Java 中的堆是 JVM 所管理的最大的一块内存空间,主要用于存放各种类的实例对象。
在 Java 中,堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor。
这样划分的目的是为了使 JVM 能够更好的管理堆内存中的对象,包括内存的分配以及回收。
堆的内存模型大致为:

1

从图中可以看出: 堆大小 = 新生代 + 老年代。其中,堆的大小可以通过参数 –Xms、-Xmx 来指定。
(本人使用的是 JDK1.6,以下涉及的 JVM 默认值均以该版本为准。)
默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。
默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。
JVM 每次只会使用 Eden 和其中的一块 Survivor 区域来为对象服务,所以无论什么时候,总是有一块 Survivor 区域是空闲着的。
因此,新生代实际可用的内存空间为 9/10 ( 即90% )的新生代空间。

posted @ 2021-05-29 17:30  abcdefghijklmnop  阅读(4726)  评论(0编辑  收藏  举报