JVM之垃圾收集器与内存分配回收策略(二)

上一篇JVM垃圾收集器与内存分配策略(一),下面是jdk1.7版本的垃圾收集器之间的关系,其中连线两端的两种垃圾收集器可以进行搭配使用,下面来总结一下这些收集器的一些特点以及关系。

一、Serial收集器

  1、serial收集器是一个单线程的收集器,单线程说明两点:①只会使用一个CPU或者一条线程来完成垃圾收集的工作;②在进程垃圾收集的时候,必须暂停掉其他所有的工作线程(Stop The World),直到收集结束。这项收集的工作是虚拟机在用户不可见的情况下将其正常工作的线程停掉,然后在后台进行自动发起收集和完成收集。这对于用户体验而言是不佳的,但是考虑到防止在收集的时候用户程序又在产生垃圾,这样的话效果不好。

  2、Serial收集器是虚拟机运行在Client模式下的默认新生代收集器,它简单高效,对于限定在单个CPU的环境中因为不存在线程切换交互的开销,单对于垃圾收集而言能够活着很高的单线程效率。在用户桌面应用程序中,分配给虚拟机管理的内存一般不会很大,对于使用少量内存的新生代而言,停顿时间一般控制在几十毫秒活着一百多毫秒,发生的也不频繁所以是可以接受的。

二、ParNew收集器

  1、上面说到Serial收集器是单线程的版本,而ParNew可以说就是Serial收集器的多线程版本,下面是ParNew收集器的简单工作流程

  2、ParNew收集器是运行在Server模式下面的虚拟机首选的新生代收集器,从最开始虚拟机之间搭配使用关系的时候可以看出,只有ParNew能够和CMS(后面会说到Concurrent Mark Sweep)搭配使用(对于Server模式)

  3、我们也说到过,ParNew收集器对于在单CPU环境下面,由于存在线程交互的开销,使用效果不会比Serial收集器好,当然随着CPU数量的增加,对于GC时候系统资源的利用率提高和自身收集效率都是有很大好处。

三、Parallel Scavenge收集器

  1、Parallel Scavenge收集器也是一个新生代收集器,使用复制算法,采用并行多线程方式的收集器。

  2、Parallel Scavenge收集器的特点在于:它想要达到一个可控制的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集器运行时间))。对于停顿时间而言,其越短就越适合与用户交互的程序,良好的响应速度能够提升用户体验,较高的吞吐量就可以高效率的利用CPU时间,完成程序执行的任务。

  3、Parallel Scavenge收集器提供下面两个参数用于控制吞吐量

  ①-XX:MaxGCPauseMillis 用于设置最大垃圾收集停顿时间的参数:MaxGCPauseMillis的数值是一个大于0的毫秒数,垃圾收集器将尽可能保证在设置的时间内完成工作。这里要注意的是:GC停顿时间是以牺牲吞吐量和新生代空间来换取的,如果将新生代调小一些,那么垃圾收集也会变得更加频繁。比如说将新生代空间由500M调整为300M,将收集频率由10秒变为5s,相对而言收集变得更加频繁,然后停顿时间由100ms变为70ms,这样的话虽然停顿时间缩短了,但是系统的吞吐量也变小了。

  ②-XX:GCTimeRatio 直接设置吞吐量大小的参数:其值是一个>0且<100的整数,也就是垃圾收集时间占总时间的比例。比如将参数设置为19,那么允许的最大GC时间就是总时间的5%=1/(1+19),默认值是99%=1/(1+99)

  4、Parallel Scavenge收集器也被称为吞吐量优先的收集器,除了上面的参数之外,它还提供一种自适应策略,

  +XX:UserAdapterSizePolicy  :这是一个开关参数,当这个参数打开之后,不需要手动指定新生代的大小(-Xmn)、Eden区和Survivor区的比例、晋升老年代对象年龄大小等参数了,虚拟机将会根据当前系统的运行情况收集性能控制信息动态调整这些参数。

四、Serial Old收集器

  1、Serial Old收集器是Serial的老年代版本,同样是一个单线程的收集器,采用标记-整理的算法进行收集,主要也是在Client模式下面使用

  2、下面是Serial Old的简单工作流程

五、Parallel Old收集器

  1、Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和标记-整理算法来进行实现

  2、下面是Parallel Old收集器的工作流程

  

  3、在最开始介绍各种收集器之间搭配使用的时候,我们可以看到,在Parallel Old收集器出现之前,Parallel Scavenge收集器就只能和单线程的Serial Old收集器搭配使用,而Serial Old在Server应用性能上面比较低(多CPU情况下无法充分利用多CPU的处理能力),所以原本注重吞吐量的Parallel Scavenge收集器就不能充分发挥作用。而使用Parallel Old+Parallel Scavenge收集器则可以充分发挥多CPU的处理能力,在吞吐量的提高上面比较客观。

