记一次 JAVA 的内存泄露分析

摘要:本文属于原创,欢迎转载,转载请保留出处:https://github.com/jasonGeng88/blog

当前环境

  1. jdk == 1.8
  2. httpasyncclient == 4.1.3

代码地址

git 地址:https://github.com/jasonGeng88/java-network-programming

背景

前不久,上线了一个新项目,这个项目是一个压测系统,可以简单的看做通过回放词表(http请求数据),不断地向服务发送请求,以达到压测服务的目的。在测试过程中,一切还算顺利,修复了几个小bug后,就上线了。在上线后给到第一个业务方使用时,就发现来一个严重的问题,应用大概跑了10多分钟,就收到了大量的 Full GC 的告警。

针对这一问题,我们首先和业务方确认了压测的场景内容,回放的词表数量大概是10万条,回放的速率单机在 100qps 左右,按照我们之前的预估,这远远低于单机能承受的极限。按道理是不会产生内存问题的。

线上排查

首先,我们需要在服务器上进行排查。通过 JDK 自带的 jmap 工具,查看一下 JAVA 应用中具体存在了哪些对象,以及其实例数和所占大小。具体命令如下:

jmap -histo:live `pid of java`# 为了便于观察,还是将输出写入文件
jmap -histo:live `pid of java` > /tmp/jmap00

经过观察,确实发现有对象被实例化了20多万,根据业务逻辑,实例化最多的也就是词表,那也就10多万,怎么会有20多万呢,我们在代码中也没有找到对此有显示声明实例化的地方。至此,我们需要对 dump 内存,在离线进行进一步分析,dump 命令如下:

jmap -dump:format=b,file=heap.dump `pid of java`

离线分析

从服务器上下载了 dump 的 heap.dump 后,我们需要通过工具进行深入的分析。这里推荐的工具有 mat、visualVM。

我个人比较喜欢使用 visualVM 进行分析,它除了可以分析离线的 dump 文件,还可以与 IDEA 进行集成,通过 IDEA 启动应用,进行实时的分析应用的CPU、内存以及GC情况(GC情况,需要在visualVM中安装visual GC 插件)。工具具体展示如下(这里仅仅为了展示效果,数据不是真的):

当然,mat 也是非常好用的工具,它能帮我们快速的定位到内存泄露的地方,便于我们排查。
展示如下:

场景再现

经过分析,最后我们定位到是使用 httpasyncclient 产生的内存泄露问题。httpasyncclient 是 Apache 提供的一个 HTTP 的工具包,主要提供了 reactor 的 io 非阻塞模型,实现了异步发送 http 请求的功能。

下面通过一个 Demo,来简单讲下具体内存泄露的原因。

httpasyncclient 使用介绍:

  • maven 依赖
<dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpasyncclient</artifactId><version>4.1.3</version>
</dependency>
  • HttpAsyncClient 客户端
public class HttpAsyncClient {private CloseableHttpAsyncClient httpclient;public HttpAsyncClient() {httpclient = HttpAsyncClients.createDefault();httpclient.start();}public void execute(HttpUriRequest request, FutureCallback<HttpResponse> callback){httpclient.execute(request, callback);}public void close() throws IOException {httpclient.close();}}

主要逻辑:

Demo 的主要逻辑是这样的,首先创建一个缓存列表,用来保存需要发送的请求数据。然后,通过循环的方式从缓存列表中取出需要发送的请求,将其交由 httpasyncclient 客户端进行发送。

具体代码如下:

public class ReplayApplication {public static void main(String[] args) throws InterruptedException {//创建有内存泄露的回放客户端ReplayWithProblem replay1 = new ReplayWithProblem();//加载一万条请求数据放入缓存List<HttpUriRequest> cache1 = replay1.loadMockRequest(10000);//开始循环回放replay1.start(cache1);}
}

回放客户端实现(内存泄露):

这里以回放百度为例,创建10000条mock数据放入缓存列表。回放时,以 while 循环每100ms 发送一个请求出去。具体代码如下:

public class ReplayWithProblem {public List<HttpUriRequest> loadMockRequest(int n){List<HttpUriRequest> cache = new ArrayList<HttpUriRequest>(n);for (int i = 0; i < n; i++) {HttpGet request = new HttpGet("http://www.baidu.com?a="+i);cache.add(request);}return cache;}public void start(List<HttpUriRequest> cache) throws InterruptedException {HttpAsyncClient httpClient = new HttpAsyncClient();int i = 0;while (true){final HttpUriRequest request = cache.get(i%cache.size());httpClient.execute(request, new FutureCallback<HttpResponse>() {public void completed(final HttpResponse response) {System.out.println(request.getRequestLine() + "->" + response.getStatusLine());}public void failed(final Exception ex) {System.out.println(request.getRequestLine() + "->" + ex);}public void cancelled() {System.out.println(request.getRequestLine() + " cancelled");}});i++;Thread.sleep(100);}}}

内存分析:

启动 ReplayApplication 应用(IDEA 中安装 VisualVM Launcher后,可以直接启动visualvm),通过 visualVM 进行观察。

