关于java:运维你们-JAVA-服务内存占用太高还只增不减告警了快来接锅

6次阅读

共计 3878 个字符,预计需要花费 10 分钟才能阅读完成。

先点赞再看,养成好习惯

某天,运维老哥忽然找我:“你们的某 JAVA 服务内存占用太高,告警了!GC 后也没开释,内存只增不减,是不是内存透露了!”

而后我连忙看了下监控,一切正常,间隔上次发版好几天了,FULL GC 一次没有,YoungGC,十分钟一次,堆闲暇也很短缺。

运维:“你们这个服务当初堆内存 used 才 800M,但这个 JAVA 过程曾经占了 6G 内存了,是不是你们程序出啥内存泄露的 bug 了!”

我想都没想,间接回了一句:“不可能,咱们服务十分稳固,不会有这种问题!”

不过说完之后,心田还是自我质疑了一下:会不会真有什么 bug?难道是堆外泄露?线程没销毁?导致内存泄露了???

而后我很“镇定”的补了一句:“我先上服务器看看啥状况”,被打脸可就不好了,还是不要装太满的好……

迅速上登上服务器又认真的查看了各种指标,Heap/GC/Thread/Process 之类的,发现一切正常,并没有什么“透露”的迹象。

和运维的“沟通”

咱们这个服务很失常啊,各个指标都 ok,什么内存只增不减,在哪呢

运维:你看你们这个 JAVA 服务,堆当初 used 才 400MB,但这个过程当初内存占用都 6G 了,还说没问题?必定是内存泄露了,锅接好,连忙回去查问题吧

而后我指着监控信息,让运维看:“大哥你看这监控历史,堆内存是达到过 6G 的,只是前面 GC 了,没问题啊!”

运维:“回收了你这内存也没开释啊,你看这个过程 Res 还是 6G,必定有问题啊”

我心想这运维怕不是个 der,JVM GC 回收和过程内存又不是一回事,不过还是和得他解释一下,不然始终 baba 个没完

“JVM 的垃圾回收,只是一个 逻辑上 的回收,回收的只是 JVM 申请的那一块逻辑堆区域,将数据标记为闲暇之类的操作,不是调用 free 将内存归还给操作系统”

运维顿了两秒后,忽然脸色一转,开始笑起来:“咳咳,我可能没留神这个。你再给我讲讲 JVM 的这个内存治理 / 回收和过程上内存的关系呗”

尽管我心田是回绝的,但得罪谁也不能得罪运维啊,想想还是给大哥解释解释,“增进下感情”

操作系统 与 JVM 的内存调配

JVM 的主动内存治理,其实只是先向操作系统申请了一大块内存,而后本人在这块已申请的内存区域中进行“主动内存治理”。JAVA 中的对象在创立前,会先从这块申请的一大块内存中划分出一部分来给这个对象应用,在 GC 时也只是这个对象所处的内存区域数据清空,标记为闲暇而已

运维:“原来是这样,那按你的意思,JVM 就不会将 GC 回收后的闲暇内存还给操作系统了吗?”

为什么不把内存归还给操作系统?

JVM 还是会偿还内存给操作系统的,只是因为这个代价比拟大,所以不会轻易进行。而且不同垃圾回收器 的内存调配算法不同,偿还内存的代价也不同。

比方在革除算法(sweep)中,是通过闲暇链表(free-list)算法来分配内存的。简略的说就是将已申请的大块内存区域分为 N 个小区域,将这些区域同链表的构造组织起来,就像这样:

每个 data 区域能够包容 N 个对象,那么当一次 GC 后,某些对象会被回收,可是此时这个 data 区域中还有其余存活的对象,如果想将整个 data 区域开释那是必定不行的。

所以这个偿还内存给操作系统的操作并没有那么简略,执行起来代价过高,JVM 天然不会在每次 GC 后都进行内存的偿还。

怎么偿还?

尽管代价高,但 JVM 还是提供了这个偿还内存的性能。JVM 提供了 -XX:MinHeapFreeRatio-XX:MaxHeapFreeRatio 两个参数,用于配置这个偿还策略。

  • MinHeapFreeRatio 代表当闲暇区域大小降落到该值时,会进行扩容,扩容的下限为 Xmx
  • MaxHeapFreeRatio 代表当闲暇区域超过该值时,会进行“缩容”,缩容的上限为Xms

不过尽管有这个偿还的性能,不过因为这个代价比拟低廉,所以 JVM 在偿还的时候,是线性递增偿还的,并不是一次全副偿还。

然而然而然而,通过实测,这个偿还内存的机制,在不同的垃圾回收器,甚至不同的 JDK 版本中还不一样!

不同版本 & 垃圾回收器下的体现不同

