g1gc调优的一次实战记录

Posted 请叫我大师兄_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了g1gc调优的一次实战记录相关的知识,希望对你有一定的参考价值。

Java内存区域 或 JVM 内存结构


线程共享

  • 方法区
  • 直接内存
  • (非运行时数据区的一部分)
    线程私有
  • 程序计数器
  • 虚拟机栈
  • 本地方法栈
    JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK7 就已经开始了),取而代之是元空间

识别垃圾

  • 1,引用计数法(Reference Counting)
  • 2,可达性分析,又称引用链法(Tracing GC)

GC算法

  • 1,Mark-Sweep(标记-清除)
    产生碎片
  • 2,Copying(复制)
    空间利用率不高的缺点,另外就是存活对象比较大时复制的成本比较高
  • 3,Mark-Compact (标记-整理)
    好理论,解决1,2 gc算法的不足
  • X,分代收集算法
    正规的算法,就是1、2、3,这个分代算是灵活运用上面的三种。
    一般将堆分为新生代和老年代。
    新生代使用: 复制算法
    老年代使用: 标记 - 清除 或者 标记 - 整理 算法

G1GC

  • G1 GC是Java HotSpot虚拟机的低暂停服务器风格分代垃圾收集器,适用于具有大内存的多处理器计算机。G1 GC使用并发(concurrent)和并行(parallel)阶段来实现其目标暂停时间并保持良好的吞吐量。
  • 全堆操作(例如全局标记)与应用程序线程并行执行。这样可以防止与堆或活动数据大小成比例的中断。
  • G1是一个有整理内存过程的垃圾收集器,在回收垃圾的时候会压缩存活对象。不会产生很多内存碎片。
  • G1的Stop The World(STW)更可控,G1在停顿时间上添加了预测机制,用户可以指定期望停顿时间。

G1GC的设计目标

  • 与应用线程同时工作,几乎不需要stop-the-world(与CMS类似);
  • 整理剩余空间,不产生内存碎片;(CMS只能在full-GC时,用stop-the-world整理碎片内存)
  • GC停顿更加可控;
  • 不牺牲系统的吞吐量;
  • gc不要求额外的内存空间(CMS需要预留空间存储浮动垃圾);

什么是STW

不管选择哪种GC算法,stop-the-world都是不可避免的。Stop-the-world意味着从应用中停下来并进入到GC执行过程中去。一旦Stop-the-world发生,除了GC所需的线程外,其他线程都将停止工作,中断了的线程直到GC任务结束才继续它们的任务。GC调优通常就是为了改善stop-the-world的时间

G1是要替换掉CMS

CMS算是分代收集器,在JDK9 被标记弃用,JDK14 被删除,G1GC则是分区收集器,在某些方便弥补了CMS的不足,比如,CMS使用的是mark-sweep算法,会产生内存碎片;然而G1基于copying算法,高效的整理剩余内存,而不需要使用free-list去管理内存碎片。另外,G1提供了更多手段,以达到对gc停顿时间可控。

目前在 Hotspot VM 中主要有分代收集和分区收集两大类,具体可以看下面的这个图,不过未来会逐渐向分区收集发展。

并发(concurrent)和并行(parallel)

  • 在单CPU系统中,系统调度在某一时刻只能让一个线程运行,通过不断切换需要运行的线程让其运行的方式就叫并发(concurrent)
  • 在多CPU系统中,可以让两个以上的线程同时运行,这种可以同时让两个以上线程同时运行的方式叫做并行(parallel)
  • 并发:意味着应用程序同时(并发)在一项以上的任务上取得进展,尽力压榨一个CPU的处理能力,抢占更多的执行时间片
  • 并行:应用程序将其任务分解为较小的子任务,这些子任务可以并行处理,例如在多个CPU上同时进行。重点是多个CPU。

G1GC Region

  • 传统的GC收集器将连续的内存空间划分为新生代、老年代和永久代(JDK 8去除了永久代/PermGen,引入了元空间Metaspace),这种划分的特点是各代的存储地址(逻辑地址)是连续的。
  • G1 GC是一个区域化的代垃圾收集器
  • G1也是分代管理内存的,他的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址。
  • G1最大的特点就是高效的执行回收,优先去执行那些大量对象可回收的区域(region)
  • G1使用了停顿预测模型,来满足用户设定的gc停顿时间,根据用户设定的目标时间,g1会自动的选择哪些region要清除,一次清除多少个region。
  • G1从多个region中复制存活的对象,然后集中放入一个region中,同时整理、清除内存(copying收集算法)。