六、CMS收集器

  1、CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿回收时间为目标的收集器。对于服务器响应速度快、系统停顿时间短的要求能够很好的满足。

  2、CMS收集器是基于标记-清除的算法来进行实现的,其工作过程分为下面四个步骤

  ①初始标记:需要Stop The World,该阶段仅仅是标记一些GC Roots能够直接关联到的对象,

  ②并发标记:并发标记阶段就是GC Tracing的过程,标记与GC Roots间接关联的对象,不需要Stop The World

  ③重新标记:修正并发标记阶段因用户程序继续执行而导致标记产生变动的那一部分对象,这一阶段的时间开销会比初始标记稍长,但是会比并发标记短

  ④并发清除:进行垃圾收集

  3、下面是CMS收集器的工作流程:其中耗时比较长的并发标记和并发清除都可以和用户程序一起执行,采用并发的方式减少了时间停顿

  

  4、下面是CMS收集器的缺点

  ①由于他的并发性,导致其对于CPU的资源比较敏感,如果硬件资源不够,那么CMS收集器的效率会很低;除此之外,虽然CMS工作时候不会导致用户程序停顿,但是会占用CPU的一部分资源导致用户程序忽然变慢,总的吞吐量会降低。

  ②CMS收集器无法处理浮动垃圾,可能会出现并发标记失败而提前出发一次Full GC。由于CMS并发清理阶段用户程序还在继续运行,就会产生新的垃圾对象,这一部分出现在标记过程之后,CMS无法在当次收集的过程中处理掉,就只能留待下一次进行处理,这些就是浮动垃圾。

  ③CMS收集结束会产生大量空间碎片。CMS是基于标记-清除算法来进行实现的收集器,这种收集算法就会导致比较多的空间碎片,而且在无法找到足够的连续空间来分配当前对象的时候,就会提前触发一次Full GC。CMS收集器提供-XX:+UseCMSCompactAtFullCollection参数用于不得不触发Full GC的时候进行内存碎片的整理合并(但是这种方式的缺点也很显然:内存整理的过程是无法并发的,就会导致停顿时间变长)。

七、G1收集器

  1、G1收集器的特点:

  ①并行与并发:CPU利用多CPU、多个核的特点,使用多个CPU来缩短Stop The  World的时间

  ②分代收集:G1采用不同与其他方式去处理新创建的对象和已经存活一段时间(熬过多次GC)的对象

  ③空间整合:G1整体上采用的是标记-整理算法来实现,局部上看则是复制算法实现,而这两种算法都不会产生空间碎片

  ④可预测的停顿:G1除了降低停顿之外,还建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,控制消耗在垃圾收集上面的时间不超过N秒

  2、在G1收集器的眼里,它将Java堆分为多个大小相等的区域(Region),虽然还保留有新生代和老年代的概念,但是都是一个Region的集合,不再是物理隔离的了。

  3、G1能够建立可预测的停顿,是因为能有计划的避免在Java堆中进行全区域的垃圾收集。G1追踪各个Region里面的垃圾堆积回收后所获得空间大小以及回收所需要的时间值 (作为一个回收价值参考,然后在后台建立一个优先列表,然后在收集的时候根据允许的收集时间优先回收价值最大的Region。

  4、关于G1的化整为零思想

  实际上,Region的实现复杂:考虑将Java堆分为多个Region后,垃圾收集不一定就是按照想要的按照Region为单位的方式进行执行,因为Region不是孤立的。当一个对象分配在某一个Region中,该对象不是只能够被本Region中的对象进行引用,而是可以和整个Java堆中任意的对象发生引用关系(换句话说,不同的Region之间其中的对象总是可能存在引用关系)。这样的问题不止存在于G1收集器中,在其他的收集器中(新生代中的对象可能和老年代的对象有引用关系)同样存在,那么GC的时候效率就会降低很多(扫描整个Java堆)。

  在G1和其他分代收集器中,处理不同区域之间的对象引用的方式是这样的:使用Remember Set来避免全堆扫描。G1中每个Region都有一个与之对应的Remember Set,虚拟机在发现对Reference 类型的数据进行Write操作的时候,会首先触发一个Write Barrier中断操作,去检查该Reference对象是否在其他Region存在引用(在其他分代收集器中就是检查老年代中的对象是否引用了新生代中的对象),如果存在的话就会将相关引用信息记录到被引用对象所属的Region的Remember Set中。这样在进行内存回收的时候,在GCRoot的枚举范围之内加入Remember Set就能保证不对整个Java堆扫描也不会遗漏存在引用关系的对象。

  5、下面是G1收集器的简单工作流程

  

  ①初始标记:仅仅标记一些能与GC Roots之间关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发执行的时候,能在正确可用的Region中创建对象(需要Stop The World)

  ②并发标记:从GC Roots开始进行可达性分析,找出存活的对象,这一阶段用户线程的执行可能会改变引用关系,JVM会将标记变动记录线程的Remember Set Logs里面,在最终标记阶段合并到Remember Set中

  ③最终标记:修正在并发标记阶段因为用户程序继续执行导致标记产生变动的那一部分标记记录,并发标记阶段中Remember Set Logs中的数据合并到Remember Set中,需要Stop The  World,可以并行执行

  ④筛选回收:首先对各个Region的回收价值和成本进行排序,根据所期望的GC停顿时间来指定回收计划(可以和用户程序一起并发执行)

  6、优点

  ①与CMS收集器相比:不会产生空间碎片(按照Region为单位进行回收,并且采用的是标记整理算法来实现);并发阶段可以为工作线程预留足够的空间(单独分配空闲的Region空间提供使用);有可以预测的停顿时间

八、内存分配与回收策略

1、对象优先在Eden区上分配

  a)关于Minor GC和Full GC

  ①Minor GC:新生代GC,指发生在新生代的垃圾收集动作,因为Java新生对象大多数具有朝生夕灭的特点,所以新生代GC发生的比较频繁,回收速度也比较快

  ②Full GC:老年代GC,也叫MajorGC;发生在老年代,一般回收速度比Minor GC慢10倍。

  b)一般情况下,对象优先在Eden区分配,当Eden区没有足够的空间的时候,将会触发一次Minor GC;

  c)下面使用一个简单的例子,然后通过-XX:+PrintGCDetails查看GC日志信息

 1 package cn.jvm.test;
 2 
 3 public class Test08 {
 4     private static final int test = 1024 * 1024;
 5     //-Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
 6     public static void main(String[] args) {
 7         byte[] t1, t2, t3, t4;
 8         t1 = new byte[2 * test];
 9         t2 = new byte[2 * test];
10         t3 = new byte[2 * test]; //这个时候Eden区已经分配了6M内存,而Survivor区只有1M大小
11         t4 = new byte[4 * test];
12     }
13 }

  ①上面的例子中,我们使用-Xms20M设置堆空间初始大小为20M, -Xmx20M设置堆空间最大为20M, -Xmn10M设置新生代大小为10M, -XX:+PrintGCDetails 打印GC日志信息,-XX:SurvivorRatio=8设置新生代中Eden:From:To = 8:1:1

  ②下面是GC日志信息

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

  a)大对象就是指那些需要连续内存空间的Java对象,典型的就是很长的字符串或者数组(尽量避免使用朝生夕灭的大对象,因为会使虚拟机不得不提前出发GC找到足够的连续空间进行分配)

  b)虚拟机提供-XX:PretenureSizeThreshold参数,用于设置大于这个值的时候对象直接在老年代进行分配(避免在Eden区和Survivor区发生大量的内存复制)

  c)下面是一个简单的例子,和打印的GC日志信息

