优化 full gc 的四个方向【草稿】

有句老话说得好:道可道,非常道。很多人认为优化 JVM 无非就是调整 Xmx 和 Xms 参数,但在实际情况中,这句话的确没毛病。

如果是 2B 项目或逻辑简单、业务轻量并发不高的应用程序,调整 Xmx 和 Xms 参数确实可以解决大部分问题。

但是对于架构复杂,高并发,大量 2C 接口的应用程序来说,调整 Xmx 和 Xms 参数只是优化 JVM 的基础,并不足以解决所有问题。在这种情况下,针对应用程序的特点,对 JVM 参数进行针对性的优化,可以极大地提高应用程序的性能。

本文以优化 Full GC 为例,将其分为四个方向:三个 JVM,一个 code。

在进行 JVM 层面优化时,我们需要关注以下三个方向:调整堆容量大小、调整垃圾收集器、优化元空间大小和关注安全点。

JVM 中有多个垃圾收集器可供选择,每个收集器都有自己的优势和劣势。根据应用程序内存使用情况、操作系统、应用程序类型等综合因素选择合适的垃圾收集器非常重要。

优化元空间大小也是减少 Full GC 的重要方向之一。通过调整元空间的大小,我们可以有效地降低 Full GC 的频率和时长。

最后,关注安全点也是优化 JVM 的一项关键任务。通过调整安全点的设置,可以将 Full GC 的时间和频率降至最低。

除了 JVM 层面的优化,还有一种优化方式是在代码层面进行优化。例如,尽量避免创建大量临时对象、避免频繁的对象创建和销毁等。

总之,优化 JVM 并不是简单的调整 Xmx 和 Xms 参数,而是需要综合考虑多个因素来提高应用程序的性能。如果您的应用程序充满挑战,我建议您通过本文所述的四个方向进行 JVM 优化,并借此机会在简历中留下印记。

jvm 层面

避免动态回收内存

Xmx 和 Xms 值设置一样,避免 jvm 动态回收内存引起 gc。

选择合适的垃圾收集器

截屏2023-06-25 19.55.30.png 垃圾收集器种类不少,我根据运行方式将其分为三大类,串行,并行和并发。根据你的应用特点和服务器配置选择合适的垃圾收集器,如果还不清楚,那就看看小明怎么选吧。

老咸是个计算机系学生,因为兴趣爱好开发了一款单机游戏。起初游戏剧情简单,容量只有百来兆,运行在古董单核电脑上。平时老咸不仅在这台电脑上打游戏,偶尔还有学习需求,360收集器刚好满足需求。

随着老咸技术进步,单机游戏升级成联机游戏,极大的增大了游戏乐趣的同时,也给游戏本身带来很大的副作用,其中最明显的是 stw 变长了,游戏多次在老咸即将五杀之时卡住,不用想,一定是 gc 在跑。作为垃圾佬的老咸不得不从远在大洋彼岸的表哥淘一枚双核cpu,伴随着咔嚓一声,这款游戏也终于吃上双核 cpu 的红利,那一年,老咸刚满20岁。

随着 jdk9 诞生,老咸也大学毕业了。而老咸已不是曾经的少年,在那满是头发的桌子上,老咸的中指缓缓落到 enter 键,伴随一声叮咚,从老咸囧黑的瞳孔看到,jdk9 升级成功。终于,老咸成为宿舍里第一个吃上并行垃圾收集器的男人。老咸脑海里甚至浮现出 gc 线程和业务线程并行工作的场景,温盈的泪水挂满 java boy 执拗的脸盘。

cms 虽好,可不要贪杯哦。内存碎片,浮动垃圾
g1 新一代垃圾收集器,结合 cms 的优点,以 region 为单位切分堆,使得 gc 的粒度不再是整个年轻代或整个堆,极大提高 gc 的效率,适合大内存

调整 matespace 大小

元空间(Metaspace)是存储类元数据(Class Metadata)的区域,包括运行时常量池、静态变量、类信息等等。当类加载器从元空间加载或卸载类时,元空间的类信息就会增加或减少。随着加载的类数量不断增加,元空间里的类信息也会变得越来越多,那么当元空间满时会发生什么?

会发生 full gc。当元空间使用量达到 MetaspaceSize,jvm 需要腾出元空间,导致类加载和卸载变得更加频繁,从而导致 full gc。这种 full gc 会占用指标,幸好他是可以避免的,只要我们控制好 MetaspaceSize 就好,一般是设置成 256m。