上面是我之前跑过的测试后果:

public static void main(String[] args) throws IOException, InterruptedException {List<Object> dataList = new ArrayList<>();
    for (int i = 0; i < 25; i++) {byte[] data = createData(1024 * 1024 * 40);// 40 MB
        dataList.add(data);
    }
    Thread.sleep(10000);
    dataList = null; // 待会 GC 间接回收
    for (int i = 0; i < 100; i++) {
        // 测试屡次 GC
        System.gc();
        Thread.sleep(1000);
    }
    System.in.read();}
public static byte[] createData(int size){byte[] data = new byte[size];
    for (int i = 0; i < size; i++) {data[i] = Byte.MAX_VALUE;
    }
    return data;
}
JAVA 版本 垃圾回收器 VM Options 是否能够“偿还”
JAVA 8 UseParallelGC(ParallerGC + ParallerOld) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40
JAVA 8 CMS+ParNew -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
JAVA 8 UseG1GC(G1) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseG1GC
JAVA 11 UseG1GC(G1) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40
JAVA 16 UseZGC(ZGC) -Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseZGC

测试后果刷新了我的认知。,MaxHeapFreeRatio 这个参数如同并没有什么用,无论我是配置 40,还是配置 90,回收的比例都有和理论的后果都有很大差距。

然而文档中,可不是这么说的……

而且 ZGC 的后果也是挺意外的,JEP 351 提到了 ZGC 会将未应用的内存开释,但测试后果里并没有。

除了以上测试后果,stackoverflow 上还有一些其余的说法,我就没有再一一测试了

  1. JAVA 9 后 -XX:-ShrinkHeapInSteps 参数,能够让 JVM 已非线性递增的形式偿还内存
  2. JAVA 12 后的 G1,再利用闲暇时,能够主动的偿还内存

所以,官网文档的说法,也只能当作一个参考,JVM 并没有过多的走漏这个实现细节。

不过这个是否偿还的机制,除了这位“激情”的运维老哥,个别人也不太会去关怀,巴不得 JVM 多用点内存,少 GC 几回……

而且别说闲暇主动偿还了,咱们心愿的是一启动就调配个最大内存,防止它运行中扩容影响服务;所以个别 JAVA 程序还会将 XmsXmx 配置为相等的大小,防止这个扩容的操作。

听到这里,运维老哥若有所思的说到:“那是不是只有我把 Xms 和 Xmx 配置成一样的大小,这个 JAVA 过程一启动就会占用这个大小的内存呢?”

我接着答到:“不会的,哪怕你 Xms6G,启动也只会占用理论写入的内存,大概率达不到 6G,这里还波及一个操作系统内存调配的小常识”

Xms6G,为什么启动之后 used 才 200M?

过程在申请内存时,并不是间接调配物理内存的,而是调配一块虚拟空间,到真正堆这块虚拟空间写入数据时才会通过缺页异样(Page Fault)解决机制调配物理内存,也就是咱们看到的过程 Res 指标。

能够简略的认为操作系统的内存调配是“惰性”的,调配并不会产生理论的占用,有数据写入时才会产生内存占用,影响 Res。

所以,哪怕配置了Xms6G,启动后也不会间接占用 6G 内存,只是 JVM 在启动后会malloc 6G 而已,但理论占用的内存取决于你有没有往这 6G 内存区域中写数据的。

运维:“卧槽,还有惰性调配这种货色!长常识了”

我:“这下明确了吧,这个内存状况是失常的,咱们的服务一点问题都没有”

运维:“🐂🍺,是我了解错了,你们这个服务没啥问题”

我:“嗯呐,没事那我先去忙(摸鱼)了”

总结

对于大多数服务端场景来说,并不需要 JVM 这个手动开释内存的操作。至于 JVM 是否偿还内存给操作系统这个问题,咱们也并不关怀。而且基于下面那个测试后果,不同 JAVA 版本,不同垃圾回收器版本区别这么大,更是没必要去深究了。

综上,JVM 尽管能够开释闲暇内存给操作系统,然而不肯定会开释,在不同 JAVA 版本,不同垃圾回收器版本下体现不同,晓得有这个机制就行。

参考

  • https://docs.oracle.com/javase/10/gctuning/factors-affecting-garbage-collection-performance.htm#JSGCT-GUID-B0BFEFCB-F045-4105-BFA4-C97DE81DAC5B
  • https://stackoverflow.com/questions/30458195/does-gc-release-back-memory-to-os
  • 《深刻了解 Java 虚拟机:JVM 高级个性与最佳实际(第 2 版)》– 周志明 著

原创不易,禁止未受权的转载。如果我的文章对您有帮忙,就请点赞 / 珍藏 / 关注激励反对一下把!

正文完
 0