文章
分析服务器内存占用过高
从应用层面、系统/环境层面和外部/安全因素三个维度,详细分析各类常见场景、诊断思路以及对应的优化建议
目录
- 在服务器上线后,如果长期监控发现内存使用居高不下(如常年保持在 80%~100%),就需要及时排查并优化。下面从应用层面、系统/环境层面和外部/安全因素三个维度,详细分析各类常见场景、诊断思路以及对应的优化建议。
- 一、应用层面因素
- 1. 内存泄漏(Memory Leak)
- 2. 高频率、大对象创建与临时对象累积
- 3. 缓存与 Buffer 使用不当
- 4. 缺乏合理的 JVM 参数与垃圾收集器调优
- 5. 线程/连接池配置不合理
- 二、系统/环境层面因素
- 1. 操作系统缓存(Page Cache)与文件系统占用
- 2. 系统守护进程或安全软件
- 3. 虚拟化/容器环境内存隔离失效
- 4. 内核参数或驱动问题
- 三、外部/安全因素
- 1. 短期流量暴涨导致缓存暴增
- 2. DDoS/恶意攻击触发大量会话分配
- 四、诊断思路与工具
- 1. 先确认 “占用” 类型:应用堆内存 vs. 系统缓存 vs. 本地内存
- 2. 针对 Java 应用的诊断
- 3. 针对其他语言或系统服务的诊断
- 五、优化建议
- 1. 修复应用层的内存泄漏
- 2. 优化线程/连接池配置
- 3. 控制系统缓存与 Page Cache 占用
- 4. 容器/虚拟化环境优化
- 5. 对抗恶意/异常访问
- } ``` - 如遇到 Crawling 或 DDoS,可结合 WAF(ModSecurity、云厂商 WAF)进行拦截;配合 Fail2Ban 或 Firewall(iptables)封禁恶意 IP。 2. 合理设置 HTTP 超时与最大 Header/Body 大小 - 防止 Slowloris/Slow POST 等攻击,设定 keepalive_timeout、client_header_timeout、client_body_timeout(Nginx)为合理值(如 15s~30s); - 限制单次上传最大 client_max_body_size(例如分片上传可限制在 100MB~200MB),避免单个超大请求导致内存暴增。
- 六、常见案例示范
- 案例 1:分片上传服务内存占用陡增
- 案例 2:系统级 Page Cache 占用过高
- 七、总结
- 📎 参考文章
在服务器上线后,如果长期监控发现内存使用居高不下(如常年保持在 80%~100%),就需要及时排查并优化。下面从应用层面、系统/环境层面和外部/安全因素三个维度,详细分析各类常见场景、诊断思路以及对应的优化建议。#
一、应用层面因素#
1. 内存泄漏(Memory Leak)#
- 什么是内存泄漏? 在 Java/Python/C++ 等语言中,程序持续分配新对象,但由于逻辑漏洞或引用链未断,导致某些对象无法被垃圾回收(GC),随着请求或任务积累,堆内存或其他内存(如本地直接内存、缓冲区)不断被占用,最终触发 OOM(OutOfMemoryError)或交换区频繁使用。
- 典型原因:
- 全局缓存(Cache)/容器未清理
- 使用
static Map、ConcurrentHashMap、Guava Cache 等存储数据时,如果 key 或 value 未及时淘汰,就会随着业务波动不断累积。 - 例如一个在线用户列表缓存,如果使用简单的
HashMap存储用户会话数据,却忘了在用户登出或超时后移除,就会导致缓存不断膨胀。
- 使用
- 监听器/回调机制未解除注册
- 注册了大量的事件监听(如 Kafka 消费者、Spring ApplicationListener、WebSocket Listener 等)后,没有在不需要时
removeListener或关闭会话,导致 Listener 对象常驻内存。
- 注册了大量的事件监听(如 Kafka 消费者、Spring ApplicationListener、WebSocket Listener 等)后,没有在不需要时
- 集合遍历时错误引用
- 直接将集合中元素拷贝到某个全局结构(如
List、Set)后,却从未清空。尤其是在做数据聚合、批量分析后忘了 clear,导致集合大小不断增加。
- 直接将集合中元素拷贝到某个全局结构(如
- 第三方库/本地 JNI 缓冲区
- 某些第三方库(如 Netty、FastJSON、JNA/JNI)会分配直接内存(Direct ByteBuffer)或本地内存。如果使用不当(如忘记调用
ByteBuffer.clear()、release()),就会产生本地内存泄漏,Java 堆看似空闲,实际 native 内存被占满。
- 某些第三方库(如 Netty、FastJSON、JNA/JNI)会分配直接内存(Direct ByteBuffer)或本地内存。如果使用不当(如忘记调用
- 全局缓存(Cache)/容器未清理
排查示例(Java) ```java // 假设有个全局缓存: public class GlobalCache { private static final Map<String, BigObject> CACHE = new ConcurrentHashMap<>();
public static void put(String key, BigObject v) {
CACHE.put(key, v);
}
// ... 但从未提供 remove 或 expire 逻辑
}
// 结果:随着业务不断 put,CACHE 里 BigObject 越来越多,GC 无法回收 ```
2. 高频率、大对象创建与临时对象累积#
- 临时对象过多导致频繁 GC
- 在处理请求时,每次都 new 大量短生命周期对象(如构造大量字符串拼接、创建大量 DTO/Entity),会让 Eden 区迅速充满,频繁触发 Minor GC,进而可能触发 Full GC,带来额外的 CPU 与内存开销。
- 示例:使用
String拼接大量日志、在循环里不断创建new ArrayList<>()、用new BufferedInputStream等不合理的对象分配方式,都可能造成内存抖动。
- 大对象(Large Object)直接进入 Old 区
- 如果某个对象本身占据数 MB(如读取超过一定阈值的图片、视频字节数组),会被直接晋升到老年代(Old Generation),加上后续未及时释放,会导致老年代快速膨胀。
- 示例:在上传/下载业务里,如果一次性把整个大文件读入内存(
byte[] data = Files.readAllBytes(path)),然后对其做哈希计算,结束后没有及时让对象失去引用,就会让 GC 难以回收。
3. 缓存与 Buffer 使用不当#
- 本地缓存(如 Ehcache、Caffeine、Guava Cache)配置不合理
- 如果没有设置缓存的最大容量(
maximumSize)或过期策略(expireAfterWrite、expireAfterAccess),就会无限制地往缓存里 put 数据,导致内存彻底耗尽。
- 如果没有设置缓存的最大容量(
- HTTP/Netty 等缓冲区(Buffer/Channel)未释放
- Netty 的
ByteBuf如果使用PooledByteBufAllocator,需要手动调用release();如果直接使用Unpooled缓冲,也要及时清理。否则,随着请求流量波动,内存会涨得很快。
- Netty 的
- IO 读写流未及时 close
- 如在上传文件时,使用
InputStream、OutputStream读取大文件,如果因异常未在finally里close()流,导致底层缓冲区难以回收。
- 如在上传文件时,使用
4. 缺乏合理的 JVM 参数与垃圾收集器调优#
- 堆大小设置不当
- 如果你给 JVM 设置的堆(Heap)过大(
Xms、Xmx都是 16G 以上),而实际应用只需要几百 MB,那么 GC 周期会相对拉长,堆中垃圾对象积累时,可能让堆一直保持高占用直到达到触发阈值。 - 反之,如果堆设置过小,业务稍微一大波量就频繁触发 Full GC,从而让系统频繁暂停并占用 CPU,甚至触发 OOM。
- 如果你给 JVM 设置的堆(Heap)过大(
- 未按业务类型选择合适 GC
- 对于对暂停时间敏感的在线业务,一般推荐使用 G1 GC 或 ZGC(如果 JDK 支持),它们能更好地控制停顿;若使用老旧的 CMS 或 ParallelGC,有时会导致长时间 Stop-The-World,造成内存占用看似居高不下。
- Metaspace/PermGen 泄漏
- 如果长期动态加载/卸载类(如使用 Spring Boot 热部署、脚本引擎加载),而未及时释放 ClassLoader 引用,会造成 Metaspace(JDK8+)或 PermGen(JDK7 及以下)区域泄漏,最终抛
java.lang.OutOfMemoryError: Metaspace。
- 如果长期动态加载/卸载类(如使用 Spring Boot 热部署、脚本引擎加载),而未及时释放 ClassLoader 引用,会造成 Metaspace(JDK8+)或 PermGen(JDK7 及以下)区域泄漏,最终抛
5. 线程/连接池配置不合理#
- 线程池(ThreadPoolExecutor)或连接池(DB/HTTP 连接池)过大
- 如果业务是 CPU 密集型,而线程数配置成 200+,超出 CPU 核心数太多,会导致每个线程分配栈空间(默认 1MB),配合同步锁/队列,整体占用内存急速攀升。
- 如果使用 C3P0/HikariCP 等数据库连接池,将最大连接数设置得过高(如 200+),那么每个连接对象、Statement 缓冲、ResultSet 等也会占用大量内存。
- 线程阻塞无法释放
- 当线程因 IO(网络、文件)或者锁阻塞时,虽然它不消耗 CPU,但它保留的栈帧和本地变量引用仍在堆上占内存。大量阻塞线程累积,也会让内存被占用而不释放。
二、系统/环境层面因素#
1. 操作系统缓存(Page Cache)与文件系统占用#
- Linux Page Cache
- Linux 会把磁盘文件页缓存到内存(RAM)中,以提高 I/O 性能。当读写大文件(如日志、上传分片、下载合并)时,Page Cache 会迅速填满可用内存,表现为
free、top中内存占用很高,但并不是应用真正的 heap 占用。 - 诊断:
- 使用
free -h看buffers/cache项;或者cat /proc/meminfo,看Cached/Buffers数值。 - 执行
sync && echo 3 > /proc/sys/vm/drop_caches可以临时回收 Page Cache(仅做诊断,不可在生产直接使用,会影响性能)。
- 使用
- Linux 会把磁盘文件页缓存到内存(RAM)中,以提高 I/O 性能。当读写大文件(如日志、上传分片、下载合并)时,Page Cache 会迅速填满可用内存,表现为
- 文件句柄/映射文件(mmap)泄露
- 当应用频繁
mmap()大文件而不munmap(),或者open()句柄没有在close(),就会导致操作系统为这部分映射占用 RAM。 - 诊断:
lsof -p <pid>看打开的文件句柄数;cat /proc/<pid>/maps看内存映射情况。
- 当应用频繁
2. 系统守护进程或安全软件#
- 日志轮转/压缩/备份进程
- 系统中如果有
logrotate、rsync、tar/zip等定时任务压缩或备份海量日志,会短时间把结果加载到内存或触发大量磁盘/网络 I/O,进而消耗一部分内存作缓冲。
- 系统中如果有
- 安全扫描/防病毒进程
- 在 Linux 上跑的
clamav、aide、tripwire、auditd等安全审计、病毒扫描工具,会扫描文件时把文件内容加载到内存,可能导致内存被占满。一旦扫描完成,内存会释放,但在扫描高峰期,你会看到系统内存被占用很高。
- 在 Linux 上跑的
3. 虚拟化/容器环境内存隔离失效#
- Docker 容器未限制内存
- 如果你把 Spring Boot 应用打成 Docker 镜像,而在运行时没有通过
-memory、-memory-swap等参数对容器进行限制,容器中进程会一直占用宿主机的可用内存,直到宿主机物理内存耗尽并触发 OOM。
- 如果你把 Spring Boot 应用打成 Docker 镜像,而在运行时没有通过
- Kubernetes Pod 内存请求/限制设置不合理
- 在 Kubernetes 中,如果给 Pod 的
requests.memory和limits.memory没有做好预估,或者两者相差过大,可能导致 Pod 在内存需求增加时占用节点过多内存,但节点无法调度其他 Pod,最终发生 OOM。
- 在 Kubernetes 中,如果给 Pod 的
4. 内核参数或驱动问题#
- 虚拟内存过度交换(Swap)
- 当系统物理内存不足时,会把一部分内存页换到 Swap 分区。此时你在
free或top里看到used很高,但实际应用占用并没那么大,是 Swap 被占用。 - 如果 Swap 太小或者被 Swap 的速度过快,还会导致系统频繁进行页面换进换出,进而影响整体性能。
- 当系统物理内存不足时,会把一部分内存页换到 Swap 分区。此时你在
- 内核 Bug/驱动缺陷
- 少见但存在:某些网卡/存储驱动在高负载场景下会出现内存泄漏(如中断风暴导致内核任务堆积),或者内核模块 Bug 导致申请的 slab 缓存无法释放。
- 诊断:
- 通过
slabtop查看内核 slab 分配情况;dmesg、/var/log/kern.log查看内核报错日志。
- 通过
三、外部/安全因素#
1. 短期流量暴涨导致缓存暴增#
- 爬虫/镜像站无节制抓取
- 大规模爬虫短时间内访问大量资源,会让应用层对每个请求生成或更新缓存(如查询结果缓存、图片缩略图、临时会话),并且很可能带着大量 Cookie/Header 导致缓存失效率上升,最终占满可用内存。
- 刷量脚本/机器人访问
- 当某些脚本对接口持续发 GET/POST 请求,触发后台临时数据结构(如会话缓存、验证码列表)频繁新增而不清理,导致内存迅速被占满。
2. DDoS/恶意攻击触发大量会话分配#
- 半开连接攻击(SYN Flood)
- 网络层面发送大量 SYN 包让服务器陷入半开连接,此时系统内核会为每个连接分配内存结构(如
struct sock等),导致内核态内存占满;同时应用层也会收到大量连接请求,分配线程或协程,占用堆栈内存。
- 网络层面发送大量 SYN 包让服务器陷入半开连接,此时系统内核会为每个连接分配内存结构(如
- HTTP Flood 攻击
- 模拟合法请求访问带有大文件上传/下载接口,使后端不断为每个请求分配缓存、缓冲区、会话,内存被耗尽。
- 资源耗尽(Slowloris)
- 持续慢速发送 HTTP header,导致服务器保持大量半开 HTTP 会话,占用 HTTP 服务线程池,甚至导致线程池内存膨胀。
四、诊断思路与工具#
当监控报警提示“内存使用率 ≥ 80%”或应用开始出现 OutOfMemoryError 时,需要尽快定位内存究竟被谁或哪个环节占满。下面提供一套常见的诊断流程及对应工具:
1. 先确认 “占用” 类型:应用堆内存 vs. 系统缓存 vs. 本地内存#
- 查看系统整体内存情况
free -h
top -o %MEM
```
- free -h 可以让你看到 used、free、shared、buff/cache、available。
- 如果 buff/cache 较大,说明系统 Page Cache 占用高,不一定是应用泄漏。
- 使用 top -o %MEM 可以看到哪个进程占用内存最多,重点关注 Java 进程(java)或你的应用名。
- 查看进程实际虚拟内存(VSZ) vs. 常驻内存(RSS)
ps aux --sort -rss | head -n 10
```
- VSZ(虚拟内存大小) 表示进程地址空间总量,包括 swap、映射到文件等;
- RSS(常驻内存集)才是实际驻留在物理 RAM 中的大小。
- 如果仅仅是 VSZ 高,而 RSS 适中,可能是映射内存或交换分区导致。
2. 针对 Java 应用的诊断#
- 查看 JVM 配置
- 通过
ps -ef | grep java,检查启动脚本里Xms、Xmx、XX:MetaspaceSize、XX:MaxMetaspaceSize、XX:MaxDirectMemorySize等参数,确认堆内存和元空间大小是否合理。
- 通过
- 实时监控 GC/内存使用情况
- 启动时添加
Xlog:gc*(JDK11+)或verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log,观察 GC 周期、停顿时间、堆使用峰值。 - 使用
jstat -gc <pid> 1000实时查看各个代的使用情况和 GC 次数。
- 启动时添加
- 导出堆快照 (Heap Dump)
- 当怀疑内存泄漏时,可在出现 OOM 前/后执行:
- 当怀疑内存泄漏时,可在出现 OOM 前/后执行:
jmap -dump:live,format=b,file=heapdump.hprof
```
- 或者在 JVM 参数里加上 XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/,当 OOM 发生时自动生成 .hprof 文件。
- 使用 Eclipse MAT (Memory Analyzer Tool)、VisualVM 或 YourKit 等工具打开 .hprof,查看 Dominators Tree,找出最大占用对象、泄漏 Suspects,分析引用链。
4. 线程堆栈结合内存查看
- 如果怀疑某些线程持续创建临时对象导致老年代膨胀,可以:
1. 用 jstack <pid> > threaddump.txt 导出线程堆栈;
2. 在 VisualVM 里监控各线程的 CPU/内存分配情况,注意哪些线程在不断分配新对象。
5. 监测 Metaspace/PermGen
- 若出现 java.lang.OutOfMemoryError: Metaspace,需要检查是否存在动态类加载、Spring 热部署未释放 ClassLoader、Spring XML 文件频繁重启等问题。
3. 针对其他语言或系统服务的诊断#
- Python/Node.js 服务
- 使用
psutil(Python)或process.memoryUsage()(Node.js)监测每个请求内存分配; - 借助
objgraph(Python)或heapdump(Node.js)生成内存快照,找出泄漏对象。
- 使用
- 操作系统级别检查
slabtop:查看内核 slab 缓存的使用情况,判断是否存在内核内存泄漏(例如某个 slab 占用过多)。dmesg、/var/log/messages:查看内核报错,如 OOM-Killer 杀死进程记录、磁盘 I/O 错误、驱动相关警告。smem:展示每个进程的 Proportional Set Size(PSS),更准确反映共享库占用,有助于判断真实内存占用。
- 容器/虚拟化环境
- Docker:
docker stats可以查看每个容器的内存使用量; - Kubernetes:通过
kubectl top pod、kubectl describe pod查看 Pod 的Memory Usage、Memory Requests/Limits,并留意是否有 OOMKill 事件。
- Docker:
五、优化建议#
1. 修复应用层的内存泄漏#
- 对缓存、Map、集合设置容量上限与过期策略
- 对于 Guava Cache 或 Caffeine Cache,一定要设置
maximumSize和expireAfterWrite/expireAfterAccess:
- 对于 Guava Cache 或 Caffeine Cache,一定要设置
Cache<String, BigObject> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterAccess(Duration.ofMinutes(30))
.build();
- 对于自己维护的 `Map`,可定期调度清理任务,检查过期数据并删除;或者使用 `WeakHashMap`、`ConcurrentLinkedHashMap` 之类对 key/value 引用进行弱引用/软引用处理。 2. **关闭不再使用的监听器与资源** - 若使用 Spring `ApplicationListener`,在不需要时调用 `removeApplicationListener`; - WebSocket、Kafka 消费者等需在应用退出或会话结束时 `close()` 或 `unsubscribe()`。 3. **避免不必要的大对象一次性加载** - 对于大文件操作,推荐使用 **流式处理**: java
try (InputStream in = new FileInputStream(bigFile);
DigestInputStream dis = new DigestInputStream(in, MessageDigest.getInstance("SHA-256"))) {
byte[] buffer = new byte[8192];
while (dis.read(buffer) != -1) {
// 逐块计算哈希
}
}
```
- 避免 Files.readAllBytes() 这类一次性把所有字节加载到堆中的方式。
4. 减少临时对象的创建
- 对于大批量日志拼接,尽量用 StringBuilder 并重用;
- 循环中使用同一个 byte[] 缓存,避免不断 new byte[4096];
- 用 对象池/缓冲池(如 Netty 的 ByteBuf 池化、数据库连接池)来复用对象。
5. 合理配置 JVM 及 GC 策略
- 根据业务需求设置 Xms、Xmx,使堆大小与实际负载匹配,不要过度留白;
- 若线上业务对 GC 停顿敏感,可选用 G1 GC(XX:+UseG1GC),并结合 XX:MaxGCPauseMillis=200 等参数来控制停顿。
- 监控 Metaspace 大小,如发现持续增长,则检查是否存在类加载/卸载不平衡。
2. 优化线程/连接池配置#
- 合理设置线程池大小
- CPU 密集型任务: 线程数 ≈ CPU 核心数 + 1;
- IO 密集型任务: 线程数 ≈ CPU 核心数 × (1 + 阻塞系数),阻塞系数可根据 RTT 或 I/O 等待时间与计算时间比来估算;
- 使用 Spring Boot 的
ThreadPoolTaskExecutor,在application.yml中做如下示例配置:
spring:
task:
execution:
pool:
core-size: 8
max-size: 16
queue-capacity: 500
keep-alive: 60s
```
2. 数据库连接池配置优化
- 对于 HikariCP,maximumPoolSize 不宜远超 CPU 核心数 × (1 + 阻塞系数),否则每个连接都会占用一定本地缓存和内存;
- 开启连接池监控(如 HikariCP 的 metricTrackerFactory)观察空闲/活跃连接数,避免连接过度膨胀。
3. 控制系统缓存与 Page Cache 占用#
- **调整
vm.swappiness和 **vm.vfs_cache_pressure- 将
vm.swappiness调低(如10~20)可以降低内核倾向于把数据 swap 到磁盘; - 将
vm.vfs_cache_pressure调低(如50),让内核尽量保留文件系统缓存,减少频繁释放 Page Cache 的开销。
- 将
sysctl -w vm.swappiness=20
sysctl -w vm.vfs_cache_pressure=50
```
2. 定期清理不必要的日志与临时文件
- 对 /var/log、/tmp 目录实施 logrotate 和 定期清理,避免过多历史日志文件占满磁盘缓存。
- 在磁盘 I/O 高峰完成后,可在维护窗口执行一次 Page Cache 清理(echo 3 > /proc/sys/vm/drop_caches),但要注意这会短暂影响文件系统性能,须谨慎操作。
4. 容器/虚拟化环境优化#
- 对 Docker 容器设置内存限制
- 在运行容器时加参数:
- 在运行容器时加参数:
docker run -d
--name myapp
--memory=4g
--memory-swap=4g
myapp-image
- 这样即使应用出现内存泄漏,也只会在容器内触发 OOMKill,不至于影响宿主机上的其他服务。 2. **Kubernetes Pod 资源配置** - 在 `Deployment`/`StatefulSet` 中为 Pod 设置合适的 `resources.requests.memory` 和 `resources.limits.memory`,比如: yaml
resources:
requests:
memory: "2Gi"
limits:
memory: "4Gi"
```
- 如果 Pod 连续多次触发 OOMKill,则说明应用内存泄漏或内存需求比预估要高,需要进一步排查。
5. 对抗恶意/异常访问#
- Nginx/负载均衡层限流与防护
- 在 Nginx 加入
limit_conn_zone、limit_req_zone防止同一 IP/业务方瞬时过大连接数或请求数:
- 在 Nginx 加入
http { limit_req_zone binaryremoteaddrzone=reqlimit:10mrate=20r/s;limitconnzonebinary_remote_addr zone=conn_limit:10m;
server {
...
location /upload {
limit_req zone=req_limit burst=50 nodelay;
limit_conn conn_limit 10;
proxy_pass http://backend;
}
}
}
```
- 如遇到 Crawling 或 DDoS,可结合 WAF(ModSecurity、云厂商 WAF)进行拦截;配合 Fail2Ban 或 Firewall(iptables)封禁恶意 IP。
2. 合理设置 HTTP 超时与最大 Header/Body 大小
- 防止 Slowloris/Slow POST 等攻击,设定 keepalive_timeout、client_header_timeout、client_body_timeout(Nginx)为合理值(如 15s~30s);
- 限制单次上传最大 client_max_body_size(例如分片上传可限制在 100MB~200MB),避免单个超大请求导致内存暴增。#
六、常见案例示范#
案例 1:分片上传服务内存占用陡增#
现象:分片上传高峰期,服务器内存使用从 30%→90%,24 小时后 OOM 并重启。 排查步骤:
top发现java进程占用内存飙升。jmap -heap <pid>发现 Eden、Old 区持续增长,Full GC 无法回收大量对象。- 导出
heapdump.hprof,用 MAT 分析后发现:- 一个
ConcurrentHashMap<String, UploadSession>引用栈非常高,占用大量老年代。 UploadSession对象内维护了一个List<ChunkInfo>,但由于设计缺陷,在分片完成后没有清空 List,而 List 持续保留了byte[]缓冲,导致对象不能被 GC。 优化方案:
- 一个
- 在分片合并完成后,立即调用
sessionChunks.clear(),并将UploadSession对象从缓存中移除。 - 将
ChunkInfo中的byte[]缓冲改为仅创建FileChannel映射,合并后立即关闭。 - 对
UploadSession缓存容量做上限限制,例如最多保留 10,000 条会话,超过则使用 LRU 淘汰策略。
案例 2:系统级 Page Cache 占用过高#
现象:后台日志突然变慢,free -h 显示 buffers/cache 占用 80%,可用内存很少。
排查步骤:
free -h:显示Mem: 32G total, 2G free, 28G buffers/cache。iotop查看读写 I/O,发现正在做大规模备份(tar czf /backup/logs_*.tar.gz /var/log/*)。- 使用
echo 3 > /proc/sys/vm/drop_caches后,可用内存马上从 2G→20G,但性能暂时下降。 优化方案: - 将备份脚本调整到半夜低峰期运行;使用
ionice -c2 -n7降低 I/O 优先级。 - 在备份时加
-one-file-system限制范围,避免一次性打包过多小文件。 - 如果备份频率高,考虑用增量备份(
rsync、tar --listed-incremental)减少对 Page Cache 的冲击。
七、总结#
- 应用层面:内存泄漏、缓存/集合无限制扩张、大对象一次性加载、临时对象过多、线程/连接池配置不合理、JVM 参数及 GC 策略不当,都是最常见的“Top 内存杀手”。
- 系统/环境层面:Linux Page Cache、kernel slab、日志轮转、备份/压缩、驱动 Bug、容器未限制内存,都可能让“可用内存”瞬间被占满。
- 外部/恶意因素:爬虫刷取、DDoS 攻击、Slowloris、堡垒机扫描等恶意流量会触发大量会话分配,占用应用和内核内存。
- 诊断工具:
- 应用级:
jmap、jstat、jconsole、VisualVM、MAT、YourKit等; - 系统级:
free、top、ps aux、slabtop、smem、lsof、iotop、dmesg、lxc-destroy/docker stats; - 容器/K8s:
docker stats、kubectl top pods、kubectl describe pod→ 看OOMKilled; - 网络流量:
iftop、nload、tcpdump→ 判断是否有洪水攻击。
- 应用级:
- 优化措施:
- 修复内存泄漏:对缓存加限、清理不再使用的全局引用、关闭无用监听器、避免直接加载大对象、使用流式处理。
- 调整 JVM/GC:合理设置堆/Metaspace/Direct Memory,大对象外推;选适合 G1/ZGC;监测 GC 日志并持续优化。
- 配置线程与连接池:线程数与 CPU 及 I/O 需求匹配;数据库连接池设置合理大小;避免大量阻塞线程。
- 控制系统缓存:合理调节
vm.swappiness、vfs_cache_pressure,并非所有内存占用都代表内存泄漏,要区分 Page Cache;定时清理日志与临时文件。 - 容器/虚拟化限制:通过 Docker/K8s 限制内存,防止单个容器/Pod 抢占宿主机内存。
- 防护恶意流量:Nginx 层限流、WAF 拦截、iptables 黑名单,防止爬虫、DDoS、Slowloris 等攻击迅速耗尽内存。 只要掌握 “应用看堆,系统看缓存,外部留警惕” 的思路,并结合日常监控(如 Prometheus+Grafana、ELK 警报),定期做内存快照与审计,就能在“内存使用过高”问题出现前,提前预警并做出优化,确保系统始终运行在一个健康、可控的状态。