什么!服务器内存又双叒叕打满了!

这篇具有很好参考价值的文章主要介绍了什么!服务器内存又双叒叕打满了!。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

1 问题背景

深夜,一阵急促的报警声响起,原来是收到一连串容器内存使用率过高的报警。查看服务器状态,容器内存使用率持续高达99%,记录排查过程和分析,供大家参考。

2 问题现象

检查容器状态,观察到以下现象:

  • JVM堆内存使用率正常,在50%左右波动,在凌晨1点的时候年轻代使用无明显波动,老年代使用有直线上涨,但是执行了一次Full GC(也可能是Major GC)后恢复了正常。

  • 容器内存使用率在凌晨1点的时候有一个直线的上涨,并且随后一直保持在上涨后的水平。

服务使用的配置如下:

主要启动参数:-Xms4g -Xmx4g -Xmn2g -XX:+UseG1GC -XX:G1HeapRegionSize=8m -XX:G1ReservePercent=15 -XX:InitiatingHeapOccupancyPercent=50

容器配置:4C5G

3 定位原因

虽然容器内存已经使用99%,但是JVM堆使用率在经过一次Full GC后已经降到了50%之后,所以并没有立马重启服务,还是先来看看造成Full GC的原因。

鉴于堆使用率已经恢复正常,查看当时的内存快照意义不大,决定先从观察到的现象入手。因为凌晨1点的时候系统的流量不会很大,所以大概率是因为定时任务造成了,先排查报警服务凌晨1点执行的定时任务。幸运的是调度平台当时只有一个定时任务在执行,立马查看对应的逻辑,发现了以下代码片段中潜在的问题(简化之后):

public void job() {
    // ... do business
    int pageSize = 500;
    while ( xxx ) {
        // 每次查询500个订单 
        List<String> orderNoList = orderService.getOrderPage(pageSize);
        // 查询500个订单对应的账单
        List<OrderBill> orderBills = billService.findByOrderNos(orderNoList);
        // ... do business
    }
    // ... do business
}

由于大部分订单包含12至24期账单,导致此处的orderBills在平均情况下含有数千至数万条数据。经过粗略估算,这些对象的总大小大约为5MB左右。

看启动参数中G1HeapRegionSize=8m,代表每个Region的大小为8M,G1 GC会将大于Region一半大小的对象直接分配到老年代。所以orderBills对象会被直接分配到老年代,这也和在凌晨1点的时候年轻代使用无明显波动,老年代使用有直线上涨的现象相符合。在后续的循环中,由于垃圾收集器清理的速度赶不上内存分配的速度,导致垃圾逐渐积累并最终填满整个堆空间,触发了Full GC

为什么GC之后容器的内存还是一直居高不下呢?

经过一番查阅资料,原来JVM启动时并不会立即实际占用全部Xms指定的内存。Xms参数指定的是堆的初始大小,JVM会按照这个值预留内存空间,但实际上只有在需要时才会逐渐使用这些预留的内存。

JVM在完成内存释放后,是否将释放的内存返回给操作系统,这一行为取决于具体的JVM实现及其采用的垃圾回收策略。在很多情况下,JVM在进行垃圾收集并释放了堆内存之后,并不会立即将这部分内存归还给操作系统。

相反,它会保留这部分内存以备将来Java应用程序的使用,因为从操作系统重新申请内存通常会比从JVM内部管理的内存分配更为昂贵(时间成本上)。所以一般程序还会将XmsXmx配置为相等的大小,避免频繁申请和释放内存造成的性能开销。

上面两个问题找到原因了,小菜同学决定在本地复现一下进行验证。

新建一个memorytest项目,写一个方法模拟内存分配:

/**
 * 模拟内存分配
 * @param num 循环次数
 * @param size 每次分配几MB的数据
 */
@RequestMapping("/memory/add/{num}/{size}")
public String add(@PathVariable("num") Integer num, @PathVariable("size") Integer size) {
    for (int i = 0; i < num; i++) {
        // 模拟查询出来的大对象
        byte[] allocation = new byte[size * 1024 * 1024];
    }
    return "";
}

