天天看点

G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结

目录

1、畅想GC的目标

2、jvm调优的目标

3、GC调优时机

4、垃圾收集器的选择

5、G1调优策略

6、G1垃圾收集实践

6.1、JVM自动选择垃圾收集器

6.2、G1垃圾收集

6.3、GC日志分析

7、小结

前言         c++和java之间有一堵由内存动态分配和垃圾收集技术所围成的墙,墙外面的人想进去墙里面的人想出来。

1、畅想GC的目标

    詹姆斯· 高斯林  (James Gosling)是一名软件专家,喊出了口号:“一次编写,到处乱跑。”     他在1995年写java这门编程语言的时候,可能并没有想到java会如此广泛的应用于web开发,没有意识到要进行更多的web交互场景,应用对停顿时间要求是如此的严格,否则在刚开始设计垃圾回收的时候就不会粗暴的直接将应用线程停掉了。这个在现在来看是不太能接受的,随着jdk往上发展,web的高并发,交互场景的越来越频繁, 所以要追求低停顿和高吞吐量成了程序员们的追求,所以垃圾收集器就需要与时俱进的进行不断的优化,再优化,直到没有停顿。 以至于出现了Z-GC,Z也不知道是不是zero的意思,代表着程序员们的极致追求,没有停顿时间。

The Z Garbage Collector (ZGC) is a scalable low latency garbage collector. 
ZGC performs all expensive work concurrently, without stopping the execution of application threads for more than 10ms, 
which makes is suitable for applications which require low latency and/or use a very large heap (multi-terabytes).
The Z Garbage Collector is available as an experimental feature, 
and is enabled with the command-line options
 -XX:+UnlockExperimentalVMOptions -XX:+UseZGC.
           

ZGC的官网描述:Z-GC目标:能够让应用gc停顿的时间低于:10ms,适用于更大堆。

参考资料:Z-Gabage: https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0  

2、jvm调优的目标

随着互联网的web应用流量激增,堆内存空间的不断增大,从官方垃圾收集器的一步步优化之路不难发现,程序员对JVM 的垃圾收集追求的目标在于以下三点:

  • 吞吐量- Throughput;运行用户代码时间/(运行用户代码时间+垃圾收集时间)
  • 停顿时间-P auseTime;垃圾收集器进行 垃圾回收中断应用执行响应的时间
  • GC的频率-GCTimes;一般不做硬性要求,能接受一定程度的younggc,但一定要避免full-gc;

停顿时间越短就越适合需要和用户交互的程序,良好的响应速度能提升用户体验; 高吞吐量则可以高效地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 吞吐量和停顿时间也是评价一个垃圾收集器优劣的指标。 可能JVM对垃圾收集器追求的终极目标是:没有停顿时间且拥有高吞吐量。 遗憾的是,目前还没有这样一款垃圾收集器问世,当然,也不是所有的应用都是追求停顿时间,可能有的不在乎时间而在乎吞吐量,所以我们目前所能做到的就是不断调优,根据业务场景找到适应各自的项目需求的垃圾收集器,让上面的三个主要的目标达到最优。

3、GC调优时机

什么时候才需要调优?GC到底影响什么?GC的常见症状? 首先一定不是无聊,天马行空的改参数,那样反而适得其反。 例如 GC停顿导致 常见的问题的症状 :

  • 系统CPU飙升很快;
    • 系统运行的响应时间长,接口响应超时;
    • 网站经常不定期出现:长时间没有响应的现象。
    • gc次数太多,用户线程代码执行受影响,cpu使用会高?
  • 内存的使用率逐渐增大,不够用了;

4、垃圾收集器的选择

jvm调优:如何调优才能实现我们的目标 呢,首先是垃圾收集器的选择。 首先要明确,jvm的调优没有万能公式,每个项目背景和要求不同,调优的策略和参数都不一样。 首先关于垃圾收集器的选择:并不是并发度越高就越好的,停顿时间越短就越好。需要根据具体的情况来看。 官网垃圾收集器的选择标准: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/collectors.html#sthref28

Selecting a Collector
首先让jvm来自动选择,不能满足;
调整堆的大小,减少垃圾回收次数;
Unless your application has rather strict pause time requirements, first run your application and allow the VM to select a collector. If necessary, adjust the heap size to improve performance. 
如果仍然不能满足:
If the performance still does not meet your goals, then use the following guidelines as a starting point for selecting a collector.

