当前环境

  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简单内存泄露分析及解决

    一.问题产生    项目采用Tomcat6.0为服务器,数据库为mysql5.1,数据库持久层为hibernate3.0,以springMVC3.0为框架,项目开发完成后,上线前夕进行稳定性拷机,测试 ...

  2. java中内存泄露有几种?如何分析泄露原因

    一.Java内存回收机制 不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址.Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(Hea ...

  3. Android Studio 使用Memory Monitor进行内存泄露分析

    在使用Android Studio进行内存泄露分析之前,我们先回顾一下Java相关的内存管理机制,然后再讲述一下内存分析工具如何使用. 一.Java内存管理机制 1. Java内存分配策略 Java ...

  4. 关于内存泄露分析插件 MAT 的用法

    关于内存泄露分析插件 MAT 的用法,建议大家有时间看一下,下面的文章 http://www.blogjava.net/rosen/archive/2010/05/21/321575.html htt ...

  5. 学会用Clang来进行内存泄露分析

    最近项目出现了内存泄露的问题,对于PC x86平台来说,一点点的内存泄露往往不会出错,很难进行debug调试.这个时候我们可以用到苹果给我们带来的神器--Clang编译器来进行内存泄露分析检测,开关打 ...

  6. java造成内存泄露原因

    一.Java内存回收机制  不论哪种语言的内存分配方式,都需要返回所分配内存的真实地址,也就是返回一个指针到内存块的首地址.Java中对象是采用new或者反射的方法创建的,这些对象的创建都是在堆(He ...

  7. Java的内存泄露

    Java的内存泄露 1.静态集合类引起内存泄漏 像HashMap.Vector等的使用最容易出现内存泄露,这些静态变量的生命周期和应用程序一致,他们所引用的所有的对象Object也不能被释放,因为他们 ...

  8. java的内存泄露是如何发生的,如何避免和发现

    java的垃圾回收与内存泄露的关系:[新手可忽略不影响继续学习] 马克-to-win:上一节讲了,(i)对象被置成null.(ii)局部对象(无需置成null)当程序运行到右大括号.(iii)匿名对象 ...

  9. Java内存泄露分析和解决方案及Windows自带查看工具

    Java内存泄漏是每个Java程序员都会遇到的问题,程序在本地运行一切正常,可是布署到远端就会出现内存无限制的增长,最后系统瘫痪,那么如何最快最好的检测程序的稳定性,防止系统崩盘,作者用自已的亲身经历 ...

随机推荐

  1. 哪有Python视频百度云链接?

    说到Python视频哪个好,小编在这里不好下结论.毕竟孰好孰坏,只有合适的才是最好的.就像买鞋子,一双鞋子好不好,只有合脚才是真的好.不过,这里有些个人认为通俗易懂的Python视频可以分享给大家. ...

  2. wireshark 随笔

    在进行通信开发的过程中,我们往往会把本机既作为客户端又作为服务器端来调试代码,使得本机自己和自己通信.但是wireshark此时是无法抓取到数据包的,需要通过简单的设置才可以. 具体方法如下: ①:以 ...

  3. CSS display使用

    .今天我们来分析一下display的一些用法,display样式在我们整个CSS设置中,非常重要,掌握好display,才能有效的解决CSS布局的问题,在理解display之前,我们先了解两个概念:块 ...

  4. jvm系列 (四) ---强、软、弱、虚引用

    java引用 目录 jvm系列(一):jvm内存区域与溢出 jvm系列(二):垃圾收集器与内存分配策略 jvm系列(三):锁的优化 我的博客目录 为什么将引用分为不同的强度 因为我们需要实现这样一种情 ...

  5. 我的Java起源

    2015年九月八号我和我爸一同去Java的培训学校,我是一名读了高一就不读的学生.抛弃高中就一个理由--不喜欢,是的,我不喜欢高中的课程.我不理解我一辈子不会出地球为什么我要学习那些内容,如果我就自己 ...

  6. 如何快速把 Vue 项目升级到 webpack3

    由于 webpack3升级后,新增了 Scope Hositing(作用域提升) 功能,据说是对 js的性能提升很大.因此,我们做了一个测试对比,就是 webpack3和 webpack1/2 的性能 ...

  7. 01-TypeScript概述

    本篇文章向大家介绍新的TypeScript客户端脚本语言,主要涉及两个方面,一是传统JavaScript语言的弱点,二是TypeScript语言的优势. 一.JavaScript的弱点 1.弱类型,缺 ...

  8. 交换机端口呈现err-disable的原因

    导致交换机端口呈现err-disable状态的原因有很多,为方便大家查询,特归纳如下:   1. duplex mismatch (A is correct) 2. port-channel misc ...

  9. UX是什么?

    UX(用户体验),操作过安卓手机或者苹果手机的系统吧?那么操作过程的整体体验就叫UX,而操作过程中所看到的界面颜色啦,图案,字体大小啦等等都属于UI设计,而交互设计(Interaction Desig ...

  10. 第二次项目冲刺(Beta阶段)5.24

    1.提供当天站立式会议照片一张 会议内容: ①检查前一天的任务情况. ②制定新一轮的任务计划. 2.每个人的工作 (1)工作安排 队员 今日进展 明日安排 王婧 #63Web输出以文件名为标题 #63 ...