24讲内存持续上升,我该如何排查问题
你好,我是刘超。
我想你肯定遇到过内存溢出,或是内存使⽤率过⾼的问题。碰到内存持续上升的情况,其实我们很难从业务⽇志中查看到具体
的问题,那么⾯对多个进程以及⼤量业务线程,我们该如何精准地找到背后的原因呢?
常⽤的监控和诊断内存⼯具
⼯欲善其事,必先利其器。平时排查内存性能瓶颈时,我们往往需要⽤到⼀些Linux命令⾏或者JDK⼯具来辅助我们监测系统或者虚拟机内存的使⽤情况,下⾯我就来介绍⼏种好⽤且常⽤的⼯具。
Linux命令⾏⼯具之top命令
top命令是我们在Linux下最常⽤的命令之⼀,它可以实时显示正在执⾏进程的CPU使⽤率、内存使⽤率以及系统负载等信息。其中上半部分显示的是系统的统计信息,下半部分显示的是进程的使⽤率统计信息。
除了简单的top之外,我们还可以通过top -Hp pid查看具体线程使⽤系统资源情况:
Linux命令⾏⼯具之vmstat命令
vmstat是⼀款指定采样周期和次数的功能性监测⼯具,我们可以看到,它不仅可以统计内存的使⽤情况,还可以观测到CPU的使⽤率、swap的使⽤情况。但vmstat⼀般很少⽤来查看内存的使⽤情况,⽽是经常被⽤来观察进程的上下⽂切换。
r:等待运⾏的进程数;
b:处于⾮中断睡眠状态的进程数;
swpd:虚拟内存使⽤情况;
free:空闲的内存;
buff:⽤来作为缓冲的内存数;
si:从磁盘交换到内存的交换⻚数量;
so:从内存交换到磁盘的交换⻚数量;
bi:发送到块设备的块数;
bo:从块设备接收到的块数;
in:每秒中断数;
cs:每秒上下⽂切换次数;
us:⽤户CPU使⽤时间;
sy:内核CPU系统使⽤时间;
id:空闲时间;
wa:等待I/O时间;
st:运⾏虚拟机窃取的时间。
Linux命令⾏⼯具之pidstat命令
pidstat是Sysstat中的⼀个组件,也是⼀款功能强⼤的性能监测⼯具,我们可以通过命令:yum install sysstat安装该监控组
件。之前的top和vmstat两个命令都是监测进程的内存、CPU以及I/O使⽤情况,⽽pidstat命令则是深⼊到线程级别。
通过pidstat -help命令,我们可以查看到有以下⼏个常⽤的参数来监测线程的性能:
常⽤参数:
-u:默认的参数,显示各个进程的cpu使⽤情况;
-r:显示各个进程的内存使⽤情况;
-d:显示各个进程的I/O使⽤情况;
-w:显示每个进程的上下⽂切换情况;
-p:指定进程号;
-t:显示进程中线程的统计信息。
我们可以通过相关命令(例如ps或jps)查询到相关进程ID,再运⾏以下命令来监测该进程的内存使⽤情况:
其中pidstat的参数-p⽤于指定进程ID,-r表示监控内存的使⽤情况,1表示每秒的意思,3则表示采样次数。其中显示的⼏个关键指标的含义是:
Minflt/s:任务每秒发⽣的次要错误,不需要从磁盘中加载⻚;
Majflt/s:任务每秒发⽣的主要错误,需要从磁盘中加载⻚;
VSZ:虚拟地址⼤⼩,虚拟内存使⽤KB;
RSS:常驻集合⼤⼩,⾮交换区内存使⽤KB。
如果我们需要继续查看该进程下的线程内存使⽤率,则在后⾯添加-t指令即可:
我们知道,Java是基于JVM上运⾏的,⼤部分内存都是在JVM的⽤户内存中创建的,所以除了通过以上Linux命令来监控整个
服务器内存的使⽤情况之外,我们更需要知道JVM中的内存使⽤情况。JDK中就⾃带了很多命令⼯具可以监测到JVM的内存分配以及使⽤情况。
JDK⼯具之jstat命令
jstat可以监测Java应⽤程序的实时运⾏情况,包括堆内存信息以及垃圾回收信息。我们可以运⾏jstat -help查看⼀些关键参数信息:
再通过jstat -option查看jstat有哪些操作:
-class:显示ClassLoad的相关信息;
-compiler:显示JIT编译的相关信息;
-gc:显示和gc相关的堆信息;
-gccapacity:显示各个代的容量以及使⽤情况;
-gcmetacapacity:显示Metaspace的⼤⼩;
-gcnew:显示新⽣代信息;
-gcnewcapacity:显示新⽣代⼤⼩和使⽤情况;
-gcold:显示⽼年代和永久代的信息;
-gcoldcapacity :显示⽼年代的⼤⼩;
-gcutil:显示垃圾收集信息;
-gccause:显示垃圾回收的相关信息(通-gcutil),同时显示最后⼀次或当前正在发⽣的垃圾回收的诱因;
-printcompilation:输出JIT编译的⽅法信息。
它的功能⽐较多,在这⾥我例举⼀个常⽤功能,如何使⽤jstat查看堆内存的使⽤情况。我们可以⽤jstat -gc pid查看:
S0C:年轻代中To Survivor的容量(单位KB);
S1C:年轻代中From Survivor的容量(单位KB);
S0U:年轻代中To Survivor⽬前已使⽤空间(单位KB);
S1U:年轻代中From Survivor⽬前已使⽤空间(单位KB);
EC:年轻代中Eden的容量(单位KB);
EU:年轻代中Eden⽬前已使⽤空间(单位KB);
OC:Old代的容量(单位KB);
OU:Old代⽬前已使⽤空间(单位KB);
MC:Metaspace的容量(单位KB);
MU:Metaspace⽬前已使⽤空间(单位KB);
YGC:从应⽤程序启动到采样时年轻代中gc次数;
YGCT:从应⽤程序启动到采样时年轻代中gc所⽤时间(s);
FGC:从应⽤程序启动到采样时old代(全gc)gc次数;
FGCT:从应⽤程序启动到采样时old代(全gc)gc所⽤时间(s);
GCT:从应⽤程序启动到采样时gc⽤的总时间(s)。
JDK⼯具之jstack命令
这个⼯具在模块三的答疑课堂中介绍过,它是⼀种线程堆栈分析⼯具,最常⽤的功能就是使⽤ jstack pid 命令查看线程的堆栈信息,通常会结合top -Hp pid 或 pidstat -p pid -t⼀起查看具体线程的状态,也经常⽤来排查⼀些死锁的异常。
每个线程堆栈的信息中,都可以查看到线程ID、线程的状态(wait、sleep、running 等状态)以及是否持有锁等。
JDK⼯具之jmap命令
在第23讲中我们使⽤过jmap查看堆内存初始化配置信息以及堆内存的使⽤情况。那么除了这个功能,我们其实还可以使⽤
jmap输出堆内存中的对象信息,包括产⽣了哪些对象,对象数量多少等。我们可以⽤jmap来查看堆内存初始化配置信息以及堆内存的使⽤情况:
我们可以使⽤jmap -histo[:live] pid查看堆内存中的对象数⽬、⼤⼩统计直⽅图,如果带上live则只统计活对象:
我们可以通过jmap命令把堆内存的使⽤情况dump到⽂件中:
我们可以将⽂件下载下来,使⽤ MAT ⼯具打开⽂件进⾏分析:
下⾯我们⽤⼀个实战案例来综合使⽤下刚刚介绍的⼏种⼯具,具体操作⼀下如何分析⼀个内存泄漏问题。
实战演练
我们平时遇到的内存溢出问题⼀般分为两种,⼀种是由于⼤峰值下没有限流,瞬间创建⼤量对象⽽导致的内存溢出;另⼀种则是由于内存泄漏⽽导致的内存溢出。
使⽤限流,我们⼀般就可以解决第⼀种内存溢出问题,但其实很多时候,内存溢出往往是内存泄漏导致的,这种问题就是程序的BUG,我们需要及时找到问题代码。
下⾯我模拟了⼀个内存泄漏导致的内存溢出案例,我们来实践⼀下。
我们知道,ThreadLocal的作⽤是提供线程的私有变量,这种变量可以在⼀个线程的整个⽣命周期中传递,可以减少⼀个线程在多个函数或类中创建公共变量来传递信息,避免了复杂度。但在使⽤时,如果ThreadLocal使⽤不恰当,就可能导致内存泄漏。
这个案例的场景就是ThreadLocal,下⾯我们创建100个线程。运⾏以下代码,系统⼀会⼉就发送了内存溢出异常:
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(100, 100, 1, TimeUnit.MINUTES, new LinkedBlockingQueue<>());//创建线程池,通过线程池,保证创建的线程存活
final static ThreadLocal
@RequestMapping(value = “/test0”)
public String test0(HttpServletRequest request) { poolExecutor.execute(new Runnable() {
public void run() {
Byte[] c = new Byte[4096*1024];
localVariable.set(c);//为线程添加变量
}
});
return “success”;
}
@RequestMapping(value = “/test1”)
public String test1(HttpServletRequest request) { List
Byte[] b = new Byte[1024*20];
temp1.add(b);//添加局部变量
return “success”;
}
在启动应⽤程序之前,我们可以通过HeapDumpOnOutOfMemoryError和HeapDumpPath这两个参数开启堆内存异常⽇志,通
过以下命令启动应⽤程序:
java -jar -Xms1000m -Xmx4000m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof -Xms1g -Xmx |
---|
⾸先,请求test0链接10000次,之后再请求test1链接10000次,这个时候我们请求test1的接⼝报异常了。
通过⽇志,我们很好分辨这是⼀个内存溢出异常。我们⾸先通过Linux系统命令查看进程在整个系统中内存的使⽤率是多少,
最简单就是top命令了。
从top命令查看进程的内存使⽤情况,可以发现在机器只有8G内存且只分配了4G内存给Java进程的情况下,Java进程内存使
⽤率已经达到了55%,再通过top -Hp pid查看具体线程占⽤系统资源情况。
再通过jstack pid查看具体线程的堆栈信息,可以发现该线程⼀直处于 TIMED_WAITING 状态,此时CPU使⽤率和负载并没有
出现异常,我们可以排除死锁或I/O阻塞的异常问题了。
我们再通过jmap查看堆内存的使⽤情况,可以发现,⽼年代的使⽤率⼏乎快占满了,⽽且内存⼀直得不到释放:
通过以上堆内存的情况,我们基本可以判断系统发⽣了内存泄漏。下⾯我们就需要找到具体是什么对象⼀直⽆法回收,什么原因导致了内存泄漏。
我们需要查看具体的堆内存对象,看看是哪个对象占⽤了堆内存,可以通过jstat查看存活对象的数量:
Byte对象占⽤内存明显异常,说明代码中Byte对象存在内存泄漏,我们在启动时,已经设置了dump⽂件,通过MAT打开dump
的内存⽇志⽂件,我们可以发现MAT已经提示了byte内存异常:
再点击进⼊到Histogram⻚⾯,可以查看到对象数量排序,我们可以看到Byte[]数组排在了第⼀位,选中对象后右击选择with
incomming reference功能,可以查看到具体哪个对象引⽤了这个对象。
在这⾥我们就可以很明显地查看到是ThreadLocal这块的代码出现了问题。
总结
在⼀些⽐较简单的业务场景下,排查系统性能问题相对来说简单,且容易找到具体原因。但在⼀些复杂的业务场景下,或是⼀些开源框架下的源码问题,相对来说就很难排查了,有时候通过⼯具只能猜测到可能是某些地⽅出现了问题,⽽实际排查则要结合源码做具体分析。
可以说没有捷径,排查线上的性能问题本身就不是⼀件很简单的事情,除了将今天介绍的这些⼯具融会贯通,还需要我们不断地去累积经验,真正做到性能调优。
思考题
除了以上我讲到的那些排查内存性能瓶颈的⼯具之外,你知道要在代码中对JVM的内存进⾏监控,常⽤的⽅法是什么? 期待在留⾔区看到你的分享。也欢迎你点击“请朋友读”,把今天的内容分享给身边的朋友,邀请他⼀起讨论。
精选留⾔ <br />![](https://cdn.nlark.com/yuque/0/2022/png/1852637/1646315821240-1903e957-f154-447b-bf9c-22a0d3bc25ef.png#)每天晒⽩⽛<br />放两篇⾃⼰在⼯作中排查JVM问题的两篇⽂章【⾮⼴告,纯技术⽂】<br />[https://mp.weixin.qq.com/s/ji_8NhN4NnEHrfAlA9X_ag](https://mp.weixin.qq.com/s/ji_8NhN4NnEHrfAlA9X_ag)
https://mp.weixin.qq.com/s/IPi3xiordGh-zcSSRie6nA
2019-07-18 18:06
作者回复
赞!
2019-07-22 09:47
Geek_75b4cd
⽼师是否可以讲下如何避免threadLocal内存泄漏呢
2019-07-18 23:39
作者回复
我们知道,ThreadLocal是基于ThreadLocalMap实现的,这个Map的Entry继承了WeakReference,⽽Entry对象中的key使⽤了
WeakReference封装,也就是说Entry中的key是⼀个弱引⽤类型,⽽弱引⽤类型只能存活在下次GC之前。
如果⼀个线程调⽤ThreadLocal的set设置变量,当前ThreadLocalMap则新增⼀条记录,但发⽣⼀次垃圾回收,此时key值被回收,⽽value值依然存在内存中,由于当前线程⼀直存在,所以value值将⼀直被引⽤。.
这些被垃圾回收掉的key就存在⼀条引⽤链的关系⼀直存在:Thread —> ThreadLocalMap—>Entry—>Value,这条引⽤链会导致
Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。
我们只需要在使⽤完该key值之后,通过remove⽅法remove掉,就可以防⽌内存泄漏了。
2019-07-19 13:20
WL
请问⼀下⽼师内存泄露和内存溢出具体有啥区别,有点不太理解内存泄露的概念。
2019-07-18 20:59
作者回复
内存泄漏是指不再使⽤的对象⽆法得到及时的回收,持续占⽤内存空间,从⽽造成内存空间的浪费。例如,我们之前在第3讲中聊到的在Java6中substring⽅法可能会导致内存泄漏情况发⽣。当调⽤substring⽅法时会调⽤new string构造函数,此时会复
⽤原来字符串的char数组,⽽如果我们仅仅是⽤substring获取⼀⼩段字符,⽽原本string字符串⾮常⼤的情况下,substring的 对象如果⼀直被引⽤,由于substring的⾥⾯的char数组仍然指向原字符串,此时string字符串也⽆法回收,从⽽导致内存泄露
。
内存溢出则是发⽣了OutOfMemoryException,内存溢出的情况有很多,例如堆内存空间不⾜,栈空间不⾜,以及⽅法区空间 不⾜都会发⽣内存溢出异常。
内存泄漏与内存溢出的关系:内存泄漏很容易导致内存溢出,但内存溢出不⼀定是内存泄漏导致的。
2019-07-19 13:22
JackJin
我⽤ab测试,设置请求数量⼀万,请求test0,内存就溢出,;还没请求到test1,?
2019-07-18 14:04
作者回复
内存泄露导致有⼤量对象⽆法回收,占满了堆内存情况下,就会导致内存溢出。我在这⾥加了⼀个test1只是为了创建更多的对象,从⽽更容易发⽣内存溢出。
2019-07-19 07:42
Liam
能否讲下这个测试⽤例是怎么设计的,为什么跑1w次AB两个⽅法,在1G的堆内存下会发⽣OOM
2019-07-18 08:03
作者回复
平时仅仅某些内存泄漏,⼀般不会导致内存溢出。
所以在这⾥,test0请求⽅法中ThreadLocal为内存泄漏,⽽test1是⼀个触发内存溢出的条件,⼩请求量时没有问题,当请求量
⽐较⼤时,就出现内存溢出情况了。
2019-07-18 11:44
Geek_75b4cd
同理不明⽩为什么这⾥test0,test1⽅法会内存泄漏,请⽼师⾃⼰讲下
2019-07-18 23:37
作者回复
tesr0是内存泄露,test1则是正常的分配堆内存,这⾥test1只是模拟在内存溢出的情况下,如果有⼤量对象创建的情况下,很容易导致内存溢出。
2019-07-19 07:38
QQ怪
学到了很多,感谢感谢
2019-07-18 22:07
撒旦的堕落
⽼师 这段代码有点不解的地⽅ test0使⽤线程池 所以线程⼀直存活 ⽽每个线程的threadlocalmap中 含有4m的内存 没法释放 10
0个线程 才400m被占⽤ ⽽test1⽅法使⽤的是局部变量 ⽅法执⾏后内存就会被回收 4g的内存为啥就溢出了
2019-07-18 08:36
作者回复
如果发⽣GC的情况下,threadlocalmap产⽣的占⽤内存对象就不⽌400m,也就是说发⽣内存溢出的情况下。
2019-07-19 13:21
⻘梅煮酒
⽼师太棒了,是我⼀直想总结⽽不知道怎么总结的⼀篇⽂章
2019-07-18 08:04
csyangchsh
分析垃圾回收⽇志,内存占⽤呈上涨趋势。另外对象年龄分布也是⼀个指征,如果使⽤visualvm,可以查看generation count。
或者取垃圾回收前后的class histogram进⾏⽐较,看哪个类的实例增多了,不过要注意string,byte数组等通常排在最前⾯。所
以要关注⾃⼰写的类,是不是排在前10⼏位。
2019-07-18 07:52
作者回复
对的,也要关注string byte类型的对象⼤⼩是不是异常。
2019-07-19 07:46
yihang
没太看懂test1⽅法,该⽅法内并没有使⽤之前的线程池和threadlocal对象啊?另外其中的创建的对象(都是局部的) 应该在⽅法执⾏完毕可以标记为回收啊?怎么调⽤它会出问题呢
2019-07-18 07:36
作者回复
test1只是⼀个添加内存对象的⽅法,仅仅是作为触发条件,如果threadLocal的对象不及时回收,test1请求量⼤了,就会导致内存溢出。
2019-07-18 11:39