内存

内存

和内存相关的指标主要有以下几个,常用的分析工具有:top、free、vmstat、pidstat 以及 JDK 自带的一些工具。

  • 系统内存的使用情况,包括剩余内存、已用内存、可用内存、缓存/缓冲区;
  • 进程(含 Java 进程)的虚拟内存、常驻内存、共享内存;
  • 进程的缺页异常数,包含主缺页异常和次缺页异常;
  • Swap 换入和换出的内存大小、Swap 参数配置;
  • JVM 堆的分配,JVM 启动参数;
  • JVM 堆的回收,GC 情况。

使用 free 可以查看系统内存的使用情况和 Swap 分区的使用情况,top 工具可以具体到每个进程,如我们可以用使用 top 工具查看 Java 进程的常驻内存大小(RES),这两个工具结合起来,可用覆盖大多数内存指标。下面是使用 free 命令的输出:

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           125G        6.8G         54G        2.5M         64G        118G
Swap:          2.0G        305M        1.7G

Swap 的作用就是把一块磁盘空间或者一个本地文件当成内存来使用,包括换出和换入两个过程。Swap 需要读写磁盘数据,所以性能不是很高,事实上,包括 ElasticSearch、Hadoop 在内绝大部分 Java 应用都建议关掉 Swap,这是因为内存的成本一直在降低,同时这也和 JVM 的垃圾回收过程(GC)有关:JVM 在 GC 的时候会遍历所有用到的堆的内存,如果这部分内存被 Swap 出去了,遍历的时候就会有磁盘 I/O。Swap 分区的升高一般和磁盘的使用强相关,具体分析时,需要结合缓存时候用、swappiness 阈值以及匿名页和文件页的活跃情况综合分析。

buff/cache 是缓存和缓冲区的大小。缓存(cache):是从磁盘读取的文件的或者向磁盘写文件时的临时存储数据,面向文件。使用 cachestat 可以查看整个系统缓存的读写命中情况,使用 cachetop 可以观察每个进程缓存的读写命中情况。缓冲区(buffer)是写入磁盘数据或从磁盘直接读取的数据的临时存储,面向块设备。free 命令的输出中,这两个指标是加在一起的,使用 vmstat 命令可以区分缓存和缓冲区,还可以看到 Swap 分区换入和换出的内存大小。

了解到常见的内存指标后,常见的内存问题又有哪些?总结如下:

  • 系统剩余内存/可用不足(某个进程占用太多、系统本身内存不足),内存溢出;
  • 内存回收异常:内存泄漏(进程在一段时间内内存使用持续走高)、GC 频率异常;
  • 缓存使用过大(大文件读取或写入)、缓存命中率不高;
  • 缺页异常过多(频繁的 I/O 读);
  • Swap 分区使用异常(使用过大);
