运维:大家 JAVA 服务内存占用过高,还只增不减!告警了,快来接锅

先点赞再看,养成好习惯java

某天,运维老哥忽然找我:“大家的某 JAVA 服务内存占用过高,告警了!GC 后也没释放,内存只增不减,是否是内存泄漏了!”算法

而后我赶忙看了下监控,一切正常,距离上次发版好几天了,FULL GC 一次没有,YoungGC,十分钟一次,堆空闲也很充足。服务器

运维:“大家这个服务如今堆内存 used 才 800M,但这个 JAVA 进程已经占了 6G 内存了,是否是大家程序出啥内存泄露的 bug 了!”
我想都没想,直接回了一句:“不可能,咱们服务很是稳定,不会有这种问题!”

image.png

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

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

迅速上登上服务器又仔细的查看了各类指标,Heap/GC/Thread/Process 之类的,发现一切正常,并无什么“泄漏”的迹象。运维

和运维的“沟通”

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

image.png

运维:你看大家这个 JAVA 服务,堆如今 used 才 400MB,但这个进程如今内存占用都 6G 了,还说没问题?确定是内存泄露了,锅接好,赶忙回去查问题吧 测试

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

运维:“回收了你这内存也没释放啊,你看这个进程 Res 仍是 6G,确定有问题啊”操作系统

我心想这运维怕不是个der,JVM GC 回收和进程内存又不是一回事,不过仍是和得他解释一下,否则一直baba个没完线程

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

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

虽然我心里是拒绝的,但得罪谁也不能得罪运维啊,想一想仍是给大哥解释解释,“增进下感情”

image.png

操做系统 与 JVM的内存分配

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

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

为何不把内存归还给操做系统?

JVM 仍是会归还内存给操做系统的,只是由于这个代价比较大,因此不会轻易进行。并且不一样垃圾回收器 的内存分配算法不一样,归还内存的代价也不一样。

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

image.png

每一个 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 内存区域中写数据的。

运维:“卧槽,还有惰性分配这种东西!长知识了”

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

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

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

image.png

总结

对于大多数服务端场景来讲,并不须要JVM 这个手动释放内存的操做。至于 JVM 是否归还内存给操做系统这个问题,咱们也并不关心。并且基于上面那个测试结果,不一样 JAVA 版本,不一样垃圾回收器版本区别这么大,更是不必去深究了。

综上,JVM 虽然能够释放空闲内存给操做系统,可是不必定会释放,在不一样 JAVA 版本,不一样垃圾回收器版本下表现不一样,知道有这个机制就行。

参考

原创不易,禁止未受权的转载。若是个人文章对您有帮助,就请点赞/收藏/关注鼓励支持一下把!
相关文章
相关标签/搜索