作者:马海琴 编辑:毕小烦

二. 内存

内存又称主存,是 CPU 能直接寻址的存储空间(由半导体器件制成)。

内存的特点是存取速率快,断电一般不保存数据(非持久化设备)。内存的作用是用于暂时存放 CPU 中的运算数据,以及与硬盘等外部存储器交换的数据,可保障 CPU 计算的稳定性和高性能。内存就像人的神经系统,负责传递数据,产生命令的交互作用。

2.1 常见的内存问题

一般建议系统内存使用率不超过 70% 。当系统资源在错误使用的情况下,可能导致使用完毕的资源无法回收或者没有回收,这个时候出现的问题叫内存泄漏;内存泄漏可能使得内存使用率持续保持在较高水位,此时一旦出现大内存的占用就很容易出现内存溢出

内存泄漏(Memory Leak)

内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

内存泄漏的分类

按发生方式来分类

  1. 常发性内存泄漏

发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。

  1. 偶发性内存泄漏

发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

  1. 一次性内存泄漏

发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。

  1. 隐式内存泄漏

程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

内存溢出(Out Of Memory)

内存溢出是指程序申请内存时,没有足够的内存供申请者使用。
比如,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM,即所谓的内存溢出;或者是创建一个大的对象,而堆内存放不下这个对象,这也是内存溢出。

当内存的 available 较少时,我们需要特别关注,因为此时一旦出现大内存的占用就有可能导致内存溢出,甚至触发系统的 OOM Killer 直接终止进程。

什么场景容易触发大内存的占用呢?

比如:流量进入高峰期、用户触发了某一个内存占用较高的功能等。

我们怎么判断服务终止是系统 OOM Killer 引起的呢?

可通过dmesg -T查看系统日志。
image.png
从上图的系统日志中,可以看到系统强制终止了进程18863,以达到释放内存的目的。

2.2 内存分析的步骤

STEP 1. 用 free 查看系统内存的使用情况

可以通过free命令查看系统内存的使用情况。关注其中 available 的大小,即应用可用内存的大小。
image.png

STEP 2. 用 jstat 查看 JVM 使用情况

当发现系统内存使用率过高,或者 cpu 使用率较高的线程为垃圾回收的线程时,可以使用jstat -gcutil pid[进程ID]分析进程的 JVM 使用情况。

如:
image.png
连续打印 JVM 的使用情况,发现 FGC 次数不断上涨,而 O 的占用率没有出现明显下降,说明存在内存泄漏导致内存已经无法回收。此时我们定位到问题出在内存,接下来就是要将内存 dump 下来进行进一步分析了。

STEP 3. dump 内存

① 手动 dump

当我们怀疑或者发现内存出现问题的时候,可以将内存 dump 下来,然后将 dump 文件下载到本地,使用mat工具进行具体的分析。

可以使用jmap命令进行 dump 操作:

  1. jmap -dump:format=b,file=path/fileName.hprof[导出路径] pid[进程ID]

在进行 dump 操作之前需要注意内存和磁盘 I/O 的使用情况,因为 dump 本身是一个非常占用内存的行为,而 dump 下的文件一般也较大,如果系统内存吃紧或者 I/O 较慢的情况下,一般不建议进行 dump 操作。

② 自动 dump

一般系统 OOM 的时候,我们都是事后才知晓的,那如何才能在 OOM 的时候保留下内存的 dump 信息呢?

可以启用 JVM 参数HeapDumpOnOutOfMemoryError,启用之后,系统会在应用 OOM 的时候,自动将当时的内存进行 dump 。在启动参数中启用该参数并指定 dump 的文件路径即可:

  1. -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump/heapdump_$(date +%Y%m%d-%H%M).hprof

STEP 4. 用 mat 分析内存

dumphprof 文件导入 mat 工具中。
查看内存泄漏报告,发现一个 MyTest 类的 testA 方法存在泄露。我们可以查看对应源码,寻找出现问题的地方。如果仅仅根据方法还是无法定位到具体的问题,那就需要继续分析。
image.png
首先查看堆信息,按深堆进行排序,我们发现一个非原生的类 ClassA 占用的堆内存很大,且实例数很多,那我们从这个类开始分析。
image.png
查看对象 ClassA的引用关系,发现是被一个 ArrayList 所引用的.
image.png
再结合内存泄漏分析中的方法 testA 进行分析,在源码中果然发现一个ArrayList 引用 ClassA 的死循环。

  1. public void testA() {
  2. List<ClassA> list = new ArrayList<>();
  3. while (true) {
  4. ClassA classA = new ClassA();
  5. classA.s = "test";
  6. list.add(classA);
  7. }
  8. }

