3.JVM-垃圾回收

垃圾回收

  • 垃圾回收
    • 1. 判断对象是否可以回收
      • 1.1引用计数法
      • 1.2可达性分析算法
      • 1.3四种引用
        • 概念
        • 软引用应用
        • 软引用引用队列
        • 弱引用应用
    • 2. 垃圾回收算法
      • 1. 标记清除(Mark-Sweep)
      • 2. 复制算法(Copying)
      • 3. 标记整理(Mark-Compact)
      • 总结
    • 3.分代回收
      • 3.1 相关VM设置参数
    • 4. 垃圾收集器
      • 4.1 串行收集器
        • Serial收集器
      • 4.2 吞吐量(Throughput)优先
        • Parallel Scavenge 收集器
          • 参数
      • 4.3 响应时间优先
        • ParNew 收集器 新生代
        • CMS收集器(Concurrent Mark Sweep)老年代
        • 参数
      • 4.4 G1垃圾收集器
        • 1) G1垃圾回收阶段
        • 2) Young Collection
          • Young Collection 跨代引用
        • 3) Young Collection + CM(Concurrent Mark)
        • 4) Mixed Collection
        • 5) Full GC
        • 6) Remark
        • 7) JDK 8u20 字符串去重
        • 8) JDK 8u40 并发标记类卸载
        • 9) JDK 8u60 回收巨型对象
        • 10) JDK9并发标记起始时间调整
        • 特点
    • 5. 垃圾回收调优(GC调优)
      • 5.1 调优领域
      • 5.2 调优目标
      • 5.3 最快的GC是不发生GC
      • 5.4 新生代调优
      • 5.5 老年代调优
      • 5.6 案例

垃圾回收

1. 判断对象是否可以回收

1.1引用计数法

3.JVM-垃圾回收_第1张图片
  如果两个对象互相引用,计数器都为1,即使他们都没有被使用,都不会被清理。

1.2可达性分析算法

3.JVM-垃圾回收_第2张图片
在Java中,可以作为GC Root 的对象包括下面几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即一般说的Native方法)引用的对象。

查看哪些对象可以作为GC Root?

  1. 使用Eclipse开发的一款分析内存的软件MAT.MAT
  2. jps获取进程号
  3. jmap -dump:format=b,live,file=1.bin 进程ID ;(format=b代表二进制文件格式,live 代表只关注存活对象并且在抓取快照之前进行一次垃圾回收 file=1.bin 代表存入当前目录下的1.bin文件中)
  4. mat打开1.bin(文件名)
    3.JVM-垃圾回收_第3张图片

1.3四种引用

3.JVM-垃圾回收_第4张图片

概念

  • 强引用就是指在程序代码之中普遍存在的,类似"Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
  • 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了 SoftReference类来实现软引用。
  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了 WeakReference类来实现弱引用。
  • 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一 个系统通知。在JDK1.2之后,提供了 PhantomReferencc类来实现虚引用。

  强引用就是沿着GC Root的引用链能够找到他他就不会被垃圾回收,只有GC Root 对他的引用都断开的时候他才能被回收,我们平时使用的引用都是强引用(Object a = new Object() a为引用)

  软弱引用跟强引用的区别就是,只要A2,A3这两个对象没有被直接的强引用所引用,当垃圾回收发生时他们可能被回收掉(只有软引用引用对象时,当垃圾回收时,并且回收完还发现内存不够时,才会回收,而弱引用则在垃圾回收时直接被回收不管内存够不够)

虚引用(以Cleaner为例)
  ByteBuffer分配一块直接内存,并且把直接内存地址传给虚引用对象,将来ByteBuffer一旦没有强引用所引用,ByteBuffer自己被回收掉,然后虚引用进入引用队列(队列中有 Reference Handler 线程定时到引用队列里看有没有新入队的 Cleaner 如果有该线程就会调用 Cleaner 中的 clean() 方法,clean() 方法就会根据前面记录的直接内存的地址调用 Unsafe.freeMemory() 方法去释放直接内存)
3.JVM-垃圾回收_第5张图片

终结器finalize

  1. 终结器对象引用的对象没有被强引用,在被回收前,终结器引用转移到引用队列,一个优先级较低的线程finallize在引用队列中寻找终结器引用;
  2. 并找到终结器引用的对象,调用finalize()方法(不建议使用,判断机制复杂,效率太低);
  3. 下次垃圾回收时,回收该对象。

1.强引用

  • 只有所有GC Roots对象都不通过【强引用】引用该对象,该对象才能被垃圾回收

2.软引用(SoftReference)

  • 仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次出发垃圾回收,回收软引用对象
  • 可以配合引用队列来释放软引用自身

3.弱引用(WeakReference)

  • 仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
  • 可以配合引用队列来释放弱引用自身

4.虚引用(PhantomReference)

  • 必须配合引用队列使用,主要配合ByteBuffer使用,被引用对象回收时,会将虚引用入队,由 Reference Handler线程调用虚引用相关方法释放直接内存

