JVM性能调优之三垃圾回收机制

     我们大家都知道,java的内存回收不需要程序员过多关注,是由JVM自行回收的。但是完全依靠JVM的GC,在一些场景下可能效果不是很好,需要人为调优。

     面对不同的业务场景,垃圾回收的调优策略也不一样。例如,在对内存要求苛刻的情况下,需要提高对象的回收效率;在 CPU 使用率高的情况下,需要降低高并发时垃圾回收的频率。可以说,垃圾回收的调优是一项必备技能。

【垃圾回收机制GC】

      从JVM性能调优之一JVM内存结构可以知道,JVM内存包含五大块。而垃圾回收则发生在堆和方法区。一般情况下一个对象不再被引用,就代表该对象可以被回收了。目前有2种算法可以判断对象是否可以被回收。

    引用计数算法:这种算法是通过一个对象的引用计数器来判断该对象是否被引用了。每一个对象被引用时计数器就加1,而当引用失效时,计数器就减1.。当计数器为0时,表示该对象不再被引用了。表示可以被GC回收了。

   可达性费心算法:GC  Roots  是该算法的基础,GC Roots 是所有对象的根对象,在 JVM 加载时,会创建一些普通对象引用正常对象。这些对象作为正常对象的起始点,在垃圾回收时,会从这些 GC Roots 开始向下搜索,当一个对象到 GC Roots 没有任何引用链相连时,就证明此对象是不可用的。目前 HotSpot 虚拟机采用的就是这种算法。

     需要说明一点,一个对象虽然没有再被引用了,JVM也不一定会马上回收改对象,GC在JVM中是自动执行的,java程序无法强制执行。我们唯一能做的是是通过调用System.gcf方法来“建议”执行GC,但是什么时候执行仍不能知道。

【GC算法】

    JVM性能调优之三垃圾回收机制

 

【衡量GC的性能指标】

     吞吐量: 这里的吞吐量是指应用程序所花费的时间和系统总运行时间的比值。我们可以按照这个公式来计算 GC 的吞吐量:系统总运行时间 = 应用程序耗时 +GC 耗时。如果系统运行了 100 分钟,GC 耗时 1 分钟,则系统吞吐量为 99%。GC 的吞吐量一般不能低于 95%。
    停顿时间:指垃圾收集器正在运行时,应用程序的暂停时间。对于串行回收器而言,停顿时间可能会比较长;而使用并发回收器,由于垃圾收集器和应用程序交替运行,程序的停顿时间就会变短,但其效率很可能不如独占垃圾收集器,系统的吞吐量也很可能会降低。
    垃圾回收频率:多久发生一次指垃圾回收呢?通常垃圾回收的频率越低越好,增大堆内存空间可以有效降低垃圾回收发生的频率,但同时也意味着堆积的回收对象越多,最终也会增加回收时的停顿时间。所以我们只要适当地增大堆内存空间,保证正常的垃圾回收频率即可。

 

【GC调优策略】

      1. 降低 Minor GC 频率
       通常情况下,由于新生代空间较小,Eden 区很快被填满,就会导致频繁 Minor GC,因此我们可以通过增大新生代空间来降低 Minor GC 的频率。

     2. 降低 Full GC 的频率
        通常情况下,由于堆内存空间不足或老年代对象太多,会触发 Full GC,频繁的 Full  GC 会带来上下文切换,增加系统的性能开销。我们可以使用哪些方法来降低 Full GC 的频率呢?
       减少创建大对象:在平常的业务场景中,我们习惯一次性从数据库中查询出一个大对象用于 web 端显示。例如,我之前碰到过一个一次性查询出 60 个字段的业务操作,这种大对象如果超过年轻代最大对象阈值,会被直接创建在老年代;即使被创建在了年轻代,由于年轻代的内存空间有限,通过 Minor GC 之后也会进入到老年代。这种大对象很容易产生较多的 Full GC。
我们可以将这种大对象拆解出来,首次只查询一些比较重要的字段,如果还需要其它字段辅助查看,再通过第二次查询显示剩余的字段。
       增大堆内存空间:在堆内存不足的情况下,增大堆内存空间,且设置初始化堆内存为最大堆内存,也可以降低 Full GC 的频率。
3、选择合适的 GC 回收器
         假设我们有这样一个需求,要求每次操作的响应时间必须在 500ms 以内。这个时候我们一般会选择响应速度较快的 GC 回收器,CMS(Concurrent Mark Sweep)回收器和 G1 回收器都是不错的选择。
       而当我们的需求对系统吞吐量有要求时,就可以选择 Parallel Scavenge 回收器来提高系统的吞吐量。