# 按照 Swap 分区的使用情况列出前 10 的进程
$ for file in /proc/*/status ; do awk '/VmSwap|Name|^Pid/{printf $2 " " $3}END{ print ""}' $file; done | sort -k 3 -n -r | head -10

内存相关指标异常后,分析思路是怎么样的?

  • 用 free/top 查看内存的全局使用情况,如系统内存的使用、Swap 分区内存使用、缓存/缓冲区占用情况等,初步判断内存问题存在的方向:进程内存、缓存/缓冲区、Swap 分区;

  • 观察一段时间内存的使用趋势。如通过 vmstat 观察内存使用是否一直在增长;通过 jmap 定时 dump 对象列表,判断是否存在内存泄漏,通过 cachetop 命令,定位缓冲区升高的根源等;

  • 根据内存问题的类型,结合应用本身,进行详细分析。

使用 free 发现缓存/缓冲区占用不大,排除缓存/缓冲区对内存的影响后 -> 使用 vmstat 或者 sar 观察一下各个进程内存使用变化趋势 -> 发现某个进程的内存时候用持续走高 -> 如果是 Java 应用,可以使用 jmap / VisualVM / heap dump 分析等工具观察对象内存的分配,或者通过 jstat 观察 GC 后的应用内存变化 -> 结合业务场景,定位为内存泄漏/GC 参数配置不合理/业务代码异常等。

问题排查

内存分为系统内存和进程内存(含 Java 应用进程),一般我们遇到的内存问题,绝大多数都会落在进程内存上,系统资源造成的瓶颈占比较小。对于 Java 进程,它自带的内存管理自动化地解决了两个问题:如何给对象分配内存以及如何回收分配给对象的内存,其核心是垃圾回收机制。

垃圾回收虽然可以有效地防止内存泄露、保证内存的有效使用,但也并不是万能的,不合理的参数配置和代码逻辑,依然会带来一系列的内存问题。此外,早期的垃圾回收器,在功能性和回收效率上也不是很好,过多的 GC 参数设置非常依赖开发人员的调优经验。比如,对于最大堆内存的不恰当设置,可能会引发堆溢出或者堆震荡等一系列问题。

系统内存不足

Java 应用一般都有单机或者集群的内存水位监控,如果单机的内存利用率大于 95%,或者集群的内存利用率大于 80%,就说明可能存在潜在的内存问题(注:这里的内存水位是系统内存)。

除了一些较极端的情况,一般系统内存不足,大概率是由 Java 应用引起的。使用 top 命令时,我们可以看到 Java 应用进程的实际内存占用,其中 RES 表示进程的常驻内存使用,VIRT 表示进程的虚拟内存占用,内存大小的关系为:VIRT > RES > Java 应用实际使用的堆大小。除了堆内存,Java 进程整体的内存占用,还有方法区/元空间、JIT 缓存等,主要组成如下:

Java 应用内存占用 = Heap(堆区)+ Code Cache(代码缓存区) + Metaspace(元空间)+ Symbol tables(符号表)+ Thread stacks(线程栈区)+ Direct buffers(堆外内存)+ JVM structures(其他的一些 JVM 自身占用)+ Mapped files(内存映射文件)+ Native Libraries(本地库)+ …

Java 进程的内存占用,可以使用 jstat -gc 命令查看,输出的指标中可以得到当前堆内存各分区、元空间的使用情况。堆外内存的统计和使用情况,可以利用 NMT(Native Memory Tracking,HotSpot VM Java8 引入)获取。线程栈使用的内存空间很容易被忽略,虽然线程栈内存采用的是懒加载的模式,不会直接使用 +Xss 的大小来分配内存,但是过多的线程也会导致不必要的内存占用,可以使用 jstackmem 这个脚本统计整体的线程占用。

系统内存不足的排查思路:

  • 首先使用 free 查看当前内存的可用空间大小,然后使用 vmstat 查看具体的内存使用情况及内存增长趋势,这个阶段一般能定位占用内存最多的进程;

  • 分析缓存 / 缓冲区的内存使用。如果这个数值在一段时间变化不大,可以忽略。如果观察到缓存 / 缓冲区的大小在持续升高,则可以使用 pcstat、cachetop、slabtop 等工具,分析缓存 / 缓冲区的具体占用;

  • 排除掉缓存 / 缓冲区对系统内存的影响后,如果发现内存还在不断增长,说明很有可能存在内存泄漏

Java 内存溢出

内存溢出是指应用新建一个对象实例时,所需的内存空间大于堆的可用空间。内存溢出的种类较多,一般会在报错日志里看到 OutOfMemoryError 关键字。常见内存溢出种类及分析思路如下:

  • java.lang.OutOfMemoryError: Java heap space。原因:堆中(新生代和老年代)无法继续分配对象了、某些对象的引用长期被持有没有被释放,垃圾回收器无法回收、使用了大量的 Finalizer 对象,这些对象并不在 GC 的回收周期内等。一般堆溢出都是由于内存泄漏引起的,如果确认没有内存泄漏,可以适当通过增大堆内存。

  • java.lang.OutOfMemoryError: GC overhead limit exceeded。原因:垃圾回收器超过 98%的时间用来垃圾回收,但回收不到 2%的堆内存,一般是因为存在内存泄漏或堆空间过小。

  • java.lang.OutOfMemoryError: Metaspace 或 java.lang.OutOfMemoryError: PermGen space。排查思路:检查是否有动态的类加载但没有及时卸载,是否有大量的字符串常量池化,永久代/元空间是否设置过小等。

  • java.lang.OutOfMemoryError: unable to create new native Thread。原因:虚拟机在拓展栈空间时,无法申请到足够的内存空间。可适当降低每个线程栈的大小以及应用整体的线程个数。此外,系统里总体的进程/线程创建总数也受到系统空闲内存和操作系统的限制,请仔细检查。注:这种栈溢出,和 StackOverflowError 不同,后者是由于方法调用层次太深,分配的栈内存不够新建栈帧导致。

此外,还有 Swap 分区溢出、本地方法栈溢出、数组分配溢出等 OutOfMemoryError 类型。

Java 内存泄漏

Java 内存泄漏可以说是开发人员的噩梦,内存泄漏与内存溢出不同则,后者简单粗暴,现场也比较好找。内存泄漏的表现是:应用运行一段时间后,内存利用率越来越高,响应越来越慢,直到最终出现进程假死。

Java 内存泄漏可能会造成系统可用内存不足、进程假死、OOM 等,排查思路却不外乎下面两种:

  • 通过 jmap 定期输出堆内对象统计,定位数量和大小持续增长的对象;
  • 使用 Profiler 工具对应用进行 Profiling,寻找内存分配热点。