返回文章列表

文章

分析服务器内存占用过高

从应用层面、系统/环境层面和外部/安全因素三个维度,详细分析各类常见场景、诊断思路以及对应的优化建议

目录
  1. 在服务器上线后,如果长期监控发现内存使用居高不下(如常年保持在 80%~100%),就需要及时排查并优化。下面从应用层面、系统/环境层面和外部/安全因素三个维度,详细分析各类常见场景、诊断思路以及对应的优化建议。
  2. 一、应用层面因素
  3. 1. 内存泄漏(Memory Leak)
  4. 2. 高频率、大对象创建与临时对象累积
  5. 3. 缓存与 Buffer 使用不当
  6. 4. 缺乏合理的 JVM 参数与垃圾收集器调优
  7. 5. 线程/连接池配置不合理
  8. 二、系统/环境层面因素
  9. 1. 操作系统缓存(Page Cache)与文件系统占用
  10. 2. 系统守护进程或安全软件
  11. 3. 虚拟化/容器环境内存隔离失效
  12. 4. 内核参数或驱动问题
  13. 三、外部/安全因素
  14. 1. 短期流量暴涨导致缓存暴增
  15. 2. DDoS/恶意攻击触发大量会话分配
  16. 四、诊断思路与工具
  17. 1. 先确认 “占用” 类型:应用堆内存 vs. 系统缓存 vs. 本地内存
  18. 2. 针对 Java 应用的诊断
  19. 3. 针对其他语言或系统服务的诊断
  20. 五、优化建议
  21. 1. 修复应用层的内存泄漏
  22. 2. 优化线程/连接池配置
  23. 3. 控制系统缓存与 Page Cache 占用
  24. 4. 容器/虚拟化环境优化
  25. 5. 对抗恶意/异常访问
  26. } ``` - 如遇到 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),避免单个超大请求导致内存暴增。
  27. 六、常见案例示范
  28. 案例 1:分片上传服务内存占用陡增
  29. 案例 2:系统级 Page Cache 占用过高
  30. 七、总结
  31. 📎 参考文章

在服务器上线后,如果长期监控发现内存使用居高不下(如常年保持在 80%~100%),就需要及时排查并优化。下面从应用层面系统/环境层面外部/安全因素三个维度,详细分析各类常见场景、诊断思路以及对应的优化建议。#

一、应用层面因素#

1. 内存泄漏(Memory Leak)#

  • 什么是内存泄漏? 在 Java/Python/C++ 等语言中,程序持续分配新对象,但由于逻辑漏洞或引用链未断,导致某些对象无法被垃圾回收(GC),随着请求或任务积累,堆内存或其他内存(如本地直接内存、缓冲区)不断被占用,最终触发 OOM(OutOfMemoryError)或交换区频繁使用。
  • 典型原因:
    1. 全局缓存(Cache)/容器未清理
      • 使用 static MapConcurrentHashMap、Guava Cache 等存储数据时,如果 key 或 value 未及时淘汰,就会随着业务波动不断累积。
      • 例如一个在线用户列表缓存,如果使用简单的 HashMap 存储用户会话数据,却忘了在用户登出或超时后移除,就会导致缓存不断膨胀。
    2. 监听器/回调机制未解除注册
      • 注册了大量的事件监听(如 Kafka 消费者、Spring ApplicationListener、WebSocket Listener 等)后,没有在不需要时 removeListener 或关闭会话,导致 Listener 对象常驻内存。
    3. 集合遍历时错误引用
      • 直接将集合中元素拷贝到某个全局结构(如 ListSet)后,却从未清空。尤其是在做数据聚合、批量分析后忘了 clear,导致集合大小不断增加。
    4. 第三方库/本地 JNI 缓冲区
      • 某些第三方库(如 Netty、FastJSON、JNA/JNI)会分配直接内存(Direct ByteBuffer)或本地内存。如果使用不当(如忘记调用 ByteBuffer.clear()release()),就会产生本地内存泄漏,Java 堆看似空闲,实际 native 内存被占满。

排查示例(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)或过期策略expireAfterWriteexpireAfterAccess),就会无限制地往缓存里 put 数据,导致内存彻底耗尽。
  • HTTP/Netty 等缓冲区(Buffer/Channel)未释放
    • Netty 的 ByteBuf 如果使用 PooledByteBufAllocator,需要手动调用 release();如果直接使用 Unpooled 缓冲,也要及时清理。否则,随着请求流量波动,内存会涨得很快。
  • IO 读写流未及时 close
    • 如在上传文件时,使用 InputStreamOutputStream 读取大文件,如果因异常未在 finallyclose() 流,导致底层缓冲区难以回收。

4. 缺乏合理的 JVM 参数与垃圾收集器调优#

  • 堆大小设置不当
    • 如果你给 JVM 设置的堆(Heap)过大(XmsXmx 都是 16G 以上),而实际应用只需要几百 MB,那么 GC 周期会相对拉长,堆中垃圾对象积累时,可能让堆一直保持高占用直到达到触发阈值。
    • 反之,如果堆设置过小,业务稍微一大波量就频繁触发 Full GC,从而让系统频繁暂停并占用 CPU,甚至触发 OOM。
  • 未按业务类型选择合适 GC
    • 对于对暂停时间敏感的在线业务,一般推荐使用 G1 GCZGC(如果 JDK 支持),它们能更好地控制停顿;若使用老旧的 CMS 或 ParallelGC,有时会导致长时间 Stop-The-World,造成内存占用看似居高不下。
  • Metaspace/PermGen 泄漏
    • 如果长期动态加载/卸载类(如使用 Spring Boot 热部署、脚本引擎加载),而未及时释放 ClassLoader 引用,会造成 Metaspace(JDK8+)或 PermGen(JDK7 及以下)区域泄漏,最终抛 java.lang.OutOfMemoryError: Metaspace

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 会迅速填满可用内存,表现为 freetop 中内存占用很高,但并不是应用真正的 heap 占用。
    • 诊断:
      • 使用 free -hbuffers/cache 项;或者 cat /proc/meminfo,看 Cached/Buffers 数值。
      • 执行 sync && echo 3 > /proc/sys/vm/drop_caches 可以临时回收 Page Cache(仅做诊断,不可在生产直接使用,会影响性能)。
  • 文件句柄/映射文件(mmap)泄露
    • 当应用频繁 mmap() 大文件而不 munmap(),或者 open() 句柄没有在 close(),就会导致操作系统为这部分映射占用 RAM。
    • 诊断:
      • lsof -p <pid> 看打开的文件句柄数;cat /proc/<pid>/maps 看内存映射情况。

2. 系统守护进程或安全软件#

  • 日志轮转/压缩/备份进程
    • 系统中如果有 logrotatersynctar/zip 等定时任务压缩或备份海量日志,会短时间把结果加载到内存或触发大量磁盘/网络 I/O,进而消耗一部分内存作缓冲。
  • 安全扫描/防病毒进程
    • 在 Linux 上跑的 clamavaidetripwireauditd 等安全审计、病毒扫描工具,会扫描文件时把文件内容加载到内存,可能导致内存被占满。一旦扫描完成,内存会释放,但在扫描高峰期,你会看到系统内存被占用很高。

3. 虚拟化/容器环境内存隔离失效#

  • Docker 容器未限制内存
    • 如果你把 Spring Boot 应用打成 Docker 镜像,而在运行时没有通过 -memory-memory-swap 等参数对容器进行限制,容器中进程会一直占用宿主机的可用内存,直到宿主机物理内存耗尽并触发 OOM。
  • Kubernetes Pod 内存请求/限制设置不合理
    • 在 Kubernetes 中,如果给 Pod 的 requests.memorylimits.memory 没有做好预估,或者两者相差过大,可能导致 Pod 在内存需求增加时占用节点过多内存,但节点无法调度其他 Pod,最终发生 OOM。

4. 内核参数或驱动问题#

  • 虚拟内存过度交换(Swap)
    • 当系统物理内存不足时,会把一部分内存页换到 Swap 分区。此时你在 freetop 里看到 used 很高,但实际应用占用并没那么大,是 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 等),导致内核态内存占满;同时应用层也会收到大量连接请求,分配线程或协程,占用堆栈内存。
  • HTTP Flood 攻击
    • 模拟合法请求访问带有大文件上传/下载接口,使后端不断为每个请求分配缓存、缓冲区、会话,内存被耗尽。
  • 资源耗尽(Slowloris)
    • 持续慢速发送 HTTP header,导致服务器保持大量半开 HTTP 会话,占用 HTTP 服务线程池,甚至导致线程池内存膨胀。

四、诊断思路与工具#

当监控报警提示“内存使用率 ≥ 80%”或应用开始出现 OutOfMemoryError 时,需要尽快定位内存究竟被谁或哪个环节占满。下面提供一套常见的诊断流程及对应工具:

1. 先确认 “占用” 类型:应用堆内存 vs. 系统缓存 vs. 本地内存#

  • 查看系统整体内存情况

free -h top -o %MEM ``` - free -h 可以让你看到 usedfreesharedbuff/cacheavailable。 - 如果 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 应用的诊断#

  1. 查看 JVM 配置
    • 通过 ps -ef | grep java,检查启动脚本里 XmsXmxXX:MetaspaceSizeXX:MaxMetaspaceSizeXX:MaxDirectMemorySize 等参数,确认堆内存和元空间大小是否合理。
  2. 实时监控 GC/内存使用情况
    • 启动时添加 Xlog:gc*(JDK11+)或 verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log,观察 GC 周期、停顿时间、堆使用峰值。
    • 使用 jstat -gc <pid> 1000 实时查看各个代的使用情况和 GC 次数。
  3. 导出堆快照 (Heap Dump)
    • 当怀疑内存泄漏时,可在出现 OOM 前/后执行:

jmap -dump:live,format=b,file=heapdump.hprof ``` - 或者在 JVM 参数里加上 XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/,当 OOM 发生时自动生成 .hprof 文件。 - 使用 Eclipse MAT (Memory Analyzer Tool)VisualVMYourKit 等工具打开 .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),更准确反映共享库占用,有助于判断真实内存占用。
  • 容器/虚拟化环境
    • Dockerdocker stats 可以查看每个容器的内存使用量;
    • Kubernetes:通过 kubectl top podkubectl describe pod 查看 Pod 的 Memory UsageMemory Requests/Limits,并留意是否有 OOMKill 事件。

五、优化建议#

1. 修复应用层的内存泄漏#

  1. 对缓存、Map、集合设置容量上限与过期策略
    • 对于 Guava Cache 或 Caffeine Cache,一定要设置 maximumSizeexpireAfterWrite/expireAfterAccess

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 策略 - 根据业务需求设置 XmsXmx,使堆大小与实际负载匹配,不要过度留白; - 若线上业务对 GC 停顿敏感,可选用 G1 GCXX:+UseG1GC),并结合 XX:MaxGCPauseMillis=200 等参数来控制停顿。 - 监控 Metaspace 大小,如发现持续增长,则检查是否存在类加载/卸载不平衡。

2. 优化线程/连接池配置#

  1. 合理设置线程池大小
    • 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 占用#

  1. **调整 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. 容器/虚拟化环境优化#

  1. 对 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. 对抗恶意/异常访问#

  1. Nginx/负载均衡层限流与防护
    • 在 Nginx 加入 limit_conn_zonelimit_req_zone 防止同一 IP/业务方瞬时过大连接数或请求数:

http { limit_req_zone binaryremoteaddrzone=reqlimit:10mrate=20r/s;limitconnzonebinary_remote_addr zone=req_limit:10m rate=20r/s; limit_conn_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_timeoutclient_header_timeoutclient_body_timeout(Nginx)为合理值(如 15s~30s); - 限制单次上传最大 client_max_body_size(例如分片上传可限制在 100MB~200MB),避免单个超大请求导致内存暴增。#

六、常见案例示范#

案例 1:分片上传服务内存占用陡增#

现象:分片上传高峰期,服务器内存使用从 30%→90%,24 小时后 OOM 并重启。 排查步骤

  1. top 发现 java 进程占用内存飙升。
  2. jmap -heap <pid> 发现 Eden、Old 区持续增长,Full GC 无法回收大量对象。
  3. 导出 heapdump.hprof,用 MAT 分析后发现:
    • 一个 ConcurrentHashMap<String, UploadSession> 引用栈非常高,占用大量老年代。
    • UploadSession 对象内维护了一个 List<ChunkInfo>,但由于设计缺陷,在分片完成后没有清空 List,而 List 持续保留了 byte[] 缓冲,导致对象不能被 GC。 优化方案
  4. 在分片合并完成后,立即调用 sessionChunks.clear(),并将 UploadSession 对象从缓存中移除。
  5. ChunkInfo 中的 byte[] 缓冲改为仅创建 FileChannel 映射,合并后立即关闭。
  6. UploadSession 缓存容量做上限限制,例如最多保留 10,000 条会话,超过则使用 LRU 淘汰策略。

案例 2:系统级 Page Cache 占用过高#

现象:后台日志突然变慢,free -h 显示 buffers/cache 占用 80%,可用内存很少。 排查步骤

  1. free -h:显示 Mem: 32G total, 2G free, 28G buffers/cache
  2. iotop 查看读写 I/O,发现正在做大规模备份(tar czf /backup/logs_*.tar.gz /var/log/*)。
  3. 使用 echo 3 > /proc/sys/vm/drop_caches 后,可用内存马上从 2G→20G,但性能暂时下降。 优化方案
  4. 将备份脚本调整到半夜低峰期运行;使用 ionice -c2 -n7 降低 I/O 优先级。
  5. 在备份时加 -one-file-system 限制范围,避免一次性打包过多小文件。
  6. 如果备份频率高,考虑用增量备份(rsynctar --listed-incremental)减少对 Page Cache 的冲击。

七、总结#

  • 应用层面:内存泄漏、缓存/集合无限制扩张、大对象一次性加载、临时对象过多、线程/连接池配置不合理、JVM 参数及 GC 策略不当,都是最常见的“Top 内存杀手”。
  • 系统/环境层面:Linux Page Cache、kernel slab、日志轮转、备份/压缩、驱动 Bug、容器未限制内存,都可能让“可用内存”瞬间被占满。
  • 外部/恶意因素:爬虫刷取、DDoS 攻击、Slowloris、堡垒机扫描等恶意流量会触发大量会话分配,占用应用和内核内存。
  • 诊断工具
    • 应用级jmapjstatjconsoleVisualVMMATYourKit 等;
    • 系统级freetopps auxslabtopsmemlsofiotopdmesglxc-destroy/docker stats
    • 容器/K8sdocker statskubectl top podskubectl describe pod → 看 OOMKilled
    • 网络流量iftopnloadtcpdump → 判断是否有洪水攻击。
  • 优化措施
    1. 修复内存泄漏:对缓存加限、清理不再使用的全局引用、关闭无用监听器、避免直接加载大对象、使用流式处理。
    2. 调整 JVM/GC:合理设置堆/Metaspace/Direct Memory,大对象外推;选适合 G1/ZGC;监测 GC 日志并持续优化。
    3. 配置线程与连接池:线程数与 CPU 及 I/O 需求匹配;数据库连接池设置合理大小;避免大量阻塞线程。
    4. 控制系统缓存:合理调节 vm.swappinessvfs_cache_pressure,并非所有内存占用都代表内存泄漏,要区分 Page Cache;定时清理日志与临时文件。
    5. 容器/虚拟化限制:通过 Docker/K8s 限制内存,防止单个容器/Pod 抢占宿主机内存。
    6. 防护恶意流量:Nginx 层限流、WAF 拦截、iptables 黑名单,防止爬虫、DDoS、Slowloris 等攻击迅速耗尽内存。 只要掌握 “应用看堆,系统看缓存,外部留警惕” 的思路,并结合日常监控(如 Prometheus+Grafana、ELK 警报),定期做内存快照与审计,就能在“内存使用过高”问题出现前,提前预警并做出优化,确保系统始终运行在一个健康、可控的状态。

📎 参考文章#