说完理论,java boy 不信服怎么搞?还能怎么搞,撸起袖子搞

/**
 * -Xms4096m
 * -Xmx4096m
 * -XX:MetaspaceSize=205m
 * -XX:MaxMetaspaceSize=205m
 */
while (true) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(StarterApplication.class);
    enhancer.setUseCache(false);
    enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
    enhancer.create();
}

截屏2023-06-25 19.55.30.png

oc 老年代容量,ou 老年代使用量,mc 元空间容量,mu 元空间使用量,ygc 和 fgc 望文生义

打印有点乱,各位看官老爷将就将就。老爷们把目光锁定到红色方框,横红框和竖红框重合区域。从中我们可以分析道,伴随着 ygc 增长到38,元空间增长到209920k,full gc 终于开始干活了,但是我们注意到,老年代才100366k,要知道启动参数老年代有 4g,老年代的使用远没有达到 full gc 的条件。聪明的 boy 早已看穿一切,脱口而出担保机制,老咸一眼看常你的心思,随手丢了一篇,消息在人声鼎沸中

从中可以得出结论,除了老年代不足和 System.gc 以外,元空间不足也可以触发 full gc。还记得我们甚至启动参数 mateSpaceSize=205m 吗,再看看输出日志,mu=200091.9k#≈195m,而195≈205,这难道是巧合吗,不,这是天意。

code 层面

啰嗦完 jvm 层面,我们看看 code 层面。 code 层面目前有安全点和 tlab 可吹,但 tlab 我还不没搞清楚,这里我先插个眼,研究清楚我再传送

安全点

简单来说,安全点是保证业务线程 stw 时机

gc 时需要弄清楚两件事,一是业务线程 stw,二是业务线程 stw 时机。为了保证用户应用性能,业务线程几乎 full time 在执行业务逻辑,但 gc 时没人通知业务线程,所以需要业务线程自己问 gc,大爷,您要干活了吗?但这又带来个问题,什么时机,什么频率询问?

jvm 在特定的位置给字节码插入一个指令(比喻),当业务线程执行到该指令,会停下来询问是否要 gc,这就是安全点。另外一提,当所有业务线程到达安全点,gc 才会开始。

知道了安全点,那安全点有什么可优化的地方呢?

源于 why大神的一篇博文,一个 rocketMq 的 bug,现象是很长时间的 full gc。代码是一个循环执行业务逻辑,耗时操作,且里面有个关键字 prevent gc,大神深深沉迷于此,一步步庖丁解牛,最后发现 bug 跟 prevent gc 没关系,反而是安全点的问题 image.png
你有没有想过,我们使用循环的时候,如果循环耗时,且循环次数很大,gc 启动的时候,循环要不要停下来?回顾上面说的安全点,循环应该要停下来,所以 jvm 就得给循环插入安全点,jvm 确实插进去了,但。
凡是有个但,有两个专有名词,不可数循环和可数循环,jvm 只会在不可数循环中插入安全点,而声明不可数循环的做法是把 i 定义成 long,反而把 i 定义 int 不会插入安全点。
大概是因为,jvm 认为用户在不怎么耗时,循环次数不多的循环定义 int,在耗时,循环次数很多的循环定义 long,就是默认用户本来就会(胆子挺大)。

总结 rocketmMq 的 bug 就是它的循环耗时较长,i 定义为可数循环。gc 线程启动时,业务线程处于循环中,且没有安全点,gc 线程只能等待,而其他线程已经进入安全点,并处于 stw,所以整个应用只有循环的线程在执行,这影响可大了,轻则服务挂机,重则服务雪崩。

那是不是我们在所有的循环都用 long?要知道,安全点的插入使得业务现场轮询 gc,也是一种消耗,无脑 long 不可取,平常编码的时候,要根据具体业务场景定义

tlab分配

我们一般认为 Java 中 new 的对象都是在堆上分配,这个说法不够准确,应该是大部分对象在堆上的 TLAB 分配,还有一部分在栈上分配或者是堆上直接分配,可能 Eden 区也可能年老代。同时,对于一些的 GC 算法,还可能直接在老年代上面分配,例如 G1 中的 humongous allocations(大对象分配),就是对象在超过 Region 一半大小的时候,直接在老年代的连续空间分配。
这里,我们先只关心 TLAB 分配。

全部评论

相关推荐

头像
04-29 10:53
已编辑
东北大学 自动化类
点赞 评论 收藏
转发
点赞 收藏 评论
分享
牛客网
牛客企业服务