G1GC 停顿预测模型

  • 机器学习听说过吧,这个G1GC里面这个停顿预测模型,就类似这个机器学习,自行训练,自己判断每次回收谁,回收多少。
  • G1 uses a pause prediction model to meet a user-defined pause time target and selects the number of regions to collect based on the specified pause time target.
  • G1 GC是一个响应时间优先的GC收集器,它与CMS最大的不同是,用户可以设定整个GC过程的期望停顿时间,参数-XX:MaxGCPauseMillis指定一个G1收集过程目标停顿时间,默认值200ms,不过它不是硬性条件,只是期望值。那么G1怎么满足用户的期望呢?就需要这个停顿预测模型了。G1根据这个模型统计计算出来的历史数据来预测本次收集需要选择的Region数量,从而尽量满足用户设定的目标停顿时间。

G1GC内部三种形式的GC

Young GC (STW)

Eden区耗尽的时候就会触发新生代收集,新生代垃圾收集会对整个新生代(E + S)进行回收

  • 新生代垃圾收集期间,整个应用STW
  • 新生代垃圾收集是由多线程并发执行的
  • 通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • 新生代收集结束后依然存活的对象,会被疏散evacuation到n(n>=1)个新的Survivor分区,或者是老年代。

Mixed GC (STW)

  • Stop The World
  • 选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region
  • Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap

FULL GC (STW)

  • Stop The World
  • 如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。
  • G1是不提供full GC的。这个serial old GC full gc是单线程的(在Java 8中)并且非常慢,因此应避免在G1 gc的时候出现这个full gc
  • 不能觉得用了G1gc收集器之后,Java heap里面的gc不是young gc 就mixed gc,还有这个full gc呢
  • G1GC发生了Full GC,就说明当前程序的运行是问题的,就得考虑为什么会Full GC 了

G1GC 支持的配置参数以及默认值


es7的jvm参数配置

-Xms4g -Xmx4g -XX:+UseG1GC -XX:G1ReservePercent=25 -XX:InitiatingHeapOccupancyPercent=30
-XX:InitiatingHeapOccupancyPercent=45
设置触发全局并发标记周期的 Java 堆占用率阈值。默认占用率是整个 Java 堆的 45%。es7调小这个默认值,希望提前触发全局标记降低full gc的风险
-XX:G1ReservePercent=10
设置作为空闲空间的预留内存百分比,以降低目标空间溢出的风险。默认值是 10%。增加或减少百分比时,请确保对总的 Java 堆调整相同的量,es7调大这个值,扩大预留空间。

G1 调优建议

  • 不要显式的设置新生代的大小(用Xmn或-XX:NewRatio参数)
  • -XX:ConcGCThreads=n 设置并行标记的线程数。将n设置为并行垃圾回收线程数 (ParallelGCThreads) 的 1/4 左右。
  • -XX:ParallelGCThreads=n 设置 STW 工作线程数的值。将 n 的值设置为逻辑处理器的数量。如果逻辑处理器不止八个,则将n的值设置为逻辑处理器数的 5/8 左右。

G1的推荐使用场景

  • G1的首要重点是为运行需要大堆且GC延迟有限的应用程序的用户提供解决方案。这意味着堆大小约为6GB或更大,并且稳定且可预测的暂停时间低于0.5秒。
  • 如果当前具有CMS或ParallelOld垃圾收集器运行的应用程序具有以下一个或多个特征,则将其切换到G1将非常有益。
  • 超过50%的Java堆被实时数据占用。
  • 对象分配率或提升率差异很大。
  • 不必要的长时间垃圾收集或压缩暂停(长于0.5到1秒)

CMS 和 G1 对比


CMS不能解决Old区的碎片化问题,导致一定会出现长时间停顿的FullGC

G1与CMS相比的优势

  • 1、并行和并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
  • 2、分代收集:虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。它能够采用不同的方式去处理新创建的对象和已经存活了一段时间,熬过多次GC的旧对象以获取更好的收集效果。
  • 3、空间整合:与CMS的“标记–清理”算法不同**,G1从整体来看是基于“标记-整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
  • 4、可预测停顿:这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。

什么时候GC

  • 内存不够用了