  • 启动情况:

  • visualVM 中前后3分钟的内存对象占比情况:


说明:$0代表的是对象本身,$1代表的是该对象中的第一个内部类。所以ReplayWithProblem$1: 代表的是ReplayWithProblem类中FutureCallback的回调类。

从中,我们可以发现 FutureCallback 类会被不断的创建。因为每次异步发送 http 请求,都是通过创建一个回调类来接收结果,逻辑上看上去也正常。不急,我们接着往下看。

  • visualVM 中前后3分钟的GC情况:


从图中看出,内存的 old 在不断的增长,这就不对了。内存中维持的应该只有缓存列表的http请求体,现在在不断的增长,就有说明了不断的有对象进入old区,结合上面内存对象的情况,说明了 FutureCallback 对象没有被及时的回收。

可是该回调匿名类在 http 回调结束后,引用关系就没了,在下一次 GC 理应被回收才对。我们通过对 httpasyncclient 发送请求的源码进行跟踪了一下后发现,其内部实现是将回调类塞入到了http的请求类中,而请求类是放在在缓存队列中,所以导致回调类的引用关系没有解除,大量的回调类晋升到了old区,最终导致 Full GC 产生。

  • 核心代码分析:

代码优化

找到问题的原因,我们现在来优化代码,验证我们的结论。因为List<HttpUriRequest> cache1中会保存回调对象,所以我们不能缓存请求类,只能缓存基本数据,在使用时进行动态的生成,来保证回调对象的及时回收。

代码如下:

public class ReplayApplication {public static void main(String[] args) throws InterruptedException {ReplayWithoutProblem replay2 = new ReplayWithoutProblem();List<String> cache2 = replay2.loadMockRequest(10000);replay2.start(cache2);}
}
public class ReplayWithoutProblem {public List<String> loadMockRequest(int n){List<String> cache = new ArrayList<String>(n);for (int i = 0; i < n; i++) {cache.add("http://www.baidu.com?a="+i);}return cache;}public void start(List<String> cache) throws InterruptedException {HttpAsyncClient httpClient = new HttpAsyncClient();int i = 0;while (true){String url = cache.get(i%cache.size());final HttpGet request = new HttpGet(url);httpClient.execute(request, new FutureCallback<HttpResponse>() {public void completed(final HttpResponse response) {System.out.println(request.getRequestLine() + "->" + response.getStatusLine());}public void failed(final Exception ex) {System.out.println(request.getRequestLine() + "->" + ex);}public void cancelled() {System.out.println(request.getRequestLine() + " cancelled");}});i++;Thread.sleep(100);}}}

结果验证

  • 启动情况:

  • visualVM 中前后3分钟的内存对象占比情况:


  • visualVM 中前后3分钟的GC情况:


从图中,可以证明我们得出的结论是正确的。回调类在 Eden 区就会被及时的回收掉。old 区也没有持续的增长情况了。这一次的内存泄露问题算是解决了。

总结

关于内存泄露问题在第一次排查时,往往是有点不知所措的。我们需要有正确的方法和手段,配上好用的工具,这样在解决问题时,才能游刃有余。当然对JAVA内存的基础知识也是必不可少的,这时你定位问题的关键,不然就算工具告诉你这块有错,你也不能定位原因。

最后,关于 httpasyncclient 的使用,工具本身是没有问题的。只是我们得了解它的使用场景,往往产生问题多的,都是使用的不当造成的。所以,在使用工具时,对于它的了解程度,往往决定了出现 bug 的机率。

记一次 JAVA 的内存泄露分析相关推荐

  1. java dump内存泄露分析方法

