内存泄露Bug现场

线上某外围链路服务的一个节点疯狂GC,监控图如下:

均匀1分钟触发CMS GC 36次,已无奈失常解决线上申请。

筹备工作

发现该节点有问题后,找运维将该节点从服务注册核心上摘掉,因为咱们须要去jmap dump服务的堆栈信息,而dump内存会STW,必须先摘流。

dump命令如下:

jmap -dump:format=b,file=heap.bin [pid]

dump好当前gzip压缩,便于文件传输到本地,从2G压缩到300+M左右。

剖析

将dump的文件导入到MAT中,MAT内存分布图如下:

1.2G的内存都被AccountChangeTask中的ConcurrentHashMap对象占用了,那思路就很清晰了,去查看代码中什么中央应用了 这个AccountChangeTask对象。

AccountChangeTask的整体构造如下:

@Servicepublic class AccountChangeTask {// 缓存SQL和tableName映射关系    private static final Map<String, String> sqlMap = new ConcurrentHashMap<>();        @Async    public void processTask(String sql) {        // 对sqlMap对象的get put操作,key是SQL,value是表名        // 起因是逻辑中有一些对SQL做正则解析的操作,可能比拟耗时和耗CPU,所以想通过缓存优化        ... 其余业务逻辑    }}

咱们再去查找应用了AccountChangeTask.processTask()办法的中央,代码如下:

@Intercepts({        @Signature(type = Executor.class, method = "update", args = { MappedStatement.class, Object.class }),        @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class,                RowBounds.class, ResultHandler.class }) })@Componentpublic class MybatisInterceptor implements Interceptor {        @Autowired    private AccountChangeTask accountChangeTask;    @Override    public Object intercept(Invocation invocation) throws Throwable {        // 省略...        String sql = showSql(config,boundSql)    // 填充为实在的SQL,将?填充为实在的SQL参数        // 业务逻辑判断,如果true走上面逻辑        accountChangeTask.processTask(sql);    }}

问题解决

剖析代码,起因就是缓存在ConcurrentHashMap中的SQL是被参数填充过的SQL,而线上环境的sql参数变幻无穷,有不同uid和工夫等等,申请量一上来就把ConcurrentHashMap撑爆了。

解决思路其实也很简略:在对性能没有极致要求的状况下,移除代码中对SQL的缓存;而间接走正则逻辑 并且 提前对正则表达式做好编译,可能是更正当的抉择。

总结

在没有极致性能要求的状况下,简化咱们的设计,服务可能会更具健壮性。