用以下命令启动:

java -Xms2g -Xmx2g -Xmn1g -XX:+UseG1GC -XX:G1HeapRegionSize=8m -XX:G1ReservePercent=15 -XX:InitiatingHeapOccupancyPercent=50 -jar memorytest-0.0.1-SNAPSHOT.jar

使用Jdk自带的命令查询JVM内存分配情况,先使用jps -l查询进程ID:

jps -l
16988 sun.tools.jps.Jps
9068 memorytest-0.0.1-SNAPSHOT.jar

使用jmap -heap <pid>查询堆内存分配:

jmap -heap 9068
Heap Usage:
G1 Heap:
   regions  = 256
   capacity = 2147483648 (2048.0MB)
   used     = 230686720 (220.0MB)
   free     = 1916796928 (1828.0MB)
   10.7421875% used
G1 Young Generation:
Eden Space:
   regions  = 26
   capacity = 1115684864 (1064.0MB)
   used     = 218103808 (208.0MB)
   free     = 897581056 (856.0MB)
   19.548872180451127% used
Survivor Space:
   regions  = 2
   capacity = 16777216 (16.0MB)
   used     = 16777216 (16.0MB)
   free     = 0 (0.0MB)
   100.0% used
G1 Old Generation:
   regions  = 0
   capacity = 1015021568 (968.0MB)
   used     = 0 (0.0MB)
   free     = 1015021568 (968.0MB)
   0.0% used

使用jstat -gcutil <pid> <interval[s|ms]> 1秒1次监控堆内存使用和GC情况(也可以使用jconsole可视化处理工具来查看内存的使用情况):

jstat -gcutil 9068 1000
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  21.80   0.00  94.17  88.38      1    0.023     0    0.000    0.023

该命令返回的主要的参数的意义:
S0Survivor space 0区的使用率。
S1Survivor space 1区的使用率。
EEden区的使用率。
OOld区(老年代)的使用率。
YGC:年轻代垃圾收集事件的次数。
YGCT:年轻代垃圾收集所耗费的时间(秒)。
FGCFull GC(全堆垃圾收集)事件的次数。
FGCTFull GC所耗费的时间(秒)。
GCT:垃圾收集所耗费的总时间(秒)。

此时使用ps aux --sort -rss查看Java进程占用的本机内存才370MB左右,并没有直接占用Xms设置的2g。

ps aux --sort -rss
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     9068  5.4  9.9 4718036 374536 pts/1  Sl+  15:10   0:09 java -Xms2g -Xmx2g -Xmn1g -XX:+UseG1GC -XX:G1HeapRegionSize=8m -XX:G1ReservePercent=15 -XX:InitiatingHeapOccupancyPercent=50 -jar memorytest-0.0.1-SNAPSHOT.jar

该命令返回的主要的参数的意义:
PID: 进程ID。
%CPU: 进程使用的CPU百分比。
%MEM: 进程使用的物理内存百分比。
RSS: 进程当前占用的物理内存大小,单位通常是KB。
COMMAND: 启动进程的命令行命令。

开始模拟业务分配内存,为了方便观察直接循环100000次,每次分配5MB空间:

http://127.0.0.1:8080/memory/add/100000/5

