作者:马海琴 编辑:毕小烦
二. 内存
内存又称主存,是 CPU 能直接寻址的存储空间(由半导体器件制成)。
内存的特点是存取速率快,断电一般不保存数据(非持久化设备)。内存的作用是用于暂时存放 CPU 中的运算数据,以及与硬盘等外部存储器交换的数据,可保障 CPU 计算的稳定性和高性能。内存就像人的神经系统,负责传递数据,产生命令的交互作用。
2.1 常见的内存问题
一般建议系统内存使用率不超过 70% 。当系统资源在错误使用的情况下,可能导致使用完毕的资源无法回收或者没有回收,这个时候出现的问题叫内存泄漏;内存泄漏可能使得内存使用率持续保持在较高水位,此时一旦出现大内存的占用就很容易出现内存溢出。
内存泄漏(Memory Leak)
内存泄漏是指程序在申请内存后,无法释放已申请的内存空间,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。 一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。
内存泄漏的分类
按发生方式来分类
- 常发性内存泄漏
发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
- 偶发性内存泄漏
发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
- 一次性内存泄漏
发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
- 隐式内存泄漏
程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
内存溢出(Out Of Memory)
内存溢出是指程序申请内存时,没有足够的内存供申请者使用。
比如,给了你一块存储 int 类型数据的存储空间,但是你却存储 long 类型的数据,那么结果就是内存不够用,此时就会报错 OOM,即所谓的内存溢出;或者是创建一个大的对象,而堆内存放不下这个对象,这也是内存溢出。
当内存的 available 较少时,我们需要特别关注,因为此时一旦出现大内存的占用就有可能导致内存溢出,甚至触发系统的 OOM Killer 直接终止进程。
什么场景容易触发大内存的占用呢?
比如:流量进入高峰期、用户触发了某一个内存占用较高的功能等。
我们怎么判断服务终止是系统 OOM Killer 引起的呢?
可通过dmesg -T
查看系统日志。
从上图的系统日志中,可以看到系统强制终止了进程18863
,以达到释放内存的目的。
2.2 内存分析的步骤
STEP 1. 用 free 查看系统内存的使用情况
可以通过free
命令查看系统内存的使用情况。关注其中 available
的大小,即应用可用内存的大小。
STEP 2. 用 jstat 查看 JVM 使用情况
当发现系统内存使用率过高,或者 cpu
使用率较高的线程为垃圾回收的线程时,可以使用jstat -gcutil pid[进程ID]
分析进程的 JVM 使用情况。
如:
连续打印 JVM 的使用情况,发现 FGC 次数不断上涨,而 O 的占用率没有出现明显下降,说明存在内存泄漏导致内存已经无法回收。此时我们定位到问题出在内存,接下来就是要将内存 dump
下来进行进一步分析了。
STEP 3. dump 内存
① 手动 dump
当我们怀疑或者发现内存出现问题的时候,可以将内存 dump 下来,然后将 dump 文件下载到本地,使用mat
工具进行具体的分析。
可以使用jmap
命令进行 dump 操作:
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 的文件路径即可:
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump/heapdump_$(date +%Y%m%d-%H%M).hprof
STEP 4. 用 mat 分析内存
将 dump
的 hprof
文件导入 mat
工具中。
查看内存泄漏报告,发现一个 MyTest
类的 testA
方法存在泄露。我们可以查看对应源码,寻找出现问题的地方。如果仅仅根据方法还是无法定位到具体的问题,那就需要继续分析。
首先查看堆信息,按深堆进行排序,我们发现一个非原生的类 ClassA
占用的堆内存很大,且实例数很多,那我们从这个类开始分析。
查看对象 ClassA的引用关系,发现是被一个 ArrayList 所引用的.
再结合内存泄漏分析中的方法 testA
进行分析,在源码中果然发现一个ArrayList
引用 ClassA
的死循环。
public void testA() {
List<ClassA> list = new ArrayList<>();
while (true) {
ClassA classA = new ClassA();
classA.s = "test";
list.add(classA);
}
}
在实际的生产工程中,出现问题可能会更加复杂,隐藏的也更深,但是排查问题的思路是不变的,首先要保留现场信息,然后再将源码和现场信息进行进一步分析,定位到问题后,我们还需要进行复现,代码优化后也需要再次验证。
2.3 命令详解
free 命令
free
命令可以直接看出系统内存的使用情况。
命令格式:
$ 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次
举个例子:
结果说明:
total
:物理内存的总大小;used
:被使用的物理内存大小;free
:系统未使用的物理内存总量;shared
:共享内存,由于是多个进程间共享使用;buffer/cached
:磁盘缓存的大小;available
:从应用的角度看还可以被进程使用的物理内存大小。Linux 内核为了提升磁盘操作的性能,会消耗一部分内存去缓存磁盘数据,就是buffer
和cache
。当应用程序需要内存时,如果没有足够的free
内存可以用,内核就会从buffer
和cache
中回收内存来满足应用程序的请求。所以从应用程序的角度来说,available = free + buffer + cache
。请注意,这只是一个很理想的计算方式,实际中的数据往往有较大的误差。jstat 命令
可以使用jstat -gc pid[进程ID]
查看 jvm 各代的使用情况。
也可以使用jstat -gcutil pid[进程ID]
查看各代使用百分比。
命令格式:
$ 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
:垃圾回收消耗总时间
(完)
微信搜索“毕小烦”或者扫描下面的二维码,即可订阅我的微信公众号。
如果文章对你有帮助,记得留言、点赞、加关注哦!