首先,发现线上某分析应用出现异常,连续好几天,一直没有分析数据产出。故登录到线上查看error.log日志,发现: java
首先,咱们使用jinfo pid
查看当前jvm的堆相关参数: 数组
接下来,咱们使用命令jstat -gcutil pid 1s 5
查看5秒内当前堆占用状况: bash
除了jstat命令外,咱们也可使用jmap -heap pid
查看下当前JVM堆状况: session
jmap -F -histo pid | head -n 13
查看前13行打印,即查看TOP10的大对象(最好用head限制一下,不然列出的对象会铺满你的屏幕,另外:强制链接参数
-F
对
jmap -histo:live
是无效的):
java.util.HashMap
和
java.util.ArrayList
很是显眼……先记下,后面继续分析先。
最后,咱们使用命令jmap -F -dump:file=a.bin pid
将堆dump出来,发现dump出来的文件有4.02G,很恐怖,故使用tar -czvf a.tar.gz a.bin
打包压缩一下!框架
将打包好的a.tar.gz
拉回到本地,并解压。可是因为a.bin过大,MAT打开确定会内存溢出,故调整MAT软件的最大堆内存:eclipse
[ MAT根目录下的MemoryAnalyzer.ini ]
-startup
plugins/org.eclipse.equinox.launcher_1.5.0.v20180512-1130.jar
--launcher.library
plugins/org.eclipse.equinox.launcher.win32.win32.x86_64_1.1.700.v20180518-1200
-vmargs
-Xmx6g
复制代码
-Xmx 改成6G!jvm
java.lang.Object[]
的
Details
,以下图:
jmap -histo pid | head -n 13
一个异常
ArrayList
的状况,同时它内部也正是由
HashMap
构成!
结合error.log日志报出的问题JAVA类
、报错代码行数
,再结合问题应该出在一个ArrayList上
,很容易就定位到了相关问题代码块:分布式
/** * 按照指定起止时间分析数据 * @param beginTime 起始时间 * @param endTime 截至时间 */
@Override public void exec(String beginTime, String endTime) {
List<Map<String, Object>> emlWithLoginList = new ArrayList<>();
List<Map<String, Object>> emlList = new ArrayList<>();
while (true) {
try {
//若是已分析到截至时间,则退出。
if (DateUtil.compareTime(beginTime, endTime) > 0) {
break;
}
//每次循环向前推10小时,YCYX_PERIOD=10小时
String tmpTime=DateUtil.addHours(beginTime, YCYX_PERIOD);
//1.构造请求
BoolQueryBuilder bqb = QueryBuilders.boolQuery();
bqb.must(QueryBuilders.rangeQuery(CREATE_TIME).gt(beginTime).lte(tmpTime));
bqb.must(QueryBuilders.termQuery(IS_DELETE, IS_FIELD_VALUE));
bqb.must(QueryBuilders.existsQuery(TOS));
bqb.must(QueryBuilders.existsQuery(FROMS));
bqb.must(QueryBuilders.existsQuery(SEND_TYPE));
bqb.must(QueryBuilders.existsQuery(SESSION_ID));
log.debug("emlAnalysis begin at " + beginTime + ", and end at " + lastTime);
SearchSourceBuilder requestBuilder = new SearchSourceBuilder().query(bqb).size(PAGE_SIZE).sort(CREATE_TIME, SortOrder.ASC).fetchSource(new String[] {"*"},
new String[] {EML_CONTENT});
//2.发起请求
EsHelper.getResponseScroll(EsCluster.DEFAULT, INF_EML_INDEX, "14m", requestBuilder.toString(), result -> {
//3.解析结果
EsSearchHit[] hits = result.getHits().getHits();
List<Map<String, Object>> loginList;
for (EsSearchHit hit : hits) {
//将邮件Map添加到列表中
Map<String, Object> emlMap = hit.getSource();
emlList.add(emlMap);
//将邮件+MailLogin的Map添加到另外一个列表中
Map<String, Object> emlWithLoginMap = new HashMap(emlMap);
String sessionId = emlMap.get(SESSION_ID).toString();
MailLogin mailLogin = EsService.getMailLoginBySessionId(sessionId);
emlWithLoginMap.put("Login_Key", mailLogin );
emlWithLoginList.add(emlWithLoginMap);
}
return true;
});
//4.A类检测逻辑
checkDSYJEml(emlWithLoginList);
emlWithLoginList.clear();
//5.B类检测逻辑
checkYCYXEml(emlList);
emlList.clear();
beginTime = tmpTime;
} catch (Throwable e) {
log.error("", e);
}
}
}
复制代码
由上面代码可见,问题List应该就是 emlWithLoginList
或者 emlList
的其中一个了,而两个List存储的内容基本相同,除了emlWithLoginList
内的Map元素额外存了一个叫作Login_Key
的key!ide
而经过MAT查看了问题List内Map元素的全部Key,并无找到相关叫作Login_Key
的元素,故推测问题List应该是这个emlList
! fetch
所以咱们先猜想,是不是由于当前时间段的ES数据过大致使?
代码里面能够看到,ES查询的数据,是经过CREATE_TIME(该常量值为@createtime)
进行升序查询的,故先查的数据,应该位于这个问题List
的开头,然后查的数据在问题List
的结尾。
所以经过MAT找到问题List
的第一个和最后一个元素,即获得本次查询的起、止时间:
图上说明,查询这一批数据,程序获得了31万条数据!
而我又到Kibana查询了一下这个时间段的数据量:
一会儿就迷了,这个状况有点说不通,想了半天,因而,又仔细地看了下代码:
clear()
方法清空List,为什么
emlWithLoginList
没有问题,而
emlList
却内存溢出了?
那么就很显然了:
checkYCYXEml(emlList)
时,出现了异常,就会直接被下面的catch
给捕获;emlList.clear()
代码,同时也不会走beginTime = timeTime
;beginTime = timeTime
,故查询的数据仍是以前的这个时间段的数据;emlWithLoginList
没有问题了,由于在异常代码的前面,能够进行正常的clear()
操做。那么,若是是checkYCYXEml(emlList)
时,出现了异常,error.log应该是有异常日志打印的,经过关键字checkYCYXEml
搜了一下:
按照上面说的,若是是这个缘由,这段代码会重复查询2019-11-01 16:00:00
到2019-11-02 02:00:00
的数据,且不断加入到emlList
中,最后撑爆JVM!
那么MAT中的这个问题List
应该会有多个相同元素存在(数据重复加入进去了嘛)。
如何验证这一点呢?
由于这些数据有一个emlkey
字段,是一个惟一主键,对应这条记录在ES中的_id
,所以可经过MAT,根据某一条数据的emlkey
,去查找是否问题emlList
中有多个元素均存在相同的emlkey
,便可证实。
-XX:+UseStringDeduplication
功能,来减轻重复字符串的问题。这将致使这几十万个String实例,但其底层的数组均指向同一个char数组。
图上便是对针对String进行的分组操做,这时,咱们随便找一个元素的emlkey
,查询一下:
merge shortest paths to gc roots
功能,可查看这些对象到GC ROOT的最短路径,说白了,就是想经过这个功能,查看当前这个String是属于哪一个对象下面的:
854742740486326718e
emlkey
属性,还有一个
document_id
属性,这两个值是同样的,均是表示ES的
_id
,如图:
最后这个问题,基本就定位到了,修复也就简单了,把两个Clear()方法,都移到catch后面的finally中,便可保证100%会调用,另外就是对接中间件的同事,针对IceTimeOut问题的解决了。