jstat此时监控到内存情况:

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  24.81   0.00  94.17  88.38      1    0.023     0    0.000    0.023
  0.00   0.00   1.48  61.36  93.38  89.74     12    0.192     0    0.000    0.192
  0.00 100.00   1.49  86.37  92.74  89.74     33    0.294     0    0.000    0.294
  0.00   0.00   2.27  99.41  92.74  89.74     56    0.395     0    0.000    0.395
  0.00   0.00   2.15  99.55  92.75  89.74     84    0.522     0    0.000    0.522
  0.00   0.00   1.94  99.77  92.20  89.00    119    0.663     0    0.000    0.663
  0.00   0.00   4.00  99.71  92.21  89.01    169    0.834     0    0.000    0.834
  0.00 100.00   0.75  21.43  92.22  89.01    232    0.998     1    0.051    1.049
  0.00 100.00   0.84  99.68  92.22  89.01    252    1.025     1    0.051    1.077
  0.00 100.00   0.88  99.87  92.25  89.01    274    1.064     1    0.051    1.115
  0.00   0.00   1.48  70.73  92.25  89.01    299    1.110     1    0.051    1.161
  0.00   0.00   1.48  75.90  92.25  89.01    327    1.168     1    0.051    1.219
  0.00   0.00   1.77  99.81  92.25  89.01    361    1.239     1    0.051    1.290
  0.00   0.00   2.41  99.92  92.27  89.01    409    1.340     1    0.051    1.392

此时可以看到对象都直接分配到了老年代,年轻代的内存使用没有多大的变化,并且当YGC(Young Generation Garbage Collection,年轻代垃圾回收)来不及回收时就会发生FGC(Full Garbage Collection,全堆垃圾回收)

此时再使用ps aux --sort -rss查看Java进程占用的本机内存已经到了2G了,并且在方法执行完成后(也包括FGC后)也没有释放此内存。

ps aux --sort -rss
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root     9068 77.1 60.4 4720084 2043996 pts/1 Sl+  15:10   6:03 java -Xms2g -Xmx2g -Xmn1g -XX:+UseG1GC -XX:G1HeapRegionSize=8m -XX:G1ReservePercent=15 -XX:InitiatingHeapOccupancyPercent=50 -jar memorytest-0.0.1-SNAPSHOT.jar

再模拟一次业务分配内存,为了方便观察直接循环100000次,这次每次分配2MB空间:

http://127.0.0.1:8080/memory/add/100000/2

jstat此时监控到内存情况:

  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT   
  0.00 100.00  15.04   0.00  94.14  88.45      1    0.027     0    0.000    0.027
  0.00 100.00  15.04   0.00  94.14  88.45      1    0.027     0    0.000    0.027
  0.00 100.00  93.18   0.00  93.15  89.36      2    0.043     0    0.000    0.043
  0.00 100.00  51.49   0.66  93.15  89.36     22    0.146     0    0.000    0.146
  0.00   0.00  68.15   1.48  93.15  89.36     44    0.167     0    0.000    0.167
  0.00   0.00  81.48   1.48  93.15  89.36     66    0.186     0    0.000    0.186
  0.00   0.00  16.30   1.48  93.15  89.36     89    0.207     0    0.000    0.207
  0.00   0.00  58.52   1.48  93.15  89.36    111    0.226     0    0.000    0.226
  0.00   0.00   4.44   1.48  93.16  89.36    134    0.246     0    0.000    0.246
  0.00 100.00  50.00   0.66  93.16  89.36    156    0.265     0    0.000    0.265
  0.00   0.00  88.15   1.49  93.16  89.36    178    0.284     0    0.000    0.284
  0.00   0.00  41.48   1.49  93.16  89.36    201    0.305     0    0.000    0.305
  0.00   0.00  87.41   1.49  93.16  89.36    223    0.324     0    0.000    0.324
  0.00   0.00  27.41   1.49  93.17  89.36    246    0.344     0    0.000    0.344
  0.00   0.00  89.63   1.49  93.17  89.36    263    0.358     0    0.000    0.358
  0.00   0.00  89.63   1.49  93.17  89.36    263    0.358     0    0.000    0.358

此时很明显可以看到对象都直接分配到了年轻代,年轻代的回收效率也比老年代高,并且没有产生FGC

再结合本次报警的现象,解决办法也呼之欲出:

  • 通过减少查询返回的数据量,避免大对象直接分配至老年代。

  • 调整Region大小,较大的Region可以提高大对象分配的效率,同时可能会导致GC停顿时间变长,较小的Region可能意味着更短的停顿时间,但是会增加维护开销,可能导致更高的CPU使用率。所以需要通过监控工具(如JConsoleVisualVM等)观察不同Region大小设置下的GC表现和应用吞吐量,以找到最佳平衡点。

