实战-分析java项⽬线上内存泄漏、内存溢出、频繁GC的原因前⾔
有些⼈傻傻分不清内存泄漏和内存溢出的区别,这⾥简单做个科普
内存溢出:就是内存不够⽤了,对象需要的内存⼤⼩⼤于你分配的堆⼤⼩,内存溢出最常见的错误就是OutOfMemoryError,简称OOM;
内存泄漏:对象⽤完之后没被垃圾回收器(GC)回收,既然没被回收,那么这个对象就会⼀直占⽤着内存空间,这就是内存泄漏。内存泄漏的最终结果就是会导致内存溢出。因为对象⼀直占⽤,久⽽久之,⼀直叠加到超过最⼤堆内存时,就会导致OOM。
本次分析内存泄漏的⼯具主要有2个,⼀个是arthas,另⼀个是jdk⾃带的⼯具jmap,关于这2个⼯具的⽤法,可以参考我之前写的2篇⽂章:
arthas : Arthas使⽤教程 阿⾥巴巴开源项⽬、史上最强java线上诊断⼯具
jmap: 原来jdk⾃带了这么好玩的⼯具 > jmap 使⽤教程
模拟内存泄漏
下列的java代码是⼀个模拟线上的内存泄漏的代码,这段代码的业务逻辑是从数据库中读取信⽤数据,套⽤模型,并把结果进⾏记录和传输;
;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import urrent.ScheduledThreadPoolExecutor;
import urrent.ThreadPoolExecutor;
import urrent.TimeUnit;
/**
* 从数据库中读取信⽤数据,套⽤模型,并把结果进⾏记录和传输
*
* 启动时加⼊以下参数: -Xms200M -Xmx200M -XX:+UseParallelGC -XX:+PrintGC -XX:+HeapDumpOnOutOfMemoryError
* 发现启动后会频繁GC,最后导致OOM(OutOfMemoryError)
*/
public class T15_FullGC_Problem01 {
private static class CardInfo {
BigDecimal price = new BigDecimal(0.0);
String name = "张三";
int age = 5;
Date birthdate = new Date();
public void m() {}
}
private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,
new ThreadPoolExecutor.DiscardOldestPolicy());
/**
* main⽅法
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
executor.setMaximumPoolSize(50);
// 为什么是死循环?因为在⽣产环境中会有源源不断的数据需要处理,我们⽆法模拟线上环境,所以⽤死循环代替;
for (;;){
for (;;){
modelFit();
Thread.sleep(100);
}
}
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
// do something
executor.scheduleWithFixedDelay(() -> {
/
/do sth with info
info.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
private static List<CardInfo> getAllCardInfo(){
List<CardInfo> taskList = new ArrayList<>();
for (int i = 0; i < 100; i++) {
CardInfo ci = new CardInfo();
taskList.add(ci);
}
return taskList;
}
}
启动
启动时加⼊参数-Xms200M -Xmx200M -XX:+UseParallelGC -XX:+PrintGC -XX:+HeapDumpOnOutOfMemoryError,这段代码运⾏后,⽼年代的内存占⽤会慢慢升⾼,待内存占⽤到达顶峰时,会频繁Full GC(回收⽼年代垃圾),直到撑爆内存,最后会导致OOM异常:Exception in thread "pool-1-thread-1" java.lang.OutOfMemoryError: GC overhead limit exceeded;也就是内存溢出;
运⾏监控
运⾏⼀段时间后,可以看到⼀直不停地full GC,并且有些线程的内存已经溢出报出OOM错误;
解决⽅案:使⽤arthas
在⽤arthas 的dashboard命令看⼀下内存使⽤情况,这⼀看,我的天,⽼年代的内存已经使⽤了98.64%,并且已经进⾏了3914次的GC,也就是说Full GC进⾏了3914次清理都没清掉那些垃圾;
到这时候我们就已经确定发现了内存泄漏,接下来的⼯作就是要到是那些顽固的对象没被清理调,然后在做出相应的调整;
⾸先要分析堆内存有哪些对象,这⾥使⽤到⼀个arthas的⼯具:heapdump,这个命令类似jdk的jmap,使⽤arthas导出堆转储⽂件命令;
[arthas@28747]$ heapdump --live /Users/mac/Downloads/dump.hprof
Dumping heap to /Users/mac/Downloads/dump.hprof ...
Heap dump file created
导出后是⼀个⼆进制⽂件,这个⽂件直接打开看到的是乱码的,所以我们需要借助⼀些⼯具,这边有2个选择,⽤jhat 和 jvisualVM,因为jhat⽤的不多,所以我们⽤⼤家常⽤的jvisualVM.
使⽤jvisual VM加载堆转储⽂件dump.hprof后如下图:
由此结果可以看到,Date对象和Bigdecimal对象⼀直⽆法回收,⽽每⼀个定时任务就会创建⼀个Date对象和Bigdecimal对象,到现在为⽌已经有55万个了,因为只增不减,撑爆内存是迟早的事。
解决⽅案⼆:使⽤jmap
java arraylist用法先使⽤jps命令到正在运⾏的java进程id
macdeMacBook-Pro:Downloads mac$ jps
24240 App
29410 Launcher
29411 T15_FullGC_Problem01
2851
29414 Jps
29031 Main
我们运⾏的类为T15_FullGC_Problem01,对应的进程id为29411,记住这个进程id;
接着⽤jmap命令查看这个进程id,看看内存占⽤排⾏前⼗的对象有哪些:
jmap -histo:live 29411| head -10
-histo:live :表⽰只查看存活的对象
head - 10 :这是linux⾃带的命令,表⽰查看头部前⼗⾏内容;
运⾏后,结果和上⾯的jvisual分析结果差不多,Date对象和Bigdecimal对象都是在CardInfo⽅法⾥⾯的。所以排⾏最⾼的就是这三个;
macdeMacBook-Pro:Downloads mac$ jmap -histo:live 29411| head -10
num #instances #bytes class name
----------------------------------------------
1: 447100 32191200 urrent.ScheduledThreadPoolExecutor$ScheduledFutureTask
2: 447126 17885040 java.math.BigDecimal
3: 447100 14307200 T15_FullGC_Problem01$CardInfo
4: 447100 10730400 java.util.Date
5: 447100 10730400 urrent.Executors$RunnableAdapter
6: 447100 7153600 T15_FullGC_Problem01$$Lambda$2/664223387
7: 1 2396432 [urrent.RunnableScheduledFuture;
为什么Date和Bigdecimal对象没被回收
是因为modelFit()⽅法出现了问题,
private static void modelFit(){
List<CardInfo> taskList = getAllCardInfo();
taskList.forEach(info -> {
// do something
executor.scheduleWithFixedDelay(() -> {
//do sth with info
info.m();
}, 2, 3, TimeUnit.SECONDS);
});
}
仔细看这个⽅法内的代码,关于这段代码可以有2种解释,
1、taskList链接着info对象
info是taskList中的元素,每个info元素都taskList这个对象所引⽤着,每次定时任务执⾏完后,线程内的对象都会被垃圾回收器清理掉,但是info这个对象不属于定时任务线程内的对象,所以没被清理掉;按理说taskList内的所有对象都遍历完了之后,应该会将taskList给清除掉,但是taskList还有个别元素在线程中,他们之间的引⽤还在,既然有引⽤,也就⾃然不会被清理;引⽤关系如下图
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论