原创 ning0h2o
date 2020-11-30
1. 内存异常问题
背景
- jdos机器:4核8G
- JVM设置:
export JAVA_OPTS="-Djava.library.path=/usr/local/lib -server -Xms4096m -Xmx4096m
-XX:NativeMemoryTracking=detail -XX:NewRatio=2 -XX:MaxMetaspaceSize=256m -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+CMSParallelRemarkEnabled -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/export/Logs -Djava.awt.headless=true -Dsun.net.client.defaultConnectTimeout=60000 -Dsun.net.client.defaultReadTimeout=60000 -Djmagick.systemclassloader=no -Dnetworkaddress.cache.ttl=300 -Dsun.net.inetaddr.ttl=300"
异常现象:应用程序启动之后,内存使用量缓慢持续上升,然后会在某个时间点突然陡升,陡升之后会继续缓慢上升,然后又会经历陡升阶段。这种行为大概会经历两到三轮,最后内存使用量平稳在70%左右(有时甚至超过80%)。
机器上占用最多内存的进程就是Java(超过80%占比),所以上面的内存使用率上升曲线也可以看做为Java内存使用率上升曲线。
查看内存陡升同时段JVM监控发现JVM堆内存正常。
2. JVM内存区域
知己知彼,百战不殆。Java内存异常,那必须得从JVM内存区域开始。
Java8 JVM内存区域主要可以划分为:堆区(Heap Space)、栈区(Stack Space)和非堆区(Non-Heap Space)。除栈区为其他区域都是线程共享区域。
-
Stack Space
栈区由程序计数器(PC)、虚拟机栈(Stack)和本地方法栈(Native Stack),栈区和线程关联较大,每个线程都有独立的程序计数器、方法栈和本地方法栈。
程序计数器可以看作是线程执行字节码的行号指示器,程序中的分支、循环、跳转、异常处理和线程恢复等基础功能依赖程序计数器完成。
-
虚拟机栈是Java方法执行的内存模型,JVM中有一个称为栈帧的数据结构(可以视为方法对象)保存方法有关信息,如局部变量表、操作数栈、动态链接、方法出口等信息。每当线程调用一个方法时会创建一个栈帧压入线程所有的方法栈中,等方法执行完毕后栈帧出栈。在Java代码处理异常打印堆栈信息
e.printStackTrace()
的时候可以很直观的看到这种行为。线程请求的深度如果大于虚拟机栈所允许的深度(可以使用
-Xss
设定)将会抛出StackOverflowError异常。 -
本地方法栈所发挥的作用和方法栈相似,不过本地方法栈中的方法指的是本地方法(Native Method)。
同虚拟机栈,线程请求的深度如果大于虚拟机栈所允许的深度也会抛出StackOverflowError异常。
-
Heap Space
JVM内存中最大的一块区域,主要作用是存放对象实例,所有的对象实例都在这里分配内存(不过新出现的JVM技术也可以在其他区域分配了)。这也是垃圾收集器(Garbage Collector)主要工作区域,因此也被成为
GC堆
。堆根据所使用垃圾收集算法不同,有更为细节内存区域划分。当使用分代收集算法时,堆整体被划分为新生代(Young Generation)和老年代(Old Generation),其中新生代可以进一步划分为Eden、From Survivor、To Survivor。
当堆没有足够的内存分配对象时,将会抛出OutOfMemoryError异常。
在Java8中字符串常量池被移入堆区,可以通过
jmap -heap
输出的信息验证这一点。 -
Non-Heap Space
堆之外的内存称为非堆内存,这块区域主要是JVM用于其自身内部操作(如GC)的内存。
-
元数据区(Metaspace):非堆内存中有一块用户存储虚拟机加载后的class文件信息(元数据),包括运行时常量池、字段和方法数据,以及方法和构造函数的代码等,这块区域一般称之为方法区(Method Area),在Java 7时也被称为永久代(Perm Gen),从Java 8开始由元空间(Metaspace)替换。
此区域无法满足新的内存分配需求时,将会抛出OutOfMemoryError异常。
本地内存区域(Native Memory)主要是通过JNI(Java Native Interface)和NIO一些方法(DirectByteBuffer)调用产生的一些字节缓冲区(通常通过调用C语言malloc函数分配)。
为了在不同平台上运行JVM字节码,需要将其转换为机器指令。JIT编译器负责进行此项工作。JVM将字节码编译为汇编指令时,会将这些指令(代码)存储在称为代码缓存(Code Cache)的非堆数据区域中。
注:更详细的JVM内存区域信息可以通过NMT(Native Memory Tracking)工具查看。
-
-
堆外内存
首先明确两个定义:
- 堆内存:指JVM Heap区域,可以通过-xms和-xmx为其设定大小
- 本机内存:指物理内存,有时也称为本地内存、原生内存、C Heap、Native堆等,其大小为机器内存(条)大小
堆外内存是指本机内存除Java堆内存之外那一部分本机内存。在Java中,堆外内存使用是通过(魔法类Unsafe)JNI调用本地方法分配,这部分内存不受GC管理(其实是间接管理的),而且也不受最大堆内存限制,同时在网络IO和某些场景下的文件IO中会减少数据拷贝次数。因此一些高性能中间件(如Netty)会使用堆外内存。
在Java程序中,使用堆外内存也一般是指使用
java.nio.DirectByteBuffer
,准确的来说,应该是java.nio.DirectByteBuffer
所”引用”的堆外内存。DirectByteBuffer类在JVM使用堆外内存过程中充当一个handle的角色,JVM通过对DirectByteBuffer类(里面保存了使用堆外空间的大小和堆外空间首地址)的管理来间接管理堆外内存,DirectByteBuffer对象创建则进行堆外内存分配,DirectByteBuffer对象GC回收则进行堆外内存释放。Java使用堆外内存有个臭名昭著的陷阱——冰山现象,几十个占用不大的DirectByteBuff对象后面可能存在远超这些对象几个数量级的内存占用空间,这种现象一般表现为JVM内存正常,机器内存快被消耗殆尽。
3. 堆外内存排查篇
内存陡增时,JVM内存监控正常,内存异常现象表现也与冰山现象几乎一致,那大概是堆外内存异常了。
3.1 堆外内存监控
为了判断是否是堆外内存使用导致内存异常,决定对堆外内存进行监控。首先,前面说到一些中间件会使用到堆外内存,在我们的项目使用堆外内存的中间件只有Netty。所以需要对Netty堆外内存和Java代码中使用的堆外内存监控。
Netty堆外内存监控,主要参考Netty堆外内存泄露排查盛宴这篇博客,其中提到:Netty底层有一个字段
PlatformDependent.DIRECT_MEMORY_COUNTER
有统计堆外内存的使用情况,只需要通过反射获取就可以监控Netty堆外内存使用情况。Java堆外内存监控,则使用了JMX(Java Management Extensions)提供的MBean。
@Component
public class DirectMemoryReport {
private static final Logger LOGGER = LoggerFactory.getLogger("DirectMemoryReportLogger");
private final int _1K = 1024;
private AtomicLong directMemory;
@PostConstruct
public void init() {
// 反射获取Netty堆外内存统计字段
Field field = ReflectionUtils.findField(PlatformDependent.class, "DIRECT_MEMORY_COUNTER");
field.setAccessible(true);
try {
directMemory = (AtomicLong) field.get(PlatformDependent.class);
startReport();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 10秒打印一次堆外内存使用情况
*/
private void startReport() {
Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(this::doReport, 0, 10, TimeUnit.SECONDS);
}
private void doReport() {
long memoryInKb = directMemory.get()/_1K;
LOGGER.info("netty_direct_memory: {}k", memoryInKb);
// DirectBuffer
BufferPoolMXBean directBufferPoolMXBean = ManagementFactory.getPlatformMXBeans(BufferPoolMXBean.class).get(0);
LOGGER.info("DirectBuffer count: {}, MemoryUsed: {}k", directBufferPoolMXBean.getCount(),
directBufferPoolMXBean.getMemoryUsed()/_1K);
}
}
在本地测试验证之后放到了问题机器上去跑,最后结果让人大失所望,Netty使用的堆外内存始终恒定在13107k=120M这一个数值(Netty使用池化堆外内存,这个大概是堆外内存池大小),DirectByteBuff使用的堆外内存使用量虽然时有变化,但是不到1M的使用量几乎可以忽略不计。
3.2 Btrace监控堆外内存申请
Btrace是一款动态的Java跟踪工具。Btrace通过动态(字节码)检测正在运行的Java程序的类来工作。Btrace将跟踪操作插入正在运行的Java程序的类中,并热交换所跟踪的程序类(GitHub主页介绍)。通过Btrace可以获取程序执行过程中的一切信息(如方法参数、返回值、堆栈信息等等)。Btrace使用方式其实和AOP切面很相似。
监控堆外内存申请脚本如下:
// 注意:此脚本只适用Oracle JDK环境,如果是Open JDK环境请替换包名
// 替换规则为:com.sun.btrace → org.openjdk.btrace.core
import com.sun.btrace.annotations.*;
import static com.sun.btrace.BTraceUtils.*;
@BTrace
public class DirectMemoryMonitor {
@OnMethod(clazz="java.nio.Bits", method="reserveMemory")
public static void printThreadStack() {
println("==============thread dump where reserveMemory invoked!");
// 打印堆栈信息
jstack();
}
}
这段脚本的功能是,在java.nio.Bits
类执行reserveMemory
方法时执行printThreadStack
方法(是不是很类似AOP切面),printThreadStack
方法会打印出调用reserveMemory
方法的堆栈信息,这样就能知道是谁在使用堆外内存了。
至于为什么监控堆外内存申请实际监控的是java.nio.Bits#reserveMemory
方法,限于篇幅不再继续深入了。详细原因可以参考DirectByteBuffer
构造函数。
运行脚本命令是:btrace <pid> DirectMemoryMonitor.java
,这样就可以运行BTrace脚本了,不过在实际使用中我们更希望脚本能在后台运行并把脚本运行结果输出到指定目录。在这里可以使用nohup
(no hang up)命令,命令修改之后如下:
nohup btrace <pid> DirectMemoryMonitor.java > /export/Logs/smart-launch/BTraceReport.log 2>&1 &
脚本输出日志如下:
从输出日志可以看出主要是Tomcat在申请堆外内存,通过比较同时段堆外内存监控日志,可以发现Tomcat申请的量不多(上面记录的874k)。因此,到此可以基本确定与堆外内存没有什么关系。
马后炮:JVM可选参数中
-xx:MaxDirectMemorySize
可以限制堆外内存。
4. 走投无路排查篇
堆外内存这条线索断了之后,有点找不着北了,索性来个老生常谈的堆dump分析。
4.1 堆dump分析
堆dump分析可以说是众多Java内存排查手段中最常用的一种,对某一时刻对堆进行快照转储为dump文件(文件后缀名为hprof),然后通过使用dump文件分析工具就可以知晓当时堆中各个对象的使用情况。
4.1.1 获取堆dump文件
线上机器生成堆转储文件可以通过JDK中jcmd或jmap工具生成,我使用的是jmap,命令如下:
jmap -dump:[live,]format=b,file=<file-path> <pid>
命令中参数说明:
- 可选参数
live
:如果指定了live
参数,则会在转储堆前进行一次Full GC,会显著减少堆转储文件的大小。 -
format=b
:以二进制形式转储堆。 -
file-path
:转储文件路径,文件后缀名应为hprof
-
pid
:Java进程ID,可以使用jps
获取。
不过,jmap是作为实验性工具被引入JDK中,官方更推荐使用功能更多性能更强的jcmd替代jmap,这里也介绍下jcmd转储堆的方式:
jcmd <pid> GC.heap_dump [-all] <file-path>
加上-all
选项与jmap不加live
选项转储行为一致,会转储堆中所有对象,否则会在堆转储之前进行一次Full GC。
注意:转储堆会hold住Java进程,进行转储之前请仔细评估对应用程序的影响。
4.1.2 分析dump文件
分析dump文件可以使用JDK中jvisualvm和jhat工具,不过,一般来说第三方处理堆转储的工具都领先于JDK。这里我采用的是MAT(Eclipse Memory Analyzer Tool)进行分析。
上图是MAT对内存异常机器上堆转储文件进行的一个分析,从上可以得出:堆Full GC之后的大小,class数、对象数以及加载器数量。
MAT相比其他堆转储分析工具最有特点的一点是可以自动智能生成各种报告:疑是内存泄露报告(Leak Suspects)、Top Components报告等。
查看支配树(Dominator Tree)一般是分析堆转储文件的第一步。保留了大量堆空间的对象一般称作堆的支配者(Dominator),如果顺利发现有些对象支配着堆大部分空间,那说明就离答案不远了。
从支配树报告可以看出支配着堆空间前列(按保留内存排序)的对象是:web容器(Tomcat)和Spring框架类相关对象。这对于使用了web容器和Spring框架的项目来说很正常。
对象的浅内存(Shallow Heap)、保留内存(Retained Heap)和深内存(Deep Heap)
一个对象的保留内存是指回收该对象之后可以释放的内存空间量。
一个对象的浅内存是指该对象本身大小。如果该对象包含一个指定另一个对象的引用,则引用大小会计入,目标对象大小不会计入。
一个对象的深内存则会包含那些对象的大小。
浅内存 <= 保留内存 <= 深内存
第二步一般会进行堆直方图(Histogram)的分析。
直方图会将同一类型的对象聚合在一起。直方图前列(按保留内存排序)一般是一些基础对象,Char、byte、String等。如果在直方图前列中发现一些异常多的对象,尤其是业务对象时,大概就找到内存异常点了。
经过上面两步骤排查,基本确定堆内存是没有问题的,又一次找错了方向。。。
MAT功能不限于上面介绍的这些,更多内容可参考https://wiki.eclipse.org/MemoryAnalyzer
4.2 Zip工具包内存泄露排查
Zip工具包指的是java.util.zip
工具包,此工具包提供文件数据解压缩功能,在网络IO和RPC调用中常有使用。在使用过程中如果忘记进行工具的关闭操作就会导致内存泄露问题。
排查Zip工具包泄露使用的是Btrace脚本,主要监控的是Deflater
、Inflater
、Zip流
的初始和关闭方法。
5. 内存跟踪排查篇
堆内存始终正常,堆外内存也正常,哪么突增的内存去哪了?
5.1 JVM本机内存跟踪
Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用情况的一个功能。我们可以利用jcmd这个工具来访问NMT的数据。
5.1.1 打开NMT
需要在JVM启动参数中添加-XX:NativeMemoryTracking
参数,可选的值有:
-
summary
:只统计各个分类的内存使用情况 -
detail
:统计各个分类的内存使用情况同时,还会记录各个区域内存分配情况
注意:打开NMT会带来5%-10%的性能损耗。
5.1.2 查看NMT报告
通过jcmd工具获取NMT报告命令如下:
jcmd <pid> VM.native_memory <option> scale=<KB | MB | GB>
option可选选项有:
-
summary
:分类内存使用情况 -
detail
:详细内存使用情况 -
baseline
:创建内存使用快照,方便与后面diff -
summary.diff
:和baseline的summary做diff -
detail.diff
:和baseline的detail做diff
NMT生成的报告如下:
NMT分类描述来源:NMT Memory Categories
5.2 本机内存跟踪
top
命令可以实时反映机器中各个进程的资源占用情况,这里我们主要关注RES(Resident size)部分。
pmap -x <pid>
命令可以得到所监控进程的地址空间和内存分配状态。
5.3 Crontab定时跟踪内存
准备好上述命令后,可以通过Crontab定时执行这些命令来跟踪内存的使用情况。
pid=<pid>
logPath=/export/Logs/smart-launch/logs/
pmap -x ${pid} > ${logPath}/pmap_`date '+%Y%m%d_%H%M%S'`.txt
jcmd ${pid} VM.native_memory detail > ${logPath}/nmt_`date '+%Y%m%d_%H%M%S'`.txt
top -b -n 1 > ${logPath}/top_`date '+%Y%m%d_%H%M%S'`.txt
5.4 报告diff分析
通过diff不同时间段的NMT报告和pmap报告可以分析出内存增长分配在哪了。
上面是机器刚启动时和机器内存率70%左右时Java进程的pmap diff报告。
可以看到RSS占用从机器启动时的2.1G(2236544KB)后面增长到了4.8G(5009304KB),大概增加了2.7G。0x6f080000-0x7f080000
这块内存空间RSS增长最多,从1.5G(1592668KB)增长到了4.0G(4162684KB),大概增加了2.5G,Java进程增加的内存几乎都被这个地址空间“吃掉”了。
那么,这个地址空间是啥呢?总分配的虚拟内存空间为4G,有点耐人寻味,这个地址空间是不是就是堆?
好了,就不卖关子了。这个地址空间就是堆,对比NMT报告就可以发现了,NMT detail部分很清楚的标出了0x6f080000-0x7f080000 reserved 4194304KB for Java Heap
。(为了验证这段地址是堆,其实走了不少弯路)
5.5 堆内存何时分配?
xms
用于设置堆初始容量大小,堆会在内存不足时逐渐扩张至xmx
指定大小(这个过程是可逆的),通常为了避免堆伸缩导致性能问题会把xmx
和xmx
设置为一致。
这台机器设置的xmx
和xmx
都为4G,按理来说堆会恒定为4G内存。但是结合pmap和NMT报告来看,堆有一个内存扩张的过程(有点反常识)。
实际情况是。。。这是一个正常情况。= . =
原因是:操作系统惰性内存分配机制导致的。JVM指定xmx
的初始化堆空间容量操作系统并不会立即在物理内存上分配,而是会在堆内存实际使用(发生缺页错误)才进行实际的内存分配(这个过程省略了很多细节)。虽然没有实际分配内存,但是操作系统向JVM做了一个承诺(commit):这4G内存你什么时候想用我都可以分配给你。因此这部分内存称为committed内存
,NMT报告中就有堆committed=4G。
Reserved Memory、Committed Memory、RSS(Resident Set Size)
Reserved Memory(保留内存):指把系统中的一部分内存保留起来,内核不会为它建立页表,其他应用程序无法访问到这段内存。
Committed Memory(已提交内存):将保留(Reserve)的内存页面正式提交(Commit)使用。
RSS(常驻内存集):表示进程用了具体的多少页的内存。由于linux系统采用的是虚拟内存,进程的代码,库,堆和栈使用的内存都会消耗内存,但是申请出来的内存,只要没真正touch过是不算做RSS,因为没有真正为之分配物理页面
5.6 内存缓慢增长和内存陡升现象的解释
内存缓慢增长是因为对象新建是在堆上进行的,但是这个时候堆未进行实际的内存分配。就可能出现这样一种现象,每次新建一个对象,触发一次实际的内存分配操作,从内存使用率监控来看就是内存在缓慢增长。
内存陡升是因为应用程序经过一段时间运行之后,会有对象晋升到老年代,同理也会触发实际的内存分配操作,不过这次的量不同于每次新建一个对象,而是一大批对象,从内存使用率监控来看就是内存突然陡升。
从GC日志可以比较好的验证上面的观点:
内存陡升时刻,JVM刚好执行了一次Young GC,可以看出这次Young GC停顿时间非常久,足足停顿了1秒(这对于一些实时服务会是灾难性的影响)。究其原因是这次GC大约有1G的对象进入了老年代,老年代进行了一个非常耗时的内存分配过程。
5.7 -XX:+AlwaysPreTouch
-XX:+AlwaysPreTouch
选项可以在Java应用程序启动时进行堆内存touch,确保堆内存全部驻留在物理内存中(over-commit)。
使用此选项会导致应用启动时间变长,还有内存换页率上升。不过对于主要进程是单个Java进程的机器来说,此选项带来的性能提升大于此选项带来负面影响(上面提到的GC时间变长)。
下图为使用-XX:+AlwaysPreTouch
选项启动后的内存使用率监控。
6 内存排查总结篇
排查一个多月之后的结果竟然是一切正常,着实让人哭笑不得。不过,前事不忘后事之师。
6.1 正常Java进程占用多少内存(RSS)?
这里有一个公式:
下面是对公式中的各个参数描述:
- Heap:指堆已使用的(Used)内存,注意和committed内存的区别。(used <= committed)
- Thread:指线程占用空间,计算公式为
ThreadNum * ThreadSize
- Metaspace:指元空间大小
- GC:指定GC结构使用的空间
- CodeCache:代码缓存占用空间
- DirectMemory:堆外内存空间
- NativeLibraries:JVM运行中的本地库(通常是C库,如gclib)
这个公式占大头的主要是堆,如果一个Java进程线程数很少且使用少量的堆外内存的话,Java进程RSS会比较接近堆内存大小。
下面是关于问题机器某时刻的内存计算:
- Java RSS为4.9G,堆内存最后分配完毕为4G,线程占用内存为线程数(500+)乘以1M,因此线程占用大约为0.5G。其他空间占用合计约0.5G左右。 JavaRSS=4.9G≈4G+0.5G+0.5G
- 70%内存使用率:使用内存为5.6G(0.7*8G),除去Java占用的约5G内存,剩下的0.6G被Nginx,其他应用程序和操作系统占有。
由上可得70%左右内存占用空间是JVM堆全部完成分配之后的一个正常的内存占用率。
6.2 内存排查流程
6.2.1 监控&保留现场
现有的JVM监控和机器监控比较完善了,可以视情况加上相应资源监控,如这次的堆外内存监控。
XX:+HeapDumpOnOutOfMemoryError
和-XX:HeapDumpPath
选项必须开启,在异常状态不可复制的情况下,这可能是唯一可以分析的现场信息了。
-XX:+PrintGCDetails
-Xloggc:/export/Logs/gc.log
-XX:+PrintGCDateStamps
-XX:+PrintHeapAtGC
选项开启GC日志,GC日志在内存异常分析中很有用。
6.2.2 使用JDK工具&堆转储分析工具
工欲善其事必先利其器。
JDK工具中jcmd有“一统天下之势”,可以打印Java进程所涉及的基本类、线程和VM信息。(Oracle文档中多次推荐使用此工具)
其他的工具这里也列举一下,可以根据情况不同选用:
- jconsole:提供JVM活动信息的GUI工具,线上机器一般用不上这个
- jhat:分析堆转储文件
- jmap:提供堆转储和JVM内存使用信息
- jinfo:查看JVM系统属性,可以动态设置一些系统属性
- jstack:转储Java进程的栈信息
- jstat:提供GC和类转载信息
- jvisulavm:监视JVM的GUI工具,可以抓取&分析堆转储文件
堆转储分析工具推荐使用第三方工具MAT(Eclipse Memory Analyzer Tool)。
6.2.3 Linux命令行&其他性能跟踪工具
Linux命令行有一些可以提供进程的运行状况,比如上面使用到的pmap(查看内存映射空间)和top命令,其他的还有ps、free等等。
Btrace是Java问题排查过程中一大利器,不要忘了上面对它的介绍,几乎可以获取Java程序执行过程中的一切信息。
gdb是Linux操作系统下程序调试工具(主要用来调试C/C++代码)。某些情况下用来调试Java应用程序也有奇效。可以用来dump指定地址空间内存(配合pmap使用),监控malloc
和free
函数调用情况等等。
6.3 排查过程中发现的一些问题
-
可能的内存泄露点
这是内存异常机器上某个时间段的支配树信息,不同于以前支配树信息前列是web容器和Spring框架类相关对象,这里位居第二的是Druid数据源对象,数据源对象中占用内存最多的是被称为stat的对象,搜索了一下,是Druid监控功能实现的关键。这个对象会记录SQL语句,并保存在LinkedHashMap结构中,看样子保存在map里面的数据是不会释放的,如果记录时间长了,势必会出现内存泄露的情况。
在Google上,以“Druid stat 内存泄露”为关键字进行搜索,果不其然出现一大堆搜索结果。同时在Druid GitHub项目中也有一大堆关于此种内存泄露情况的Issue。
然后上面这条记录的SQL也很有疑点,一条SQL语句竟然占用525848B=0.5MB的内存,copy到本地编辑器打开:
SELECT xxx, xxx, xxx, xxx FROM xx_info WHERE xx = 1 and ( ( xx = ?
and yn = ?
and xx_id in
(
?
,
?
,
?
,
?
此处省略 1万+个?
小朋友,你是否有很多问号???(黑人问号)
-
未命名的线程
请更换为带有业务名称的线程名,否则排查的时候就是噩梦。
-
线程池使用的一些问题
@Bean(name = "skuExecutorService")
public ExecutorService skuExecutorService() {
return Executors.newFixedThreadPool(50);
}
这是项目中一段创建固定线程数量线程池的代码,看起来是没啥问题。但是点开newFixedThreadPool
这个方法:
会发现FixedThreadPool使用的是无界队列,有潜在的内存溢出问题存在。
-
不合理堆区分配
jmap -histo
输出某时刻堆直方图(Histogram)
jmap -heap
输出同一时刻堆内存占用信息:
项目中会通过调用RPC查询广告主所有的sku信息,一般SKU量会比较大,会产生大量SkuSeckillInfo对象。
现在堆区新生代与老年代的比例是1:2,新生代设置的比较小,大量SkuSeckillInfo对象会充斥整个新生代,导致更频繁的Young GC,更频繁的Young GC势必导致对象晋升老年代速度变快,对象晋升老年代速度变快势必导致Full GC会比较多。(频繁的GC意味着累计更长的停顿时间)
扩展文档
- MAT使用手册:https://wiki.eclipse.org/MemoryAnalyzer
- jcmd使用手册:https://docs.oracle.com/javase/8/docs/technotes/tools/windows/jcmd.html
- NMT使用手册:https://docs.oracle.com/javase/8/docs/technotes/guides/vm/nmt-8.html
- gdb排查内存泄露:https://www.ibm.com/support/pages/linux-gdb-identify-memory-leaks
- 堆内存与RSS不一致问题:https://stackoverflow.com/questions/48982636/java-heap-xms-and-linux-free-memory-different
- Java故障排查手册:https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/index.html
- HotSpot VM选项:https://www.oracle.com/java/technologies/javase/vmoptions-jsp.html