4 问题小结

JVM内存问题可能不常有,但是一旦发生可能对我们系统造成极大的影响,此次内存飙升也给我们敲响了警钟,发现了我们在这边注重的不够,很多监控和日志并不完整,如启动参数中没有加上发生OOM时自动dump文件的参数等。好在此次服务未受到影响,所有有足够的时候给我们去排查和验证。特此结合此次排查的过程的一些收获记录一下,假设下次还有发生可以有所参考:

  • 如果对业务产生了影响,服务不可用或者进程直接没有了,首先需要快速止损,包括熔断、重启、手动GC等方式。但是操作前需要先dump出内存快照方便问题排查,命令如下:

#jmap命令保存整个Java堆(在你dump的时间不是事故发生点的时候尤其推荐)
jmap -dump:format=b,file=heap.bin <pid> 

#jmap命令只保存Java堆中的存活对象, 包含live选项,会在堆转储前执行一次Full GC
jmap -dump:live,format=b,file=heap.bin <pid>

#jcmd命令保存整个Java堆,Jdk1.7后有效
jcmd <pid> GC.heap_dump filename=heap.bin

还有在启动参数中添加发生OOM时自动生成heapdump的参数:-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heap.bin启动参数中添加GC日志打印相关的参数:

# Java8及以下
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:<path>

# Java9及以上
-Xlog:gc*:<path>:time
  • 如果对业务无影响,可以先观察现象进行排查。如果是近期有业务增加,则可以考虑是否需要扩容,如果是周期性的表现,则可以排查定时任务。

  • 对导出的内存快照文件使用MAT等工具进行分析,一般会比较直观的看到当前堆内情况。

  • 如果服务还可用,也可以使用命令进行排查,Jdk本身也已经提供非常多轻量的小工具,主要用于监视虚拟机运行状态和进行故障处理,常用的如下(还有一些第三方的工具也很好用,如arthas等):

jstat -gcutil <pid> :监视Java堆状况,主要关注已使用空间的百分比和GC情况

jmap -heap <pid> :显示Java堆详细信息

jmap -histo[:live] <pid> :显示堆中对象统计信息:快速识别哪些类的实例占用了大量的堆内存
  • 如果是栈溢出,抛出StackOverflowError异常,需要着重检查是否有死循环或者代码调用链路不合理。

  • 还有可能是堆外内存泄露(元空间、直接内存等),此情况一般较少发生,排查起来也更加复杂,小菜同学对于这部分的实操有限,这里就不详细分析了,还需要继续学习。

5 总结

面对内存使用率的异常,不仅要关注即时的解决方案,还需要考虑如何从根本上避免此类问题的再次发生。如使用优化数据结构、减少不必要的计算、采用懒加载等策略,以及建立细致的内存监控体系等。性能优化不应该仅仅是面对问题时的临时行动,而应该成为开发文化的一部分。

希望这次的经历和总结能够为未来可能遇到的类似问题提供一点参考的价值,也希望屏幕前的大佬对小菜同学写的不对或者遗漏的地方不吝指正,非常感谢!


关于作者    蔡闯,金融技术部后端研发工程师文章来源地址https://www.toymoban.com/news/detail-848105.html