在实际的生产工程中,出现问题可能会更加复杂,隐藏的也更深,但是排查问题的思路是不变的,首先要保留现场信息,然后再将源码和现场信息进行进一步分析,定位到问题后,我们还需要进行复现,代码优化后也需要再次验证。

2.3 命令详解

free 命令

free 命令可以直接看出系统内存的使用情况。

命令格式:

  1. $ free [-][bkmholtV][-s <间隔秒数>][-c <重复次数>]

参数解释:

  • b:以 Byte 为单位显示内存使用情况
  • k:以 KB 为单位显示内存使用情况
  • m:以 MB 为单位显示内存使用情况
  • h:以合适的单位显示内存使用情况,最大为三位数,自动计算对应的单位值。单位有:
    • B = bytes
    • K = kilos
    • M = megas
    • G = gigas
    • T = teras
  • o:不显示缓冲区调节列
  • l:显示高低内存的利用率
  • t:显示内存总和列
  • V:显示版本信息
  • s N:每隔N秒打印一次内存信息,ctrl+c中断循环显示
  • c N:重复打印内存信息N次

举个例子:
image.png
结果说明:

  • total:物理内存的总大小;
  • used:被使用的物理内存大小;
  • free:系统未使用的物理内存总量;
  • shared:共享内存,由于是多个进程间共享使用;
  • buffer/cached:磁盘缓存的大小;
  • available:从应用的角度看还可以被进程使用的物理内存大小。Linux 内核为了提升磁盘操作的性能,会消耗一部分内存去缓存磁盘数据,就是 buffercache。当应用程序需要内存时,如果没有足够的 free 内存可以用,内核就会从 buffercache 中回收内存来满足应用程序的请求。所以从应用程序的角度来说,available = free + buffer + cache。请注意,这只是一个很理想的计算方式,实际中的数据往往有较大的误差。

    jstat 命令

    可以使用jstat -gc pid[进程ID]查看 jvm 各代的使用情况。
    image.png
    也可以使用jstat -gcutil pid[进程ID]查看各代使用百分比。
    image.png

命令格式:

  1. $ jstat [Options] pid [interval] [count]

参数解释:

  • Options:命令参数,常用 -gc 和 -gcutil
    • -gc:统计 jdk gc 时, heap 已使用空间使用字节数表示;
    • -gcutil:统计 gc 时, heap 已使用空间使用百分比表示;
    • -class:统计 class loader 行为信息;
    • -compile:统计编译行为信息;
    • -gccapacity:统计不同 generations(新生代,老年代,持久代)的 heap 容量情况;
    • -gccause:统计引起 gc 的事件;
    • -gcnew:统计 gc 时,新生代的情况;
    • -gcnewcapacity:统计 gc 时,新生代 heap 容量;
    • -gcold:统计 gc 时,老年代的情况;
    • -gcoldcapacity:统计 gc 时,老年代 heap 容量;
    • -gcpermcapacity:统计 gc 时, permanent区 heap 容量;
  • pid:当前运行的 java进程号
  • interval:间隔时间,单位为毫秒
  • count:打印次数,如果缺省则打印无数次

jstat -gc 命令执行结果说明:

  • S0C:第一个幸存区的大小
  • S1C:第二个幸存区的大小
  • S0U:第一个幸存区的使用大小
  • S1U:第二个幸存区的使用大小
  • EC:伊甸园区的大小
  • EU:伊甸园区的使用大小
  • OC:老年代大小
  • OU:老年代使用大小
  • MC:方法区大小
  • MU:方法区使用大小
  • CCSC:压缩类空间大小
  • CCSU:压缩类空间使用大小
  • YGC:年轻代垃圾回收次数
  • YGCT:年轻代垃圾回收消耗时间
  • FGC:老年代垃圾回收次数
  • FGCT:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

单位:KB

jstat -gcutil 命令执行结果说明:

  • S0:幸存1区当前使用比例
  • S1:幸存2区当前使用比例
  • E:伊甸园区使用比例
  • **O**:老年代使用比例
  • M:元数据区使用比例
  • CCS:压缩使用比例
  • YGC:年轻代垃圾回收次数
  • **FGC**:老年代垃圾回收次数
  • **FGCT**:老年代垃圾回收消耗时间
  • GCT:垃圾回收消耗总时间

(完)
微信搜索“毕小烦”或者扫描下面的二维码,即可订阅我的微信公众号
image.png
如果文章对你有帮助,记得留言、点赞、加关注哦!