对象何时进入老年代

  • 1,迭代年龄判断 (都知道)
    这个针对的是一个对象长时间存在引用关系,最终在young区来回copy,当年纪大了,就进去old区了。
  • 2,大对象直接进入老年代(不常见)
    超过单个region区域一半大小的对象,为避免在young区来回的copy,在new的时候直接给安排到old区。
  • 3,YoungGC之后需要移区的对象放不下(基本想不到)
    一般推荐使用G1GC,那都是堆比较大的程序,多半都是处理大量数据的程序,既然是大量数据,
    那么young区满的速度是贼快的,刷刷刷的就满了。若程序运行支持,前脚创建完对象,后脚立马就使用完了,
    那么YoungGC可以很快的回收young区的内存,Old区也不会出现大量的对象。
    这个时候Young区和Old区比例,应该是Young占比比较大才对。
    问题就是对象创建完之后,不是很快的结束引用,这个对象就要在Young区来回的copy,当s区不够的时候,
    他就会被安排到Old区,对象一旦被copy到old区之后,那就只能被Mixed GC或者Full GC来回收了,
    但是Mixed GC又不是完全的回收Old区的所有没有被引用的对象,他只是挑选一部分回收价值大的region去回收。
    问题的关键就在这,
    Mixed GC
  • 1,不是频繁的执行的,
  • 2,他还不是回收所有能回收的。当堆内存大量存在可被回收的对象的时候,就会出现浪费的现象。
    一直憋着,直到触发Full GC,STW,来一次大清理。

G1GC发生Full GC之后往哪思考

堆里的数据是什么

  • 写代码的人知道程序中什么数据的对象占比最大
  • 现在的问题就是对象积压而不是泄露。因为像内存泄露之类的,是会有OOM的异常的。
  • new 对象的速度是不是杠杠滴,而且特定时间段,数据量飙升且巨大的时候才频繁的Full GC。

为什么会一直存在引用

  • 1,数据写不出去,没办法释放引用
    比如我这程序是处理完数据之后要写es,写kafka,瞬间数据量就急剧飙升,但是写出的速度不变的。
    单纯的增加程序的运行内存,程序内部的缓存都是不行的。
    必须想办法加快数据的流出,比如增加es的data节点,或者es机器换固态硬盘,加机器。
    我这写es慢的解决是es的data节点换了固态硬盘。
    写kafka慢,是因为jvm参数设置的不对,添加了 -Xmn,限制了G1GC的黑科技运行。
  • 2,思考什么数据、什么情况下会进入到Old区
    因为一旦到了Old区,相比Young区的回收,Old区的回收,速度就不会很快,量也不会很大。
    我这就是设置了 -Xmn之后,限制了New size的大小,程序在数据量急剧飙升的时候,E区会极其频繁的new对象。
    但是还没来得及释放,就得被copy到O区了,一旦被copy到O区就发生了上述的问题。

G1GC实际调优例子

测试程序的jvm参数设置是 -Xmn4G -Xms4G -Xmn64M -XX:+UseG1GC

分析:

  • 这使用了G1GC的收集器,但是使用了-Xmn 设置了 NewSize的大小
  • 看这个进程的gc情况,每秒都进行10次上下的YGC,很频繁。
  • 看OU是在默默的一点点的增长着呢,预计一下:长着长着,就会塞不下,然后触发full gc了
    删除掉 -Xmn设置之后的heap情况

    分析:
  • 删掉 -Xmn的设置之后的堆内存分布
  • newSize最大可以到 2456MB
  • 4096(Heap) = 2576(E) + 4(S) + 1516(O)
  • 自动分配之后,Y区占比变大,占比 62%
  • 看YGC列,执行次数明显变少几秒钟一次YGCGCT也增长的很慢
  • 再观察OU,有增有减。说明有Mixed GC在执行
  • SEO各区的大小是浮动的
  • 为啥 S0C 和 S0U一直是0?
    结论
    用这个例子,实际验证了 G1GC的调优建议中的不要设置New size的大小,因为这个会限制G1GC内部的停顿预测模型的工作,他牛就牛在这个模型上。

对比一下CMS GC


S0 和S1是交替在使用,在YGC的时候,有个互相copy的过程

以上是关于g1gc调优的一次实战记录的主要内容,如果未能解决你的问题,请参考以下文章

一次性能调优的实战

redis调优的实战经验

JVM 性能调优实战之:一次系统性能瓶颈的寻找过程

Nginx大并发优化实战

Android性能优化:Github下载超10万次的360°全方面性能调优指南(含内存优化布局优化实战解析等)

Android性能优化——360°全方面性能调优指南(含内存优化布局优化实战解析等)