Post

Java应用内存占用过高排查

基础

Java运行时内存结构

JVM在执行Java程序时会将它管理的内存划分为不同的数据区域:

- 程序计数器:用于存放下一条指令所在单元的地址的地方。每执行一条指令,程序计数器就会加一。每个线程都会维护一个独立的程序计数器且各线程之间的程序计数器互不影响,在程序执行过程中,线程会不断的切换,独立的线程计数器保证了当前线程的正确执行位置。程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它随着线程的创建而创建,随着线程的结束而消亡。

- Java虚拟机栈:Java虚拟机栈是线程私有的,它是由许多栈帧组成,而每个栈帧又包括了局部变量表、操作数栈、动态链接以及方法出口信息。每次方法调用都会将对应的栈帧压入虚拟机栈,当方法调用结束( 方法调用return或者方法抛出异常)又会将该栈帧从虚拟机栈中弹出。由于栈的特性(FILO) ,每次操作的都是栈顶栈帧,又被称为“当前活动栈帧”,代表当前正在执行的方法。在JVM执行引擎运行时,所有指令都针对于当前活动栈帧进行操作。可能出现StackOverFlowError:jvm规定了虚拟机栈的最大深度,当执行当前线程时栈帧压入的深度大于了规定的深度,就会抛出该错误,一般是由于递归导致的无限嵌套调用递归方法。 OutOfMemoryError:JVM的内存大小可以动态扩展,如果虚拟机在扩展栈时无法申请到足够的内存空间,就会抛出该错误。

  • 本地方法栈: native关键字修饰的方法被称为本地方法,当线程调用本地方法时,会在本地方法栈中压入当前本地方法的栈帧。该栈帧中包含本地方法的局部变量表、操作数栈、动态链接、方法出口信息。当方法执行完毕时,栈帧会从本地方法栈中弹出,与虚拟机栈相同也会出现StackOverFlowError与OutOfMemoryError错误。

  • 堆: 堆区是JVM所管理的内存中最大的一块区域,该区域被所有线程共享,堆区用于存放了大部分对象实例以及数组。 Heap堆区又分为新生代和老年代,Heap堆区是垃圾收集器GC管理的主要区域。

方法区与元空间:元空间是用于存放类信息、常量、静态变量、JIT即时编译器编译后的机器代码等数据。在JDK1.6时,HotSpot JVM采用Method Area方法区来储存这些数据,也叫永久代(持久代)。 JDK1.7时将字符串常量池、静态变量转移到了堆区 JDK1.8时采用MetaSpace代替了永久代(持久代)

GC方法

- 引用计数法:给对象配备一个整型的引用计数器,每当有一个地方引用这个对象时,计数器值就加1,当引用失效时,计数器值就减1,只要该对象的计数器的值为0,那么这个对象就是不可能再被使用的,就可以视作垃圾处理。它无法解决对象之间互相引用的问题。如Redis

  • 可达性分析法:通过一些称作“GC Roots”的对象作为起始点,从这些节点开始往下搜索,搜索走过的路径叫做引用链,当一个对象到GC Roots没有引用链相连接的时候,就可以叫做这个对象到GC Roots不可达,则此对象就可以定为不可用。如JVM

Jvm中使用可达性分析方法进行GC,GCRoot分类:

  1. Java栈(栈帧中的本地变量表)里面引用的对象。
  2. 方法区中静态属性引用的对象。
  3. 方法区中常量引用的对象。
  4. 本地方法栈中JNI引用的对象。

分代收集: JVM使用分代收集算法对堆中的新生代和老年代分别采用不用的收集方法。分代收集算法是根据对象的生命周期,把内存作分代,然后在分配对象的时候,不同生命周期的对象放在不同的代里面,不同的代上使用合适的回收算法进行回收,比方说,新生代里面的对象存活周期一般都比较短,每次垃圾回收的时候都会发现有大量的对象死去,所以新生代可以使用 复制算法来完成垃圾收集。而老年代里的对象存活率比较高,所以就采用标记清除或者标记整理进行回收。

MAT(Memory Analysis Tool)