内存小于100m,可以选择serial收集器;
* If the application has a small data set (up to approximately 100 MB), then
select the serial collector with the option 
-XX:+UseSerialGC.

单线程,使用serial;
* If the application will be run on a single processor and there are no pause time requirements, then let the VM select the collector, or select the serial collector with the option
 -XX:+UseSerialGC.

没有停顿时间的要求,关注吞吐量,选择并行收集器;
* If (a) peak application performance is the first priority and (b) there are no pause time requirements or pauses of 1 second or longer are acceptable, then let the VM select the collector, or select the parallel collector with
 -XX:+UseParallelGC.

关注停顿时间的要求,可以选择G1;
* If response time is more important than overall throughput and garbage collection pauses must be kept shorter than approximately 1 second, then select the concurrent collector with -XX:+UseConcMarkSweepGC or
  -XX:+UseG1GC.
           

根据官网推荐,垃圾收集器的选择标准总结如下:

  1. 优先调整堆的大小让服务器jvm自己来选择一个合适的垃圾收集器;
  2. 如果内存小于100M,使用串行收集器;
  3. 如果是单核,并且没有停顿时间要求,使用串行或JVM自己选
  4. 如果允许停顿时间超过1秒,选择并行CMS或JVM自己选;
  5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器G1;

从结论可以看出:选择一个合适的垃圾收集需要根据系统的要求:

  • 比如cpu的核心数:如果是单核cpu,选择并发垃圾收集器也没有用,因为单核还是串行的,线程的切换反而降低了垃圾收集的效率;
  • 堆的大小:类似G1垃圾收集,它的内存布局让它更适合大堆内存的收集,而小堆内存串行和CMS就能有比较高的性能;
  • 是否关注停顿时间:如果不关注停顿时间关注吞吐量,串行和CMS就能提供很好的性能,也并不是G1就是最好的;

5、G1调优策略

官方也给出了G1调优的一些建议指南: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations

  • (1)不要手动设置新生代和老年代的大小,只要设置整个堆的大小
    • G1收集器在运行过程中,会自己调整新生代和老年代的大小 其实是通过自动调整young代的大小 来调整对象晋升的速度,从而达到为收集器设置的暂停时间目标,如果手动设置了大小就意味着放弃了G1的自动调优, 破坏了停顿时间策略;
  • (2) 不断调优暂停时间;不要太严格
    • 一般情况下这个值设置到100ms或者200ms都是可以的(不同情况下会不一样),但如果设置成50ms就不太合理。
    • 暂停时间设置的太短,就会导致出现G1跟不上垃圾产生的速度。最终退化成Full GC。
    • 所以对这个参数的调优是一个持续 的过程,逐步调整到最佳状态。暂停时间只是一个目标,并不能总是得到满足。
  • (3)使用-XX:ConcGCThreads=n来增加标记线程的数量
    • IHOP如果阀值设置过高,可能会遇到转移失败的风险,比如对象进行转移时空间不足。如果阀值设置过低,就会使标 记周期运行过于频繁,并且有可能混合收集期回收不到空间。 IHOP值如果设置合理,但是在并发周期时间过长时,可以尝试增加并发线程数,调高ConcGCThreads。
  • (4)MixedGC调优
    • -XX: InitiatingHeapOccupancyPercent=45 :触发并发标记的堆内存使用占比;
    • -XX:G1MixedGCLiveThresholdPercent
    • -XX:G1MixedGCCountTarger
    • -XX:G1OldCSetRegionThresholdPercent
  • (5)条件允许的情况下,适当增加堆内存大小

6、G1垃圾收集实践

    一般如果发现gc频繁,或者gc停顿时间长不可接受,我们就需要对gc的参数进行调整,然后通过日志,调整参数,达到一个GC停顿时间和吞吐量的最佳的状态,我们将用以下代码来模拟查看堆区的gc日志来进一步了解jvm各垃圾收集的工作过程,由于CMS之前分析过,这里不在赘述,主要分析下G1收集器。

6.1、JVM自动选择垃圾收集器

    首先我们自己不设置垃圾收集器,让JVM自己来为我们选择,因为官方推荐这么做,当不知道如何选择的时候可以把这个权限交给JVM,JVM会默认的选择一个垃圾回收器,用下面一小段代码来不断产生垃圾,看垃圾收集器的作用及日志。

public class HeapOomGCTest {