5.终结器引用(FinalReference) 不用了

  • 无需手动编码,但其内部配合引用队列使用,在垃圾回收时,终结器引用入队(被引用对象暂时没有 被回收),再由Finalizer线程通过终结器引用找到被引用对象并调用它的finalize方法,第二次GC时 才能回收被引用对象

软引用应用

  在接收网络上的图片资源(不属于核心业务资源)到链表中的时候如果图片数量过多,使用强引用则会导致内存溢出错误,不太重要的资源,要在内存紧张时释放掉,以后需要再读取。

不重要的资源用软引用引用

public class GCTest1 {
     
    public static final int _4MB = 4 * 1024 * 1024;
    // -Xmx20m -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
     
        //list -X->byte[]
        //list --> SoftReference -->byte[]
        List<SoftReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
     
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB]);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("循环结束:" + list.size());
        for (SoftReference<byte[]> ref : list) {
     
            System.out.println(ref.get());
        }
    }
}

结果:
[B@1b6d3586
1
[B@4554617c
2
[B@74a14482
3
//第3次循环内存已经很紧张了,触发了一次垃圾回收如下。
[GC (Allocation Failure) [PSYoungGen: 1875K->488K(6144K)] 14163K->13028K(19968K), 0.0007523 secs] [Times: user=0.08 sys=0.00, real=0.00 secs] 
[B@1540e19d
4
/*第4次循环发现内存还是不够,又触发了minor回收如下。*/
[GC (Allocation Failure) --[PSYoungGen: 4809K->4809K(6144K)] 17349K->17349K(19968K), 0.0004952 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
/*minor GC 回收效率太低,触发了Full GC*/
[Full GC (Ergonomics) [PSYoungGen: 4809K->4499K(6144K)] [ParOldGen: 12540K->12510K(13824K)] 17349K->17009K(19968K), [Metaspace: 3219K->3219K(1056768K)], 0.0035946 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 

/*垃圾回收后内存还是不够,则将软连接连接的对象清除*/
[GC (Allocation Failure) --[PSYoungGen: 4499K->4499K(6144K)] 17009K->17025K(19968K), 0.0005694 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
/*minor GC 回收效率太低,触发了Full GC*/
[Full GC (Allocation Failure) [PSYoungGen: 4499K->0K(6144K)] [ParOldGen: 12526K->607K(8704K)] 17025K->607K(14848K), [Metaspace: 3219K->3219K(1056768K)], 0.0044119 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[B@677327b6
5
循环结束:5
null
null
null
null/前四次的对象被垃圾回收释放掉了
[B@677327b6
/*后面省略*/

软引用引用队列

  由上个例子看出list集合中,在内存不足时被软引用引用的对象被释放了,但是最后遍历list的时候其中一些的软引用的对象已经是null了,对这些软引用对象已经没必要将他保留在list集合中,希望吧软引用本身也清理掉。

  实现清理无用软引用:使用引用队列(ReferenceQueue泛型与软引用一致)实现对软引用的清理,当软引用关联的对象被回收时,软引用自身会被加入引用队列中去。

public class GCTest1 {
     
    public static final int _4MB = 4 * 1024 * 1024;

    public static void main(String[] args) {
     
        List<SoftReference<byte[]>> list = new ArrayList<>();
        //引用队列
        ReferenceQueue<byte[]> queue = new ReferenceQueue<>();
        for (int i = 0; i < 5; i++) {
     
            //在调用软引用构造方法时,多加一个参数(引用队列)来关联引用队列和软引用对象
            //当软引用关联的 byte[] 被回收时,软引用自身会被加入引用队列
            SoftReference<byte[]> ref = new SoftReference<>(new byte[_4MB],queue);
            System.out.println(ref.get());
            list.add(ref);
            System.out.println(list.size());
        }
        //从引用队列中取出无用的软引用对象,并移除
        Reference<?extends byte[]> poll = queue.poll();//从引用队列中取出第一个引用
        while(poll != null){
     //当引用队列不为null的时候
            list.remove(poll);//移除list中软引用对象
            poll = queue.poll();
        }

        System.out.println("=============打印List中的内容=============");
        for (SoftReference<byte[]> ref : list) {
     
            System.out.println(ref.get());
        }
    }
}

结果:
[B@1b6d3586
1
[B@4554617c
2
[B@74a14482
3
[B@1540e19d
4
[B@677327b6
5
=============打印List中的内容=============
[B@677327b6

弱引用应用

public class GCTest1 {
     
    public static final int _4MB = 4 * 1024 * 1024;

    // -Xmx20m -XX:+PrintGCDetails -verbose:gc
    public static void main(String[] args) {
     
        //list -X->byte[]
        //list --> WeakReference -->byte[]
        List<WeakReference<byte[]>> list = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
     
            WeakReference<byte[]> ref = new WeakReference<>(new byte[_4MB]);
            System.out.print(ref.get()+",");
            list.add(ref);
            System.out.println(list.size());
        }
        System.out.println("=============打印List中的内容=============");
        for (WeakReference<byte[]> ref : list) {
     
            System.out.print(ref.get() + ",");
        }
    }
}
结果:
[B@1b6d3586,1
[B@4554617c,2
[B@74a14482,3
[B@1540e19d,4
[B@677327b6,5
=============打印List中的内容=============
[B@1b6d3586,[B@4554617c,[B@74a14482,null,[B@677327b6,
/*有时打印如下:null,null,null,null,[B@677327b6,*/

负责回收弱引用的线程优先级比较低,不触发Full GC有可能不会全部回收?

2. 垃圾回收算法

1. 标记清除(Mark-Sweep)

3.JVM-垃圾回收_第6张图片
3.JVM-垃圾回收_第7张图片
  最基础的收集算法是“标记-清除”算法,如同它的名字一样,算法分 为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,它的标记过程其实在前一节讲述对象标记判定时已经介绍过了。之所以 说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而 得到的。它的主要不足有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

2. 复制算法(Copying)

3.JVM-垃圾回收_第8张图片
3.JVM-垃圾回收_第9张图片
为了解决效率问题,一种称为“复制”的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半,未免太高了一点。

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1 : 1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块 Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden 和Survivor的大小比例是8 : 1,也就是每次新生代中可用内存空间为整个新生代容量的90% (80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据;我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时, 需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

内存的分配担保就好比我们去银行借翻如果我们信誉很好,在98%的情况下都能按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一现Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

3. 标记整理(Mark-Compact)

3.JVM-垃圾回收_第10张图片
3.JVM-垃圾回收_第11张图片
  复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中 所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
  根据老年代的特点(存活较多),有人提出予另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

总结

  • 复制算法回收速度与存活对象数量相关,数量越少,速度越快,如果存活对象过多,速度可能会比标记整理速度要慢。
  • 实际JVM垃圾回收是根据情况配合三种算法使用,因为三种算法都有其实用的场景。

3.分代回收

  当前商业虚拟机的垃圾收集都采用"分代收集”(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间 对它进行分配担保,就必须使用“标记一清理”或者“标记一整理”算法来进行回收。
3.JVM-垃圾回收_第12张图片

1.对象首先分配在Eden区内;

2.在新生代空间不足时,触发Minor GC,将Eden和From中存活的对象复制到To中,存活的对象年龄加1,并且交换To和From;

  • Minor GC触发 STOP THE WORLD(STW),暂停其他用户线程,直到垃圾回收完毕,才恢复用户线程运行。
  • 当对象寿命超过阈值时,会晋升至老年代,默认阈值为15(4bit表示)有时空间紧张时会提前晋升。
  • 设置-XXPretenureSizeThreshold=3145728(3M)大对象会直接进入老年代,

3.当老年代空间不足,尝试Minor GC,空间仍不足,触发Full GC,触发 STW,时间相比于Minor GC会长的多,因为新生代和老年代回收算法不同,且老年代中对象多,回收较为复杂。

当一个线程抛出OOM异常后,它所占据的内存资源会全部被释放掉

3.1 相关VM设置参数

含义 参数
堆初始大小 -Xms
堆最大大小 -Xmx 或-XX:MaxHeapSize=size
新生代大小 -Xmn 或(-XX:NewSize=size + -XX:MaxNewSize=size)
幸存区比例(动态) -XX:InitialSurvivorRatio=ratio 和-XX:+UseAdaptiveSizePolicy
幸存区比例 -XX:SurvivorRatio=ratio
晋升阈值 -XX:MaxTenmingThreshold=threshold
晋升详情 -XX:+PrintTenurmgDistribution
GC详情 -XX:+PrintGCDetails -verbose:gc
FullGC 前 MinorGC -XX:+ScavengeBeforeFullGC
固定分配(不进行动态分配) -XX:+UseSerialGC

4. 垃圾收集器

  如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。这里讨论的收集器基于JDK 1.7 Update 14之 后的HotSpot虚拟机(在这个版本中正式提供了商用的G1收集器,之前G1仍处于实验状态)
3.JVM-垃圾回收_第13张图片
  上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它 们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。
  虽然我们是在对各个收集 器进行比较,但并非为了挑选出一个最好的收集器。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那HotSpot虚拟机就没必要实现那么多不同的收集器了。

4.1 串行收集器

  • 单线程
  • 堆内存较小,适用于个人电脑(单CPU)

Serial收集器

3.JVM-垃圾回收_第14张图片

开启Serial收集器 -XX:+UseSerialGC = Serial + SerialOld

  这个收集器是一个单线程的收集器,但它的 “单线程”的意义并不仅仅说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作, 更重要的是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。

  现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器。它也有着优于其他收集器的地方:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生, 这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个 很好的选择。

Serial Old 收集器

  Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记一 整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server 模式下,那么它主要还有两大用途:一种用途是在JDK 1.5以及之前的版本中与Parallel Scavenge收集器搭配使用,另一种用途就是作为CMS收集器的后备预案,在并发收集发生 Concurrent Mode Failure时使用。

4.2 吞吐量(Throughput)优先

  • 多线程
  • 堆内存大,多核cpu
  • 单位时间内STW时间最小 0.2+0.2 = 0.4
  • 高吞吐量可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

Parallel Scavenge 收集器

3.JVM-垃圾回收_第15张图片
  Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器。

  Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码 的时间与CPU总消耗时间的比值.

  吞吐量=运行用户代码时间/ (运行用户代码时间+ 垃圾收集时间),虚拟机总共运行了 100分钟,其中垃圾收集花掉1分钟,那吞吐量就是 99%。

  停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验,而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务

  Parallel Old是Pamllel Scavenge收集器的老年代版本,使用多线程和“标记一整理”算法。这个收集器是在JDK 1.6中才开始提供的,在此之前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。原因是,如果新生代选择了 Parallel Scavenge收集器,老年代除了 Serial Old (PS MarkSweep)收集器外别无选择(Parallel Scavenge收集器无法与CMS收集器配合工作)。由于老年代Serial Old收集器在服务端应用性能上的 "拖累”,使用了 Parallel Scavenge收集器也未必能在整体应用上获得吞吐量最大化的效果, 由于单线程的老年代收集中无法充分利用服务器多CPU的处理能力,在老年代很大而且硬 件比较高级的环境中,这种组合的吞吐量甚至还不一定有ParNew加CMS的组合“给力”。

  直到Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

参数

-XX:+UseParallelGC ~ -XX:+UseParallelOldGC 开启Parallel收集器(在JDK 1.8 下是默认开启的,开启其中一个另一个会自动开启)

-XX:ParallelGCThreads=n 修改垃圾回收线程数量(默认和cpu核数相同)

-XX:+UseAdaptiveSizePolicy 采用自适应的大小调整策略,系统会根据系统运行情况修改新生代大小(-Xmn),Eden和Survivor比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等参数;来提供最合适的停顿时间和最大吞吐量

  Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集 停顿时间的-XXzMaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。

  MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值,GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的: 系统把新生代调小一些,收集300MB新生代肯定比收集500MB快吧,这也直接导致垃圾收集发生得更频繁一些,原来10秒收集一次、每次停顿100毫秒,现在变成5秒收集一次、 每次停顿70毫秒。停顿时间的确在下降,但吞吐量也降下来了。

  GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。如果把此参数设置为19,那允许的最大GC时间就占 总时间的5% (即1 / (1+19)),默认值为99,就是允许最大1% (即1 / (1+99))的垃圾收集时间。

  由于与吞吐量关系密切,Parallel Scavenge收集器也经常称为“吞吐量优先”收集器。 除上述两个参数之外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy 。这是一个开关参数,当这个参数打开之后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄 (-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为 GC自适应的调节策略(GC Ergonomics)。如果读者对于收集器运作原来不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是一个不错的选择。只需要把基本的内存数据设置好(如-Xmx 设置最大堆),然后使用MaxGCPauseMillis参数(更关注最大停顿时间)或GCTimeRatio (更关注吞吐量)参数给虚拟机设立一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

4.3 响应时间优先

  • 多线程
  • 堆内存大,多核cpu
  • 尽可能STW单次时间减小 0.1+0.1+0.1+0.1+0.1=0.5
  • 老年代使用CMS垃圾回收器,使用的是标记清除算法。
  • 新生代ParNewGC垃圾收集器,复制算法(ParNewGC是多线程版本的Serial)。

ParNew 收集器 新生代

3.JVM-垃圾回收_第16张图片
  ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、 -XX:PretenureSizeThreshold、-XX:HandlePromotionFailure 等)、收集算法、Stop The World, 对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。

  ParNew收集器除了多线程收集之外,其他与Serial收集器相比并没有太多创新之处, 但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,除了 Serial收集器外,目前只有它能与CMS收集器配合工作。在JDK 1.5时期,HotSpot推出了一款在强交互应用中几乎可认为有划时代意义的垃圾收集器一 CMS收集器(Concurrent Mark Sweep),这款收集器是 HotSpot虚拟机中第一款真正意义上的并发(Concuirent)收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作

  不幸的是,CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器 Parallel Scavenge配合工作,所以 在JDK 1.5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者Serial收集器中的一个

  ParNew收集器也是使用-XX:+UseConcMarkSweepGC 选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它。

  ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。当然,随着可以使用的CPU的数量的增加,它对于 GC时系统资源的有效利用还是很有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU非常多(譬如32个,现在CPU动辄就4核加超线程,服务器超过32个逻辑 CPU的情况越来越多了)的环境下,可以使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。

CMS收集器(Concurrent Mark Sweep)老年代

3.JVM-垃圾回收_第17张图片
3.JVM-垃圾回收_第18张图片
  CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。

  从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于"标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些,整个过程分为4个步骤, 包括:

  • 初始标记(CMS initial mark)
  • 并发标记(CMS concurrent mark)
  • 重新标记(CMS remark)
  • 并发清除(CMS concurrent sweep)

  其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标仅仅只是标记一下 GC Roots 能直接关联到的对象;速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

  由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。通上图可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。

  CMS是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿,Sun公司的一些官方文档中也称之为并发低停顿收集器(Concurrent Low Pause Collector)。但是CMS还远达不到完美的程度,它有以下3个明显的缺点:

  • CMS收集器对CPU资源非常敏感。其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程 (或者说CPU资源)而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是 (CPU数量+3) / 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。但是当CPU不足4 个(譬如2个)时,CMS对用户程序的影响就可能变得很大,如果本来CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。为了应付这种情况,虚拟机提供了一种称为 "增量式并发收集器”(Incremental Concurrent Mark Sweep / i-CMS)的 CMS 收集器变种,所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想一样,就是在并发标记、清理的时候让GC线程、用户线程交替运行,尽量减少GC 线程的独占资源的时间,这样整个垃圾收集的过程会更长,但对用户程序的影响就会显得少一些,也就是速度下降没有那么明显。实践证明,增量时的CMS收集器效果很一般,在目前版本中,i-CMS已经被声明为"deprecated”,即不再提倡用户使用。
  • CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现"Concurrent Mode Failure"失败而导致另一次Full GC的产生。由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。在JDK 1.5的默认设置下,CMS收集器当老年代使用了 68% 的空间后就会被激活,这是一个偏保守的设置,如果在应用中老年代增长不是太快, 可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK 1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次 “Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old 收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSlnitiatingOccupancyFraction 设置得太高很容易导致大量“Concurrent Mode Failure" 失败,性能反而降低。
  • CMS是一款基于"标记一清除”算法实现的收集器,这意味着收集结束 时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC.为了解决这个问题,CMS收集器提供了一个-XX: +UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机设计者还提供了另外一个 参数-XX:CMSFullGCsBeforeCompaction.这个参数是用于设置执行多少次不压缩的 Full GC JS,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。

参数

-XX:UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld

-XX:ParallelGCThread=n,并行线程数,一般为cpu核心数,图中N=4。

-XX:ConcGCThreads=threads,一般设置为n/4,并发垃圾回收线程数。

-XX:CMSInitiatingOccupancyFraction=percent

-XX:CMSScavengeBeforeRemark 设置执行多少次不压缩的FULL GC后,执行一次带压缩的FULL GC

4.4 G1垃圾收集器

定义: Garbage First,jdk9后默认垃圾收集器。

适用场景

  • 同时注重吞吐量和低延迟,默认暂停目标时200ms。
  • 超大堆内存会将内存划分为多个相等的Region。
  • 整体上使用标记-整理算法,两个区域使用复制算法。

相关VM参数

  • -XX:UseG1GC
  • -XX:G1HeapRegionSize=size 设置Region大小(数值为2n
  • -XX:MaxGCPauseMillis=time 设置暂停时间

1) G1垃圾回收阶段

3.JVM-垃圾回收_第19张图片

2) Young Collection

  • 初始标记(Initial Marking)
  • 会触发STW
    3.JVM-垃圾回收_第20张图片
      幸存区对象也比较多了或者幸存区对象超过一定年龄触发垃圾回收幸存区的一部分晋升到老年区,不够年龄的复制到幸存区。
Young Collection 跨代引用
  • 新生代回收的跨代引用(老年代引用新生代)问题

3.JVM-垃圾回收_第21张图片
  将老年代再进行细分,分成一个个的 Card(一个大约512K),如果老年代其中有一个对象引用了新生代对象,就将它标记为 Dirty Card ,做GC Root 遍历的时候就不用遍历整个老年代了,而是只需要关注 Dirty Card 区域就可以了,这样可以减少搜索范围,提高扫描根对象效率。

  新生代有一个叫 Remembered Set ,他会记录Incoming Reference(外部引用)也就是记录都有哪些脏卡,将来对Eden做垃圾回收的时候,可以先通过 Remembered Set 得到对应的 Dirty Card 在通过这些脏卡区域去遍历 GC Root 就减少了 GC Root 的遍历时间。

  在每次对象引用发生变更时,post-write barrier更新脏卡(这是异步操作不会立即完成 Dirty Card 的更新,并把更新指令放入dirty card queue队列中,等待线程执行完成 Dirty Card 更新)

3) Young Collection + CM(Concurrent Mark)

  • 在Young GC 时会进行 GC Root 的初始标记
  • 并发标记(Concurrent Marking)
  • 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定

-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
3.JVM-垃圾回收_第22张图片

4) Mixed Collection

会对E、S、O进行全面垃圾收集

  • 最终标记(Remark)会STW
  • 筛选回收(Live Data Counting and Evacuation) 会STW,

-XX:MaxGCPauseMillis=ms
3.JVM-垃圾回收_第23张图片

  • O->O,回收老年代中的垃圾使用复制算法,将一个老年代区域中存活的对象复制到另一个老年代区域。
  • G1垃圾收集器选择回收价值高的老年代区域(即可回收较大的空间(垃圾多的区域Garbage First)),为了在MaxGCPauseMillis时间内完成垃圾回收
  • 最终标记,需要STW,避免其他用户线程在标记过程中,产生浮动垃圾。

5) Full GC

SerialGC

  • 新生代内存不足minor GC
  • 老年代内存不足Full GC

ParallelGC

  • 新生代内存不足minor GC
  • 老年代内存不足Full GC

CMS

  • 新生代内存不足minor GC
  • 老年代内存不足
    • 处于并发收集阶段不会触发 FULL GC
    • 并发失败会触发 FULL GC

G1

  • 新生代内存不足minor GC
  • 老年代内存不足
    • 老年代占用达到阈值时,若垃圾回收速度大于垃圾产生速度,继续并发回收
    • 反之,会发生FULL GC

6) Remark

3.JVM-垃圾回收_第24张图片
3.JVM-垃圾回收_第25张图片
  并发标记时:当对象的引用发生改变时,JVM会给他加一个写屏障(只要对象引用发生改变这个写屏障的代码就会被执行,并且将该对象加入到队列中),重新标记时线程就会将队列中的对象取出并检查。

  pre-write barrier + satb_mark_queue
3.JVM-垃圾回收_第26张图片

7) JDK 8u20 字符串去重

  • 优点:节约大量内存
  • 缺点:略微多占用cpu运行时间,新生代回收时间略微增加
    -XX:+UseStringDEduplication 打开去除重复字符串(默认打开)
String s1 = new String("hello");//char[]{'h','e','l','l','o'}
String s2 = new String("hello");//char[]{'h','e','l','l','o'}

  将所有新分配的字符串放入一个队列,当新生代回收时,检查是否有重复的字符串,相同的字符串指向同样的字符数组char[]

注意 与String.intern()不同

  • String.intern()注重的时字符串对象
  • 字符串去重更加注重char[]
  • JVM内部使用不同的字符串去重

8) JDK 8u40 并发标记类卸载

所有对象在并发标记后,直到哪些类不在被使用,当类的实例都被回收掉并且、类加载器内所有的类都不再被使用,则卸载他加载的所有类。
-XX:+ClassUnloadingWithConcurrentMark 默认开启

9) JDK 8u60 回收巨型对象

  • 一个对象大于region的一半时,被称为巨型对象。
  • G1不会对巨型对象进行复制(太大,复制算法好费时间)
  • 回收优先考虑。
  • G1会跟踪老年代所有的Incoming引用,老年代引用为0时的巨型对象就可以在新生代的垃圾回收中被处理。
    3.JVM-垃圾回收_第27张图片

10) JDK9并发标记起始时间调整

并发标记必须在堆空间沾满之前完成,否则触发FULL GC

JDK9之前使用-XX:InitiatingHeapOccupancyPercent 容易触发FULL GC 调小了垃圾回收频繁

JDK9 可以去动态调整

  • -XX:InitiatingHeapOccupancyPercent设置初始值
  • 进行数据采样并动态调整
  • 总会添加一个安全的空档空间

特点

与其他GC收集器相比,G1具备如下特点:

  • 并行与并发:G1能充分利用多CPU、多核环境下的硬件优势,使用多个CPU (CPU 或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿 Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
  • 分代收集:与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
  • 空间整合:与CMS的“标记一清理”算法不同,G1从整体来看是基于“标记一整理” 算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的, 但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过 N毫秒,这几乎已经是实时Java (RTSJ)的垃圾收集器的特征了。

  在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划 分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region (不需要连续)的集合。

  G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个 Region 里面的垃圾堆根的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表每次根据允许的收集时间,优先回收价值最大的Region (这也就是Garbage-First名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限的时间以可以 获取尽可能高的收集效率。

  G1把内存“化整为零”的思路,理解起来似乎很容易,但其中的实现细节却远远没有想象中那样简单,把Java堆分为多个Region 后,垃圾收集是否就真的能以Region为单位进行了?听起来顺理成章,再仔细想想就很容易发现问题所在:Region不可能是孤立的。一个对象分配在某个Region中,它并非只能被本 Region中的其他对象引用,而是可以与整个Java堆任意的对象发生引用关系。那在做可达性判定确定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准确性?这个问题其实并非在G1中才有,只是在G1中更加突出而已。在以前的分代收集中,新生代的规模一般都比老年代要小许多,新生代的收集也比老年代要频繁许多,那回收新生代中的对象时也面临相同的问题,如果回收新生代时也不得不同时扫描老年代的话,那么Minor GC的效率可能下降不少。

  在G1收集器中,Region之间的对象引用以及其他收集器中的新生代与老年代之间的对象引用,虚拟机都是使用 Remembered Set 来避免全堆扫描的。G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

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

  • 初始标记(Initial Marking)
  • 并发标记(Concurrent Marking)
  • 最终标记(Final Marking)
  • 筛选回收(Live Data Counting and Evacuation)

  G1的前几个步骤的运作过程和 CMS有很多相似之处。初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS (Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可 用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Root 开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发 执行。而最终标记阶段则是为了修正在并发标记期间因用户程序继续运作而导致标记产生变 动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面, 最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停 顿线程,但是可并行执行。最后在筛选回收阶段首先对各个Region的回收价值和成本进行排 序,根据用户所期望的GC停顿时间来制定回收计划,从Sun公司透露出来的信息来看,这 个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用 户可控制的,而且停顿用户线程将大幅提高收集效率。
3.JVM-垃圾回收_第28张图片
官方调优文档地址:地址

5. 垃圾回收调优(GC调优)

  • 掌握GC相关的JVM参数,会基本的空间调整
  • 掌握相关工具
  • 重点:调优与应用和环境有关,没有统一的规则
    查看虚拟机运行参数
    “jdk下bin目录下java命令的绝对地址” -XX:+PrintFlagsFinal -version | findstr “GC”

“C:\Program Files\Java\jdk1.8.0_202\bin\java” -XX:+PrintFlagsFinal -version | findstr “GC”

uintx AdaptiveSizeMajorGCDecayTimeScale         = 10                                  {
     product}
    uintx AutoGCSelectPauseMillis                   = 5000                                {
     product}
     bool BindGCTaskThreadsToCPUs                   = false                               {
     product}
    uintx CMSFullGCsBeforeCompaction                = 0                                   {
     product}
    uintx ConcGCThreads                             = 0                                   {
     product}
     bool DisableExplicitGC                         = false                               {
     product}
     bool ExplicitGCInvokesConcurrent               = false                               {
     product}
     bool ExplicitGCInvokesConcurrentAndUnloadsClasses  = false                               {
     product}
    uintx G1MixedGCCountTarget                      = 8                                   {
     product}
    uintx GCDrainStackTargetSize                    = 64                                  {
     product}
    uintx GCHeapFreeLimit                           = 2                                   {
     product}
    uintx GCLockerEdenExpansionPercent              = 5                                   {
     product}
     bool GCLockerInvokesConcurrent                 = false                               {
     product}
    uintx GCLogFileSize                             = 8192                                {
     product}
    uintx GCPauseIntervalMillis                     = 0                                   {
     product}
    uintx GCTaskTimeStampEntries                    = 200                                 {
     product}
    uintx GCTimeLimit                               = 98                                  {
     product}
    uintx GCTimeRatio                               = 99                                  {
     product}
     bool HeapDumpAfterFullGC                       = false                               {
     manageable}
     bool HeapDumpBeforeFullGC                      = false                               {
     manageable}
    uintx HeapSizePerGCThread                       = 87241520                            {
     product}
    uintx MaxGCMinorPauseMillis                     = 4294967295                          {
     product}
    uintx MaxGCPauseMillis                          = 4294967295                          {
     product}
    uintx NumberOfGCLogFiles                        = 0                                   {
     product}
     intx ParGCArrayScanChunk                       = 50                                  {
     product}
    uintx ParGCDesiredObjsFromOverflowList          = 20                                  {
     product}
     bool ParGCTrimOverflow                         = true                                {
     product}
     bool ParGCUseLocalOverflow                     = false                               {
     product}
    uintx ParallelGCBufferWastePct                  = 10                                  {
     product}
    uintx ParallelGCThreads                         = 8                                   {
     product}
     bool ParallelGCVerbose                         = false                               {
     product}
     bool PrintClassHistogramAfterFullGC            = false                               {
     manageable}
     bool PrintClassHistogramBeforeFullGC           = false                               {
     manageable}
     bool PrintGC                                   = false                               {
     manageable}
     bool PrintGCApplicationConcurrentTime          = false                               {
     product}
     bool PrintGCApplicationStoppedTime             = false                               {
     product}
     bool PrintGCCause                              = true                                {
     product}
     bool PrintGCDateStamps                         = false                               {
     manageable}
     bool PrintGCDetails                            = false                               {
     manageable}
     bool PrintGCID                                 = false                               {
     manageable}
     bool PrintGCTaskTimeStamps                     = false                               {
     product}
     bool PrintGCTimeStamps                         = false                               {
     manageable}
     bool PrintHeapAtGC                             = false                               {
     product rw}
     bool PrintHeapAtGCExtended                     = false                               {
     product rw}
     bool PrintJNIGCStalls                          = false                               {
     product}
     bool PrintParallelOldGCPhaseTimes              = false                               {
     product}
     bool PrintReferenceGC                          = false                               {
     product}
     bool ScavengeBeforeFullGC                      = true                                {
     product}
     bool TraceDynamicGCThreads                     = false                               {
     product}
     bool TraceParallelOldGCTasks                   = false                               {
     product}
     bool UseAdaptiveGCBoundary                     = false                               {
     product}
     bool UseAdaptiveSizeDecayMajorGCCost           = true                                {
     product}
     bool UseAdaptiveSizePolicyWithSystemGC         = false                               {
     product}
     bool UseAutoGCSelectPolicy                     = false                               {
     product}
     bool UseConcMarkSweepGC                        = false                               {
     product}
     bool UseDynamicNumberOfGCThreads               = false                               {
     product}
     bool UseG1GC                                   = false                               {
     product}
     bool UseGCLogFileRotation                      = false                               {
     product}
     bool UseGCOverheadLimit                        = true                                {
     product}
     bool UseGCTaskAffinity                         = false                               {
     product}
     bool UseMaximumCompactionOnSystemGC            = true                                {
     product}
     bool UseParNewGC                               = false                               {
     product}
     bool UseParallelGC                            := true                                {
     product}
     bool UseParallelOldGC                          = true                                {
     product}
     bool UseSerialGC                               = false                               {
     product}
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b08, mixed mode)

5.1 调优领域

  • 内存
  • 锁竞争
  • cpu占用
  • io

5.2 调优目标

  • 低延迟还是高吞吐量,选择合适收集器(科学计算高吞吐量,互联网项目低延迟)
  • CMS G1 ZGC(超低延迟)
  • Parallel GC(高吞吐量)
  • Zing(其他的虚拟机)

5.3 最快的GC是不发生GC

查看FULL GC前后的内存占用,考虑下面几个问题

  1. 数据是不是太多
    • result = statement.Query(“select * from 大表 limit n”);
  2. 数据表示太臃肿
    • 对象图
    • 对象大小 Object 16字节 Integer 24字节 int 4字节
  3. 是否存在内存泄漏
    • static Map map 不断加入对象,强引用不得回收
    • 第三方缓存产品 redis等

5.4 新生代调优

新生代特点

  • 所有new操作分配的内存都是廉价的
    • TLAB thread local allocation buffer (让每个线程用自己私有的Eden内存来进行对对象进行分配)
  • 死亡对象回收代价是0
    • 垃圾回收将Eden和From中存活的对象复制到To中,剩下的垃圾对象直接全部删除,代价很小。
  • 大部分对象用过即死
  • Minor GC比Full GC 时间小很多。

新生代是否越大越好?

  • (1/4-1/2)堆内存的大小
  • 新生代过小,会频繁触发Minor GC
  • 新生代过大,老年代相应的变小,FULL GC 门槛变低
  • 在新生代变大的过程中,吞吐量首先是增长的,之后会下降。
  • 新生代对象大多数都是用过即死,存活的对象极少,在新生代内存增大后,新生代的复制算法也不会受太多影响。
  • 新生代能够容纳所有【并发量*(请求/响应)】的数据。
  • 如果一次请求产生512k的对象,同一时间有1000个并发用户,则要保证新生代要能够存储512M的对象。

幸存区大到能保留(当前活跃对象 + 需要晋升的对象)

  • 这样能保证用不着的垃圾对象下次就能被回收
  • 如果新存区不够存放,那么对象就会被转移到老年代,回收时间就会增加

晋升阈值配置得当,使长时间存活对象尽快晋升

  • -XX:MaxTenuringThreshold=threshold 配置阈值
  • -XX:+PrintTenuringDistribution 打印晋升细节

5.5 老年代调优

以cms为例

  • 老年代内存越大越好
  • 先尝试不要做调优,如果没有FULL GC,那么已经满足需求,否则先尝试新生代调优
  • 观察发生FULL GC时老年代的内存占用,将老年代的内存提高1/4-1/3
  • -XX:CMSInitialOccupancyFraction = percent 推荐设置为70-80(预留空间给 float garbage)

5.6 案例

  • 案例一:FULL GC和Minor GC频繁

  首先分析可能原因,先做新生代调优,新生代内存过小,对象放不下直接进入老年代,不仅Minor GC频繁,老年代中也会存入大量垃圾对象,FULL GC 也会频繁发生。

  所以,适当增大新生代内存大小,使得Eden可以存下新生的多个对象,不会过于频繁触发Minor GC,幸存区存下幸存对象能够使幸存对象在新存区中保存,不会过早进入老年代,老年代内存占用减轻。

  • 案例二:请求高峰期发生FULL GC,单次暂停时间过长(CMS)

查看GC日志看哪个时间段花费时间多(一般是重新标记的时间多)

  分析:请求高峰,并发用户很多,产生的新对象很多,堆中对象数目较大。CMS中重新标记会扫描整个堆内存来标记对象,所以会耗用大量时间。

  调优:-XX:+CMSScavengeBeforeRemark 设置在重新标记前对新生代进行一次垃圾清理,会大大减少重新标记的时间。

  • 案例三 老年代充裕情况下,发生FULL GC(CMS jdk1.7)

  在jdk1.7及以前,方法区以永久代方式实现,永久代内存不足时,也会发生FULL GC ,增加

  jdk1.8及以后,利用元空间实现,使用的是系统内存,内存充裕,很少会触发FULL GC。

你可能感兴趣的:(Java基础知识)