    1.导出dump文件 jmap -dump:format=b,file=/Users/shurrik/heapdump.out 484 484是pid,用jps命令获得 2.导入dump文件分析,这里 ...

  2. java内存泄露分析方案

    java内存泄露分析方案 - 准备工作 1.工具:Memory Analyzer Tool (mat); 1)安装Memory Analyzer Tool (mat) 2.原料:dump.hprof ...

  3. java thread 内存泄露_Java ThreadLocal 内存泄露问题分析及解决方法。

    前言 在分析ThreadLocal导致的内存泄露前,需要普及了解一下内存泄露.强引用与弱引用以及GC回收机制,这样才能更好的分析为什么ThreadLocal会导致内存泄露呢?更重要的是知道该如何避免这 ...

  4. java thread 内存泄露_记一次ThreadLocal引发的内存泄露

    概念 ​首先解释下内存溢出和内存泄露的概念.内存溢出一般指的是out of memory,也就是我们经常说的OOM,常发生在堆,方法区和方法栈.内存泄露指的是一段程序在申请内存空间后,无法释放已经申请 ...

  5. JAVA内存泄露分析和解决方案及WINDOWS自带查看工具

    JAVA内存泄露分析和解决方案及WINDOWS自带查看工具 Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最 ...

  6. 如何使用MAT进行JVM内存泄露分析

    转载自  如何使用MAT进行JVM内存泄露分析 在<Java Agent的隔离实现以及卸载时一些坑>中,卸载Agent之后,使用 jmap-histo:live pid命令验证执行FGC, ...

  7. java查看内存泄露_Java内存泄露如何排查

    Java内存泄露是常常出现的问题,Java攀登网进行了该问题的整理,具体的如下所示: 1.2 内存泄露Memory Leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内 存泄露危害可以忽略 ...

  8. java 代码 内存泄露_如何用Java编写一段代码引发内存泄露

    Q:刚才我参加了面试,面试官问我如何写出会发生内存泄露的Java代码.这个问题我一点思路都没有,好囧. A1:通过以下步骤可以很容易产生内存泄露(程序代码不能访问到某些对象,但是它们仍然保存在内存中) ...

  9. 一次.net托管内存泄露分析

    简介:一次.net托管内存泄露分析 最近协助分析了一个.net进程内存泄露的问题,过程分享给大家. 症状:客户的服务端.net进程出现分钟级的cpu抖动,接近100%后落回. 图1 分析:支持同学通过 ...

最新文章

  1. OKR怎么使用比较好?
  2. 在centos下启动nginx出现Failed to start nginx.service:unit not found
  3. [codevs 3273] 两圆的交
  4. 数据结构学习笔记(六)链表算法题
  5. 第18课 闰年与平年 《小学生C++趣味编程》
  6. established 太多_ss -s closed过多,NON_ESTABLISHED告警
  7. 不同语言编程能整合到一起吗_学习编程入门指南
  8. LeetCode 135. 分发糖果(贪心算法)
  9. 电子产品EMC不合格,如何整改?
  10. 1037u处理器搭载文件服务器,悦升IVB 赛扬1037U工控主板 满足多行业需求
  11. 服务器大线程有什么作用,全面剖析超线程技术优点与缺点
  12. 注塑模具与吹塑模具的区别
  13. oracle 按时间每五分钟分割,Oracle 5分钟或30分钟分割方法
  14. c语言汇编混合编译不了,IAR汇编与C语言混合编程的问题(内附源程序)
  15. 2021年安全员-C证(陕西省)考试资料及安全员-C证(陕西省)新版试题
  16. 黄金票据的制作与使用
  17. 基于Arduino Uno开发板的红外遥控开发
  18. 米莱虾_三年之期_创作纪念
  19. 电影网站国内服务器行么,做电影网站用什么服务器,可不可以用香港主机
  20. 2021年4月总结5月计划

热门文章

  1. Spring Cloud教程 (二)应用程序上下文服务层次结构
  2. django -- 联合索引
  3. (转)STORM启动与部署TOPOLOGY
  4. 【Android】3.24 示例24--OpenGL绘制功能
  5. JS表单学习笔记(思维导图)
  6. Tab与TabHost
  7. CISCO路由器的备份与还原(1)
  8. hibernate 一对多_为什么很多人不愿意用hibernate了?
  9. 有关ArrayList增加Map引发的一个BUG
  10. Android开发学习之卡片式布局的简单实现