    public static String heapOOMtest() throws InterruptedException {
        List<Person> list = new ArrayList<Person>();
        while (true) {
            list.add(new Person());
            System.out.println("add Person success~");
            Thread.sleep(10);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        heapOOMtest();
    }
}
           

设置参数如下:

-Xms30m -Xmx30m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -Xloggc:JvmAutoSelectGC.log
           
G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结

JvmAutoSelectGC.log日志解读:

Java HotSpot(TM) 64-Bit Server VM (25.201-b09) for bsd-amd64 JRE (1.8.0_201-b09), built on Dec 15 2018 18:35:23 by "java_re" with gcc 4.2.1 (Based on Apple Inc. build 5658) (LLVM build 2336.11.00)
Memory: 4k page, physical 16777216k(651656k free)

/proc/meminfo:

CommandLine flags: -XX:InitialHeapSize=31457280 -XX:MaxHeapSize=31457280 -XX:+PrintGC -XX:+PrintGCDateStamps -XX:+PrintGCDetails 
-XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC

2020-11-22T09:05:50.054-0800: 10.188: [GC (Allocation Failure) [PSYoungGen: 8192K->1008K(9216K)] 8192K->1276K(29696K),
 0.0019565 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
           

日志具体内容分析:

-XX:+UseParallelGC :通过日志发现在没有设置GC垃圾收集器的情况下, JDK1.8默认使用的垃圾收集器:ParallerGC = ParallerScavge +  ParallerOld GC:表明进行了一次垃圾回收,前面没有Full修饰,PSYoungGen表明这是一次新生代的Minor GC,这里不管是新生代还是老年代都会STW。 Allocation Failure:表明本次引起GC的原因是因为在年轻代中没有足够的空间能够存储新的数据了。 8192K->1008K(9216K):(单位是KB)三个参数分别为:GC前该内存区域(这里是年轻代)使用容量,GC后该内存区域使用容量,该内存区域总容量。 因为我设置的总堆大小为30M=30720kb,-XX:NewRatio=2,出去其他的内存占用,所以新生代Eden区的总容量为:9216kb 0.0019565 secs:该内存区域GC耗时,单位是秒 8192K->1276K(29696K):三个参数分别为:堆区垃圾回收前的大小,堆区垃圾回收后的大小,堆区总大小。 [Times: user=0.00 sys=0.01, real=0.00 secs]:分别表示用户态耗时,内核态耗时和总耗时

6.2、G1垃圾收集

参数设置如下:

-Xms500m -Xmx500m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+UseG1GC -XX:MaxGCPauseMillis=15 -Xloggc:G1-gc.log
           
G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结
CommandLine flags: -XX:CMSInitiatingOccupancyFraction=30 -XX:InitialHeapSize=5242880 -XX:MaxHeapSize=5242880 -XX:+PrintGC 
-XX:+PrintGCDateStamps -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseG1GC #使用 G1垃圾收集;
# 什么时候发生的GC,相对的时间刻,GC发生的区域young,总共花费的时间,0.00478s,
2020-11-22T13:45:42.218-0800: 0.196: [GC pause (G1 Evacuation Pause) (young), 0.0018535 secs]
    # 表示8个垃圾回收线程,并行的时间
   [Parallel Time: 1.2 ms, GC Workers: 8]
    # GC线程开始相对于上面的0.196的时间刻
      [GC Worker Start (ms): Min: 122.3, Avg: 122.5, Max: 123.0, Diff: 0.7]
      [Ext Root Scanning (ms): Min: 0.0, Avg: 0.2, Max: 0.9, Diff: 0.9, Sum: 1.9]
      [Update RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
         [Processed Buffers: Min: 0, Avg: 0.0, Max: 0, Diff: 0, Sum: 0]
      [Scan RS (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Code Root Scanning (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.0]
      [Object Copy (ms): Min: 0.0, Avg: 0.2, Max: 0.4, Diff: 0.4, Sum: 1.8]
      [Termination (ms): Min: 0.0, Avg: 0.4, Max: 0.5, Diff: 0.5, Sum: 3.0]
         [Termination Attempts: Min: 1, Avg: 1.8, Max: 4, Diff: 3, Sum: 14]
      [GC Worker Other (ms): Min: 0.0, Avg: 0.0, Max: 0.0, Diff: 0.0, Sum: 0.1]
      [GC Worker Total (ms): Min: 0.3, Avg: 0.8, Max: 1.1, Diff: 0.8, Sum: 6.8]
      [GC Worker End (ms): Min: 123.3, Avg: 123.4, Max: 123.5, Diff: 0.1]
   [Code Root Fixup: 0.0 ms]
   [Code Root Purge: 0.0 ms]
   [Clear CT: 0.1 ms]
   [Other: 0.5 ms]
      [Choose CSet: 0.0 ms]
      [Ref Proc: 0.3 ms]
      [Ref Enq: 0.0 ms]
      [Redirty Cards: 0.2 ms]
      [Humongous Register: 0.0 ms]
      [Humongous Reclaim: 0.0 ms]
      [Free CSet: 0.0 ms]
   [Eden: 1024.0K(1024.0K)->0.0B(1024.0K) Survivors: 0.0B->1024.0K Heap: 1024.0K(6144.0K)->536.1K(6144.0K)]
 [Times: user=0.01 sys=0.00, real=0.00 secs]
           

G1-GC.log部分日志截取如下:

从上面的日志来看,其日志格式复杂了很多,可以参考G1日志详细解读:https://blogs.oracle.com/poonam/understanding-g1-gc-logs

6.3、GC日志分析

6.3.1、本地工具查看:gc-viewer

G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结

使用命令运行:

java -jar gcviewer-1.36-SNAPSHOT.jar  
           

运行这个工具jar包,打开我们刚刚生成的G1-GC.log文件,我们就可以看见jvm调优的几个核心指标,其实从工具我们也可以清楚的看到jvm的gc调优关注的是什么,调的是什么,停顿时间-吞吐量-GC频率。

G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结
G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结

  可以发现,通过此工具清楚的看到 g1发生gc的详情, 停顿耗时,gc次数,吞吐量都一目了然,之前我们的设置参数如下,设置的比较小,这样效果比较明显。

-Xms5m -Xmx5m -XX:+PrintGCDetails -XX:CMSInitiatingOccupancyFraction=30   -XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:MaxGCPauseMillis=5 -XX:+UseG1GC  -Xloggc:G1-GC.log
           

我们发现其核心指标如下:

吞吐量 最小停顿时间 平均停顿时间 最大停顿时间 gc次数
97.25 0.00048 0.00254 0.00671 13

从这几个参数我们就可以看出停顿时间,吞吐量和gc的次数是否符合我们的要求,如果不符合我们可以继续调整参数。 调整方案:

  1. 如果gc频次高,我们可以适当的增加堆内存,这样可能会增加gc的停顿时间;
  2. 然后我们可以适当的控制减小gc的停顿时间,但不要太严格;如果频次还是高;
  3. 可以修改触发G1垃圾回收的阈值 -XX:InitialHeapOccupacyPersent,默认是45%,可以适当的提高;

最终达到频次,停顿时间和吞吐量的一个最优值;   6.3.2、在线工具:GCeasy工具 这个工具也可以看到gc的详细过程及日志情况,还可以比较不同的垃圾收集器的吞吐量和停顿时间,非常方便,但是在线的工具需要注意安全性。 官网地址 : https://gceasy.io 打开我们刚刚生成的G1-gc.log日志,从官网的显示也可以看出gc调优的三个重要指标: 吞吐量 + 停顿时间 + gc频次

G1调优分析1、畅想GC的目标2、jvm调优的目标3、GC调优时机4、垃圾收集器的选择5、G1调优策略6、G1垃圾收集实践7、小结

从结果可以清晰的看到jvm调优的三个核心指标的数据,不同工具好像统计出的有一定的差别,所以我们可以通过这些工具来帮助我们更好的分析gc日志。  

7、小结

  GC的垃圾收集器的参数和选择都不唯一,需要根据项目的场景及硬件条件作出选择,适合的就是最好的,没有银弹。

  • gc调优核心指标
    • 吞吐量 — throughtput
    • 停顿时间 —pause time
    • gc频率 — gc times
  • GC日志查看工具:
    • GCeasy:在线工具;
    • GCviewer:本地工具;java -jar gc-viewer.jar

    OK---望着大河弯弯,终于敢放胆,嬉皮笑脸面对,人生的难。        水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。     参考资料: 《深入理解jvm虚拟机》 Z-Gabage: https://docs.oracle.com/en/java/javase/11/gctuning/z-garbage-collector1.html#GUID-A5A42691-095E-47BA-B6DC-FB4E5FAA43D0 G1日志详细解读: https://blogs.oracle.com/poonam/understanding-g1-gc-logs G1调优的一些建议指南: https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/g1_gc_tuning.html#recommendations