转转技术--什么!服务器内存又双叒叕打满了!

原文地址:https://mp.weixin.qq.com/s/o_epKOBXKuM-qOT7_zXayQ

  • 1 问题背景
  • 2 问题现象
  • 3 定位原因
  • 4 问题小结
  • 5 总结

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 businessint 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  = 256capacity = 2147483648 (2048.0MB)used     = 230686720 (220.0MB)free     = 1916796928 (1828.0MB)10.7421875% used
G1 Young Generation:
Eden Space:regions  = 26capacity = 1115684864 (1064.0MB)used     = 218103808 (208.0MB)free     = 897581056 (856.0MB)19.548872180451127% used
Survivor Space:regions  = 2capacity = 16777216 (16.0MB)used     = 16777216 (16.0MB)free     = 0 (0.0MB)100.0% used
G1 Old Generation:regions  = 0capacity = 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 1000S0     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.0230.00   0.00   1.48  61.36  93.38  89.74     12    0.192     0    0.000    0.1920.00 100.00   1.49  86.37  92.74  89.74     33    0.294     0    0.000    0.2940.00   0.00   2.27  99.41  92.74  89.74     56    0.395     0    0.000    0.3950.00   0.00   2.15  99.55  92.75  89.74     84    0.522     0    0.000    0.5220.00   0.00   1.94  99.77  92.20  89.00    119    0.663     0    0.000    0.6630.00   0.00   4.00  99.71  92.21  89.01    169    0.834     0    0.000    0.8340.00 100.00   0.75  21.43  92.22  89.01    232    0.998     1    0.051    1.0490.00 100.00   0.84  99.68  92.22  89.01    252    1.025     1    0.051    1.0770.00 100.00   0.88  99.87  92.25  89.01    274    1.064     1    0.051    1.1150.00   0.00   1.48  70.73  92.25  89.01    299    1.110     1    0.051    1.1610.00   0.00   1.48  75.90  92.25  89.01    327    1.168     1    0.051    1.2190.00   0.00   1.77  99.81  92.25  89.01    361    1.239     1    0.051    1.2900.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.0270.00 100.00  15.04   0.00  94.14  88.45      1    0.027     0    0.000    0.0270.00 100.00  93.18   0.00  93.15  89.36      2    0.043     0    0.000    0.0430.00 100.00  51.49   0.66  93.15  89.36     22    0.146     0    0.000    0.1460.00   0.00  68.15   1.48  93.15  89.36     44    0.167     0    0.000    0.1670.00   0.00  81.48   1.48  93.15  89.36     66    0.186     0    0.000    0.1860.00   0.00  16.30   1.48  93.15  89.36     89    0.207     0    0.000    0.2070.00   0.00  58.52   1.48  93.15  89.36    111    0.226     0    0.000    0.2260.00   0.00   4.44   1.48  93.16  89.36    134    0.246     0    0.000    0.2460.00 100.00  50.00   0.66  93.16  89.36    156    0.265     0    0.000    0.2650.00   0.00  88.15   1.49  93.16  89.36    178    0.284     0    0.000    0.2840.00   0.00  41.48   1.49  93.16  89.36    201    0.305     0    0.000    0.3050.00   0.00  87.41   1.49  93.16  89.36    223    0.324     0    0.000    0.3240.00   0.00  27.41   1.49  93.17  89.36    246    0.344     0    0.000    0.3440.00   0.00  89.63   1.49  93.17  89.36    263    0.358     0    0.000    0.3580.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 总结

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/805647.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Java8新特性】三、方法引用和构造器引用

这里写自定义目录标题 一、方法引用1、demo 二、构造器引用三、数组引用 一、方法引用 当要传递给Lambda体的操作&#xff0c;已经有实现的方法了&#xff0c;可以使用方法引用&#xff01; 方法引用&#xff1a;使用操作符 “::” 将方法名和对象或类的名字分隔开来。 主要…

C++ 11 ~ C++21标准发展及其新特性概览

