Java 服务内存占用太高

一、问题现象

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

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

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

我想都没想,直接回了一句:“不可能,我们服务非常稳定,不会有这种问题!” 

二、问题分析

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

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

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

运维:你看你们这个 JAVA 服务,堆现在 used 才 400MB,但这个进程现在内存占用都 6G 了,还说没问题?肯定是内存泄露了,锅接好,赶紧回去查问题吧

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

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

JVM GC 回收和进程内存又不是一回事,不过还是和运维解释一下,不然一直baba个没完。

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

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

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

三、操作系统 与 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 8UseParallelGC(ParallerGC + ParallerOld)-Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40
JAVA 8CMS+ParNew-Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
JAVA 8UseG1GC(G1)-Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40 -XX:+UseG1GC
JAVA 11UseG1GC(G1)-Xms100M -Xmx2G -XX:MaxHeapFreeRatio=40
JAVA 16UseZGC(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 版本,不同垃圾回收器版本下表现不同,知道有这个机制就行。

  • 5
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: Java在Linux上占用过高的内存可能是由于以下原因: 1. Java应用程序本身需要大量的内存来运行,特别是在处理大量数据时。 2. Java虚拟机(JVM)的内存管理机制可能会导致内存占用过高。例如,如果JVM没有正确配置,它可能会分配太多的内存,或者不及时释放不再使用的内存。 3. Java应用程序可能存在内存泄漏问题,导致内存占用不断增加。 为了解决这个问题,可以采取以下措施: 1. 检查Java应用程序的代码,确保它们正确地使用内存,并避免内存泄漏。 2. 配置JVM的内存参数,以确保它们适合应用程序的需求。例如,可以使用-Xmx和-Xms参数来限制JVM使用的最大和最小内存。 3. 使用Linux的内存监控工具,如top或htop,来查看Java进程的内存使用情况,并及时调整配置。 4. 如果Java应用程序需要处理大量数据,可以考虑使用分布式计算框架,如Hadoop或Spark,来分散负载并减少内存占用。 ### 回答2: 当Java应用程序在Linux主机上运行时,可能会出现内存占用过高的情况。这是因为Java应用程序运行时需要占用一定量的内存空间。而如果程序过于庞大,会使得系统不堪重负,内存占用率过高。以下是解决办法: 1. 使用JVM参数:可以通过调整JVM(Java虚拟机)参数来降低Java应用程序的内存占用率。例如可以使用-Xmx参数来限制JVM使用的最大内存量,使用-Xms参数来设置JVM的初始内存使用量,使JVM在程序启动时尽可能占用较少的内存空间。 2. 升级JDK版本:有时候,Java应用程序在运行时会遇到一些性能问题,这些问题在新的JDK版本中可能得到解决。因此升级JDK版本也是解决内存占用过高问题的一种方案。 3. 优化代码:Java应用程序内存占用过高的原因有可能是程序本身缺乏优化,例如有些代码可能是重复的或者没有优化的。所以,可以通过优化代码来降低内存占用率。 4. 分析线程:线程是Java应用程序的一个重要组成部分,如果线程不合理使用,会导致内存占用过高。因此,可以通过分析线程来检测是否存在无用线程或者繁忙的操作。如果存在,可以进行调整,从而降低内存占用率。 总之,在Linux主机上运行Java应用程序时,内存占用过高是一个常见的问题。但是通过合理的调整JVM参数、升级JDK版本、优化代码和分析线程等方法,可以有效解决这个问题,提高程序的性能并减少对系统资源的损耗。 ### 回答3: Java内存占用过高,可能是由于以下几个原因引起的。 一、JVM参数设置不合理。Java内存管理是交给JVM来完成的。JVM有许多参数可以控制内存的使用情况。如果参数设置不合理,会导致Java内存占用过高。例如,如果JVM堆内存的大小设置得太大,则会占用大量的系统内存。 二、Java应用设计不合理。Java应用程序的设计也会影响Java内存占用。如果设计的不合理,会导致内存占用过高。例如,如果Java应用程序中存在内存泄漏的情况,则会导致内存占用不断增加。 三、应用程序的代码中存在可能导致内存泄漏的代码。内存泄漏是在程序运行过程中未释放不再使用的内存情况。在Java开发中,常见的内存泄漏的情况有几种:未关闭IO资源,内部类引用外部类对象,未清空软引用和弱引用等。 解决方案: 一、调整JVM参数。合理设置JVM堆内存大小,可以将-Xms和-Xmx参数的值设置成合适的值,以避免占用过高的问题。 二、优化Java应用程序的设计。避免不良设计,应该始终关注内存的使用和优化应用程序。优化内存的使用在应用程序开发中是必不可少的。可以使用各种的映射技术,例如提高对象复用达到内存消耗减小的效果。 三、处理潜在内存泄漏代码。通过工具或者IDE的代码检查功能,查找Java代码中有可能出现内存泄漏的代码,及时修正,避免Java程序中存在类似的情况,从而减少内存占用。可以用工具,如内存分析器来分析Java内存的使用情况,识别潜在的内存泄漏问题。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值