1 package cn.jvm.test;
2 
3 public class Test09 {
4     public static void main(String[] args) {
5         byte[] test = new byte[6 * 1024 * 1024];
6     }
7 }

  ①上面的例子中,我们使用-XX:PretenureSizeThreshold=31400000设置大于这个值的时候,分配对象直接分配在老年代

  ②下面是GC日志信息

3、长期存活的对象直接进入老年代

  a)虚拟为每个分配的对象定义了一个对象年龄计数器,如果新生对象在Eden区并经过一次GC后被成功复制到Survivor区之后,就会将其对象年龄设置为1,之后每次熬过一次GC都会将对象年龄加1,当年龄达到一定的大小(默认15)的时候,就会进入老年代。

  b)虚拟机提供-XX:MaxTenuringThreshold参数设置年龄的阈值

4、动态对象年龄判定

  除了按照对象的年龄来判断对象是否需要进入老年代,在实际当中,并不是硬性要求对象的年龄必须达到XX:MaxTenuringThreshold设置的值才会进入老年代,通常情况下:当Survivor空间中所有同龄对象的大小总和超过了空间的一半,那么年龄大于等于该年龄的对象就直接分配在老年代

5、空间分配担保机制

  a)首先,我们需要知道新生代都采用复制算法,当Survivor空间不够存放存活的对象的时候,就需要老年代进行分配担保。如果老年代的剩余空间大于新生代所有对象之和,那么担保是没有问题的,但是如果新生代的所有存活对象都大于Survivor,那么这些对象就会因为进入老年代导致老年代空间不足而触发FullGC,此时的老年代担保是有风险的。

  b)对于这个风险的问题,JVM会查看参数HandlePromotionFailure参数(是否允许冒险)的设置,如果不允许就会直接进行FullGC,如果允许的话就会先查看一下每次进入老年代的对象大小之和的平均值(如果老年代的剩余空间大小大于这个平均值,就认为可以冒险),但是如果在冒险进行Minor GC的时候发现新生代100%对象都存活(这是一种极端情况),那么还是会进行FullGC。看起来担保失败的时候绕了比较大的弯子,但是为了避免FullGC的频繁出现还是会将这种冒险的行为设置为true(JDK6Update24之后的默认设置)

 

posted @ 2019-03-12 09:48  夏末秋涼  阅读(445)  评论(0编辑  收藏  举报