杰瑞科技汇

linux 内存占用 Java

如何查看 Java 内存占用?(top, ps, jstat 等)

当发现系统内存紧张时,第一步是定位是哪个 Java 进程占用了内存。

linux 内存占用 Java-图1
(图片来源网络,侵删)

使用 tophtop (快速概览)

top 是最常用的系统监控工具。

# 按内存使用率排序 (M)
top -M

或者使用更友好的 htop (如果已安装):

htop
# 然后按 F6 选择 "MEM %" 列进行排序

你会看到类似下面的输出:

PID USER      PRI  NI  VIRT   RES   SHR S  CPU%  MEM%   TIME+  COMMAND
12345 java      20   0  4.5g 1.8g  256m R  10.2  18.5  120:34 java

这里的关键列是:

linux 内存占用 Java-图2
(图片来源网络,侵删)
  • VIRT (Virtual Memory - 虚拟内存): 进程总共能访问的虚拟内存大小,它包括了实际使用的内存 (RES)、已分配但未使用的内存、共享库、代码段、数据段等。这个值通常比实际物理占用大很多,不能直接用来判断内存压力。
  • RES (Resident Memory - 常驻内存): 这是进程当前实际占用的物理内存大小,这是衡量一个进程对物理内存消耗最直接的指标。我们通常关心的是这个值。
  • SHR (Shared Memory - 共享内存): 进程与其他进程共享的内存大小(共享的代码段)。

top/htop 中,关注 RES 列,它代表了 Java 进程正在“吃掉”你多少物理内存。

使用 ps (获取精确信息)

ps 命令可以提供更精确的内存信息。

# -o 指定输出格式
# rss: Resident Set Size, 以 KB 为单位
# vsz: Virtual Memory Size, 以 KB 为单位
ps -o pid,ppid,user,cmd,rss,vsz -C java

使用 jstat (JVM 内部视图) - 最关键的工具

jstat 是 JDK 自带的工具,可以实时监控 JVM 内部的内存分配情况,这是理解 Java 内存占用的核心。

# jstat -gc <pid> <interval_in_ms> <count>
# 示例:每 1 秒查看一次 PID 为 12345 的 JVM 的 GC 情况,共 10 次
jstat -gc 12345 1000 10

输出示例:

linux 内存占用 Java-图3
(图片来源网络,侵删)
 S0C    S1C    ...   EC      OC       ...    M     CCSMSC   ...   YGC     FGC    FGCT     GCT    ...
512.0   512.0  ...  4096.0  8192.0   ...   45.59  4.1968  ...   150     2      0.150   1.200
512.0   512.0  ...  4096.0  8192.0   ...   45.59  4.1968  ...   150     2      0.150   1.200
...

关键列解释

  • 堆内存区域:
    • S0C / S1C: Survivor 0 / 1 区的 容量
    • EC: Eden 区的 容量
    • OC: Old 区的 容量
    • OU: Old 区的 已使用量这个值如果持续接近 OC,说明 Old 区即将满,可能会触发 Full GC。
  • 元空间:
    • MC: Metaspace 的 容量
    • MU: Metaspace 的 已使用量
  • GC 统计:
    • YGC: Young GC 次数。
    • FGC: Full GC 次数。
    • GCT: GC 总耗时。
    • FGCT: Full GC 总耗时。FGCT 时间持续增长,说明 Full GC 频繁发生,这是性能的杀手。

使用 jmap (生成堆转储)

当怀疑内存泄漏时,jmap 是你的利器,它可以生成 Java 堆的快照文件(.hprof),然后用工具(如 Eclipse MAT, VisualVM)分析,找出是哪些对象占用了内存。

# 生成堆转储文件
jmap -dump:format=b,file=heapdump.hprof <pid>

注意: 生成堆转dump是一个重量级操作,会使 JVM 停顿,请谨慎在生产环境使用,最好在业务低峰期执行。


为什么 Java 内存占用高?

理解了如何查看,接下来就是分析原因,Java 内存占用高通常由以下几个原因造成:

正常的内存使用(最常见)

  • 应用负载高: 服务器处理的请求多了,需要创建更多的对象来处理请求,内存自然就高了,这是最正常的情况。
  • 缓存: 很多应用会使用缓存(如 Guava Cache, Caffeine, Redis 客户端缓存)来提升性能,缓存的数据会占用大量堆内存,如果缓存配置过大,就会导致内存占用高。

JVM 自身开销

  • 元空间: 在 Java 8 及以后,元空间取代了永久代,元空间使用的是本地内存,而不是堆内存,如果加载的类非常多(使用了大量反射、动态代理、或大量第三方库),元空间可能会占用很大。
    • 检查: 使用 jstat -gcutil <pid> 查看 MU (Metaspace Used) 和 MC (Metaspace Capacity)。
  • 线程栈: 每个线程都有一个独立的栈内存,默认情况下,每个线程栈大小为 1MB (-Xss1m),如果你的应用线程数很多(几百个),这部分加起来也会很可观。
    • 计算: 线程数 * 线程栈大小。

内存泄漏(严重问题)