到了这里,关于什么!服务器内存又双叒叕打满了!的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Windows 11又双叒出现Bug,导致截图工具崩溃

    自Windows 11于10月5日正式推送以来,其自身Bug就接连不断。此前就有媒体报道Windows 11影响AMD游戏性能下降问题,不过在10月21日微软发布了KB5006746 (Build 22000.282)更新补丁,彻底解决了上述问题。 间隔不到两周,Windows 11又被曝出Bug。近日有大量网友反映微软截图工具 (snippin

    2024年02月04日
    浏览(39)
  • 服务器的内存和普通的内存有什么不一样呢

    一.首先在选择上来说 普通的内存比如PC内存等等,大家通常都会衡量不同品牌,不同产品之间的性能差异,以及追求一个性价比。 服务器内存则不一样,服务器内存最重要的是要稳定和纠错,并不会去追求更高的频率或者速度,但是会更加看重技术,可以这么说,部分先进

    2024年02月11日
    浏览(33)
  • 银河麒麟操作系统free查看服务器的内存,为什么比实际物理内存少很多?

    银河麒麟操作系统创建成功后,free -m命令查询内存大小,查询结果比实际物理内存小很多。 创建的虚拟机实际内存为8192M。系统内查询可用内存为6807M 使用 dmidecode -t memory 命令查看实际的硬件内存大小, free -m 查询系统内内存大小如下: 可以看到使用dmidecode -t memory查看的内

    2024年02月07日
    浏览(73)
  • 服务器内存使用查询

    Linux 查看内存使用情况的几种方法包括使用 free 命令、top 命令、htop 命令、vmstat 命令和/proc/meminfo 文件。这些方法可以帮助用户了解系统内存的使用情况,包括总内存、已用内存、空闲内存、缓存和交换分区等信息。 命令获取 1、虚拟文件 /proc/meminfo 在 linux 系统中,/proc/me

    2024年01月17日
    浏览(30)
  • 戴尔730服务器扩容内存

    工作需要给服务器内存扩容,服务器品牌为Dell R730,之前未接触过给服务器扩容内存,在不断查询相关资料后完成对服务器内存的扩容,故此写篇随笔记录。 前提:服务器之前加装了8条8G的内存条, 即服务器内存共64G 工作需要:在服务器原有内存大小基础上扩容服务器内存

    2024年02月19日
    浏览(30)
  • 服务器新增内存不识别问题处理

    手上有台服务器的扩容内存,扩容之后发现有一条不识别,重新插拔了一下就可以识别了。 获取异常内存的槽位号,就是Locator标志 打开机盖,找到对应的槽位,重新插拔 被标签盖住了,问题不大 清理下金手指,OKOK。

    2024年04月28日
    浏览(24)
  • Linux服务器增加虚拟交换内存

    使用dd命令创建名为swapfile 的swap交换文件(文件名和目录任意): dev/zero是Linux的一种特殊字符设备(输入设备),可以用来创建一个指定长度用于初始化的空文件,如临时交换文件,该设备无穷尽地提供0,可以提供任何你需要的数目。 bs=1024 :单位数据块(block)同时读入/输出

    2024年02月15日
    浏览(36)
  • 服务器内存满了解决之路

    背景:大清早,突然一通电话吵醒,说项目跑不了,还没洗漱赶紧跑过来,毕竟属于实时在用的系统。排查发现系统盘满了,数据写不进去了,导致报错。接手的项目,从来没考虑服务器问题,那么就解决吧,一通百度。 df -h   查看内存满了  那就 du --max-depth=1 -h 查看哪个地

    2024年02月16日
    浏览(35)
  • 服务器的组成(CPU、内存、硬盘)

    目录 服务器的分类 服务器的组成 中央处理器CPU 内存 硬盘 网卡、存储卡、RAID卡 什么是服务器 服务器是在网络中为其他客户机提供服务的高性能计算机;具有高速的CPU运算能力,能够长时间的可靠运行,有强大的I/O外部数据吞吐能力以及更好的扩展性 服务器的内部结构与普

    2024年01月18日
    浏览(55)
  • Linux 查看服务器内存、CPU 命令

    1 查看物理CPU个数:         Procs(进程) 2 查看服务器CPU内核个数 1 linux查看系统内存(硬盘) 2 查看服务器硬盘(当前文件夹下)使用率: 3 查看服务器硬盘(所有文件占用率)使用率: 1 查看内存,不带单位 2 查看内存使用情况,带单位,显示查看结果 显示的参数:       

    2024年02月15日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包