C++是一种广泛应用的编程语言,自从1983年由Bjarne Stroustrup首次引入以来,它已经经历了多次重要的标准更新。每次更新都在语言的功能性、性能和易用性方面带来了显著的改进。本文将详细介绍自C++11以来的每个主要版本的新特性,并通过示例代码展示它们的应用。 C++11 (2011…

PyQt PySide6零基础入门与项目实战视频教程

目录 课程亮点课程大纲第一章&#xff1a;基础篇 PySide6开发环境安装第二章 控件与布局篇 PySide6常用控件与界面布局使用介绍第三章 信号槽与事件机制第四章 QMainWindow应用篇第五章 样式表qss与自定义控件第六章 图表与曲线第七章 数据库编程第八章 项目实战&#xff1a;高…

Java—抽象方法与接口

声明&#xff1a;以下内容是根据B站黑马程序员的Java课程&#xff0b;博主自己的理解整理而成&#xff0c;课程很好&#xff0c;适合初学者学习。 关于此类题目&#xff0c;重要的是识别出用什么来实现&#xff0c;到底是接口还是抽象方法&#xff0c;还是共有的属性等等&…

前端小白的学习之路(Vue 三)

提示&#xff1a;学习vue2的第三天&#xff0c;笔记记录&#xff1a;生命周期&#xff0c;组件(注册&#xff0c;传值) 目录 一、生命周期 二、组件 1.注册组件 1)全局注册 2)局部注册 2.组件传值 1&#xff09;父传子 2&#xff09;子传父 3&#xff09;兄弟传值 一…

QT:事件机制

作业&#xff1a; widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QTimerEvent> #include <QTime> #include<QPushButton> #include <QTextToSpeech>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAME…

vue qrcode生成二维码

1. 安装&#xff1a;npm install vue-qr --save 2. 导入&#xff1a; vue2.x ---- import VueQr from vue-qr vue3.x ---- import vueQr from vue-qr/src/packages/vue-qr.vue3. 3. 使用&#xff1a; 生成二维码 <vue-qr ref"qrCode" :text"te…

Vue3---基础1(认识,创建)

变化 相对于Vue2&#xff0c;Vue3的变化&#xff1a; 性能的提升 打包大小减少 41% 初次渲染快 55%&#xff0c;更新渲染快133% 内存减少54% 源码的升级 使用 proxy 代替 defineProperty 实现响应式 重写虚拟 DOM 的实现和 Tree-shaking TypeScript Vue3就可以更好的支持TypeSc…

Redis中的集群(四)

集群 槽指派 CLUSTER ADDSLOTS命令的实现 CLUSTER ADDSLOTS命令接受一个或多个槽作为参数&#xff0c;并将所有输入的槽指派给接收该命令的节点负责: CLUSTER ADDSLOTS <slot> [slot ...]CLUSTER ADDSLOTS命令的实现可以用以下伪代码来表示: def CLUSTER_ADDSLOTS(*…

如何在windows环境和linux环境运行jar包

功能:实现对字符串小写转大写 java代码如下: package a_od_test;import java.util.Locale; /* 实现小写转大写 打jar包 分别在windows环境和liunx环境运行*/ public class Main28_To_Upper {public static void main(String[] args) {if (args.length 1) {System.out.printl…

C/C++ inline 函数

C/C中包含了一类inline函数&#xff0c;其只是单纯在原本函数申明或者定义前面多了一个inline 但是带来含义的确实不一样的。 如果不带inline那么主函数执行到函数入口处会跳到相应的函数代码除继续执行&#xff0c;在内存 中的代码段内存中这些代码不是连续的&#xff0c;这样…

JavaScript - 请你为数组自定义一个方法myFind,使其实现find方法的功能

难度级别:中级及以上 提问概率:50% 我们知道数组的find方法是ES6之后出现的,它强调找到第一个符合条件的元素后即跳出循环,不再继续执行,那么如果不用ES6的知识,为数组添加一个自定义方法实现find方法的功能,首先要想到在数组的原型pro…

探索进程控制第一弹(进程终止、进程等待)

文章目录 进程创建初识fork函数fork函数返回值fork常规用法fork调用失败的原因 写时拷贝进程终止进程终止是在做什么&#xff1f;进程终止的情况代码跑完&#xff0c;结果正确/不正确代码异常终止 如何终止 进程等待概述进程等待方法wait方法waitpid 进程创建 初识fork函数 在…

unity 历史版本下载

原文链接&#xff1a;https://blog.csdn.net/huang714/article/details/106623675 Unity历史版本下载列表 Unity2019系列最新版本:Unity 2019.1.3 Unity2018系列最新版本:Unity 2018.4.0 Unity2017系列最新版本:Unity 2017.4.27 Unity5.x系列最新版本:Unity 5.6.7 Unity4.x系…

基础算法(算法竞赛、蓝桥杯)--堆排序

1、B站视频链接&#xff1a;A15 堆 堆排序_哔哩哔哩_bilibili 题目链接&#xff1a;【模板】堆 - 洛谷 #include <iostream> using namespace std; int a[1000010],cnt; void up(int u){ //上浮if(u/2 && a[u/2]>a[u]) swap(a[u],a[u/2]), up(u/2); } void d…

校招详解(术语、时间、流程)

3400字的详细说明&#xff0c;介绍了程序员类岗位校招的整体时间节点和招聘流程。还对一些常见的问题进行讨论&#xff0c;例如内推、offer和三方、实习等。 第一章介绍基本的术语&#xff0c;第二章介绍整个校招的重要流程及时间点&#xff0c;然后第三章介绍每次招聘要经过的…

[方案实操|数据技术]数据要素十大创新模式(1):基于区块链的多模态数据交易服务平台

“ 区块链以其公开共享、去中心化、不可篡改、可追溯和不可抵赖等优势&#xff0c;吸引了包括金融业、医疗业和政府部门等众多利益相关方的极大兴趣&#xff0c;被认为是解决数据安全交换问题的合适方案。” 武汉东湖大数据科技股份有限公司凭借基于区块链的多模态数据交易服务…

计算机组成结构—外部存储器

目录 一、磁盘存储器 1. 磁表面存储器和磁记录原理 2. 硬磁盘的分类和基本结构 &#xff08;1&#xff09;硬磁盘存储器的分类 &#xff08;2&#xff09;硬磁盘存储器的组成 3. 磁盘的工作原理 &#xff08;1&#xff09;磁盘存储区域 &#xff08;2&#xff09;磁盘地…

计算机病毒防治

一、计算机病毒概述 计算机病毒是必然产物。 计算机病毒是一种人为制造的&#xff0c;侵入计算机系统、寄生于应用程序或系统可执行部分&#xff0c;并可以自我复制、传播&#xff0c;具有激活型、攻击性的程序代码。 计算机病毒宿主 病毒大多不以文件形式存在&#xff0c;寄生…

Linux--进程间的通信-匿名管道

进程间的通信 进程间通信&#xff08;IPC&#xff0c;Interprocess Communication&#xff09;是指在不同进程之间传输数据和交换信息的一种机制。它允许多个进程在同一操作系统中同时运行&#xff0c;并实现彼此之间的协作。 进程间通信方式&#xff1a; 管道&#xff08;Pi…