内存泄漏指程序中已不再使用的对象,由于仍然被其他对象(例如静态集合、线程等)引用,导致 GC 无法回收它们,这些对象会一直堆积在内存中,最终导致 OutOfMemoryError。

  • 常见场景:
    • 静态集合: 最经典的泄漏,一个静态的 MapList 被用来缓存数据,但忘记在不需要的时候清理 (map.clear())。
    • 未关闭的资源: 数据库连接、文件流、网络连接等,使用后没有调用 close() 方法,可能导致无法回收。
    • 监听器/回调: 注册了监听器或回调,但在不需要时没有取消注册。
    • ThreadLocal: ThreadLocal 变量在线程池中使用,且没有调用 remove() 方法,那么线程在销毁后,其持有的 ThreadLocal 值仍然无法被回收,造成内存泄漏。

JVM 配置不当

  • 堆内存过大: -Xmx (最大堆内存) 设置得过大,会导致 JVM 在启动时就吃掉大量物理内存,给操作系统其他程序带来压力,过大的堆也会导致 GC 暂停时间变长。
  • 堆内存过小: -Xms (初始堆内存) 和 -Xmx (最大堆内存) 设置得过小,应用在高峰期会频繁触发 GC,甚至因为内存不足而崩溃。

如何优化 Java 内存占用?

优化策略需要根据原因来定。

优化应用代码(治本)

  • 修复内存泄漏:
    • 使用分析工具: 这是最有效的方法,使用 jmap 生成堆转储,然后用 Eclipse MATVisualVM 分析。
    • MAT 分析: MAT 会生成一个 "Leak Suspects Report",通常能直接指出可疑的内存泄漏点,它会告诉你哪些对象占用了最多内存,并展示它们的引用链。
    • 代码审查: 检查是否有静态集合未清理、资源未关闭、ThreadLocal 未移除等情况。
  • 优化缓存:
    • 设置合理大小: 为缓存设置最大容量 (maximumSize)。
    • 设置过期时间: 设置写入后或最后访问后的过期时间 (expireAfterWrite, expireAfterAccess)。
    • 使用弱引用: 对于可以随时被回收的缓存对象,考虑使用 WeakReference
  • 减少对象创建: 重用对象,使用对象池(谨慎使用,避免引入新问题),避免在循环中创建不必要的临时对象。

优化 JVM 参数(治标)

  • 合理设置堆内存:
    • 经验法则: 将 -Xmx 设置为可用物理内存的 50% ~ 75%,但要留足内存给操作系统和其他应用程序。
    • 设置 -Xms = -Xmx: 避免堆在运行时动态扩容带来的性能抖动。
    • 示例: 服务器有 16GB 内存,其他程序预计用 4GB,可以设置 -Xms8g -Xmx8g
  • 调整 GC 策略:
    • 低延迟应用: 使用 G1 GC (-XX:+UseG1GC),它是 JDK 9+ 的默认 GC,在吞吐量和延迟之间取得了很好的平衡。
    • 高吞吐量应用: 使用 Parallel GC (-XX:+UseParallelGC),它侧重于最大化应用吞吐量,但 GC 暂停时间可能较长。
    • 避免 Full GC: Full GC 是导致长暂停的主要原因,通过合理调整 G1 GC 的参数(如 -XX:InitiatingHeapOccupancyPercent)来尽可能避免它。
  • 调整元空间:
    • 如果发现元空间频繁触发 GC 或不足,可以适当增加 -XX:MaxMetaspaceSize 的值。
    • 如果元空间占用一直很低,可以适当减小 -XX:MaxMetaspaceSize 以节省内存。
  • 调整线程栈:
    • 如果线程数非常多,可以适当减小 -Xss 的值(-Xss256k),但要确保不会导致 StackOverflowError

监控和告警

  • 建立监控体系: 使用 Prometheus + Grafana 或其他 APM 工具(如 SkyWalking, Pinpoint)对 JVM 的各项指标(堆内存使用率、GC 次数/耗时、元空间使用率、线程数)进行持续监控。
  • 设置告警: 当内存使用率超过某个阈值(如 85%),或 Full GC 频率突然增加时,触发告警,以便及时介入处理。

总结排查流程

  1. 发现: top/htop 发现 Java 进程 RES 内存很高。
  2. 定位: ps 确认进程信息。
  3. 分析:
    • 使用 jstat -gc <pid> 持续观察,是 OU (Old 区) 快满了?还是 MU (元空间) 高?还是 GC 频繁?
    • GC 不频繁,内存只是持续缓慢增长,且没有下降,高度怀疑内存泄漏
    • GC 非常频繁且耗时,可能是配置不当或应用短时间创建了海量对象。
  4. 诊断:
    • 怀疑内存泄漏: 立即使用 jmap 生成堆转储,用 MAT 分析。
    • 怀疑配置不当: 检查 -Xmx, -Xms, GC 策略等参数是否合理。
  5. 优化与验证:
    • 根据分析结果,修改代码或调整 JVM 参数。
    • 部署到测试环境验证效果。
    • 部署到生产环境,并持续监控。

希望这份详细的指南能帮助你有效地排查和解决 Linux 系统下 Java 内存占用的问题!

分享:
扫描分享到社交APP
上一篇
下一篇