安装mat后,打开hprof文件后会看到Overview(概览)的页签。Overview页签下分别包含了Actions,Reports,Step By Step 三大块功能;每一块功能下的子集所对应的作用分别是:

  1. Actions:
  • Histogram 列出每个类所对应的对象个数,以及所占用的内存大小;
  • Dominator Tree 以占用总内存的百分比的方式来列举出所有的实例对象,注意这个地方是直接列举出的对应的对象而不是类,这个视图是用来发现大内存对象的
  • Top Consumers:按照类和包分组的方式展示出占用内存最大的一个对象
  • Duplicate Classes:检测由多个类加载器所加载的类信息(用来查找重复的类)
  1. Reports:
  • Leak Suspects:通过MAT自动分析当前内存泄露的主要原因
  • Top Components:Top组件,列出大于总堆1%的组件的报告
  1. Step By Step:
  • Component Report:组件报告,分析属于公共根包或类加载器的对象

线上发生OOM,一般会配置JVM启动参数来生成dump文件:

1
2
3
-XX:+ExitOnOutOfMemoryError 
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/apps/srv/instance/app/data/express-server-heapdump-2023-11-02-21:39:12,927.hprof           

生成的dump文件可以放入MAT或VisualVM中进行可视化分析,但是这里是发生了OOM,如果只是线上内存占用高怎么排查呢?而且dump的是堆内存快照,如果出现堆外内存泄漏怎么排查呢?

堆外内存排查

使用free查看内存占用:

使用top命令查看资源占用情况:

查看JVM内存分配参数:

1
2
3
arthas中jvm命令
或
jps vm

查看堆内存:

1
jmap -heap 14821

将堆内存快照dump出来:

1
2
3
jmap -dump:live,format=b,file=/tmp/distribution_server_20211020_73.hprof 14821(live参数将会触发Full GC,建议去掉该参数)

jmap -dump:format=b,file=/tmp/finance-server-20220207.hprof 14821

如果查看到的堆内存正常,GC正常,则要排查堆外内存。

NMT(Native Memory Tracking)

Native Memory Tracking

分析堆外内存,可借助于堆外内存跟踪 NativeMemoryTracking

Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用情况的一个功能。我们可以利用jcmd(jdk自带)这个工具来访问NMT的数据。NMT必须先通过VM启动参数中打开,不过要注意的是,打开NMT会带来5%-10%的性能损耗。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-XX:NativeMemoryTracking=[off | summary | detail]
# off: 默认关闭
# summary: 只统计各个分类的内存使用情况.
# detail: Collect memory usage by individual call sites.
然后运行进程,可以使用下面的命令查看直接内存:


jcmd <pid> VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]
 
# summary: 分类内存使用情况.
# detail: 详细内存使用情况,除了summary信息之外还包含了虚拟内存使用情况。
# baseline: 创建内存使用快照,方便和后面做对比
# summary.diff: 和上一次baseline的summary对比
# detail.diff: 和上一次baseline的detail对比
# shutdown: 关闭NMT

查看java8内存分析 jvm启动命令中添加参数:-XX:NativeMemoryTracking=summary 使用:jcmd 14821 VM.native_memory summary scale=GB分析堆外内存

问题:

关于reserved和commited:

Java Hotspot VM explicitly manages the space used for metadata. Space is requested from the OS and then divided into chunks. A class loader allocates space for metadata from its chunks (a chunk is bound to a specific class loader). When classes are unloaded for a class loader, its chunks are recycled for reuse or returned to the OS. Metadata uses space allocated by mmap, not by malloc. In the line beginning with Metaspace, the used value is the amount of space used for loaded classes. The capacity value is the space available for metadata in currently allocated chunks. The committed value is the amount of space available for chunks. The reserved value is the amount of space reserved (but not necessarily committed) for metadata. The line beginning with class space line contains the corresponding values for the metadata for compressed class pointers.

导出jstack文件:

1
2
打印进程所有线程栈信息,保存到jstack.txt文件
jstack 14821 > jstack.txt

在线Thread Dump 分析工具 Java Thread Dump Analyzer (有最大使用次数) 离线Thread Dump 分析工具IBM Thread and Monitor Dump Analyzer for Java (TMDA) (可安装后本地启动,导入jstack文件分析即可)

This post is licensed under CC BY 4.0 by the author.