资讯专栏INFORMATION COLUMN

记一次 JAVA 的内存泄露分析

Tecode / 628人阅读

摘要:展示如下场景再现经过分析,最后我们定位到是使用产生的内存泄露问题。下面通过一个,来简单讲下具体内存泄露的原因。这一次的内存泄露问题算是解决了。总结关于内存泄露问题在第一次排查时,往往是有点不知所措的。

记一次 JAVA 的内存泄露分析

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

当前环境

jdk == 1.8

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 依赖


    org.apache.httpcomponents
    httpasyncclient
    4.1.3

HttpAsyncClient 客户端

public class HttpAsyncClient {

    private CloseableHttpAsyncClient httpclient;

    public HttpAsyncClient() {
        httpclient = HttpAsyncClients.createDefault();
        httpclient.start();
    }

    public void execute(HttpUriRequest request, FutureCallback 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 cache1 = replay1.loadMockRequest(10000);
        
        //开始循环回放
        replay1.start(cache1);

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

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

public class ReplayWithProblem {

    public List loadMockRequest(int n){
    
        List cache = new ArrayList(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 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() {
                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 cache1中会保存回调对象,所以我们不能缓存请求类,只能缓存基本数据,在使用时进行动态的生成,来保证回调对象的及时回收。

代码如下:

public class ReplayApplication {

    public static void main(String[] args) throws InterruptedException {

        ReplayWithoutProblem replay2 = new ReplayWithoutProblem();
        List cache2 = replay2.loadMockRequest(10000);
        replay2.start(cache2);

    }
}
public class ReplayWithoutProblem {

    public List loadMockRequest(int n){
        List cache = new ArrayList(n);
        for (int i = 0; i < n; i++) {
            cache.add("http://www.baidu.com?a="+i);
        }
        return cache;
    }

    public void start(List 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() {
                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 的机率。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/67681.html

相关文章

  • Android: 一次Android内存泄露

    摘要:关于内存泄露内存泄漏也称作存储渗漏,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。 关于内存泄露 内存泄漏也称作存储渗漏,用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。(摘自度娘) 内存泄露了 在一次项目的开发中,代码的健壮性以及重用性尤为...

    marek 评论0 收藏0
  • 一次OkHttpClient导致线程过多排查

    摘要:首先先解读下这个报警内容,原因活跃线程数过多,是监听的端口号用来获取虚拟机各项信息,代表着此时的线程数,是设置的报警阈值。 前言 前天,一位21世纪的好好青年正在工位上默念社会主义大法好的时候,钉钉上又报警了(公司项目接入了open-faclon监控,指标不正常会报警给钉钉的机器人),无奈默默流泪挥手告别社会主义大法开始定位线上问题。 报警内容 首先我们先来看下报警信息,为防止泄露公...

    tianyu 评论0 收藏0
  • 一次Redis数据库配置导致连接数泄露问题

    摘要:首先,修改了下数据库配置成功重启之后,重新运行前面的监控脚本,以便观察修复后情况,初步可以确认这下服务器端和客户端的连接数一致了再又经过几天的脚本自动采集数据后分析,系统又恢复平稳运行了,连接数一直稳定在理论最大连接数之下。 问题背景 去年圣诞节当天,突然收到一个我经手过的项目的告警邮件,错误消息显示Redis::CommandError: ERR max number of clie...

    Anleb 评论0 收藏0
  • 一次JVM调优

    摘要:现象登入生产环境,使用命令因为这时候并没有打的,所以只能观察现象。其他的可以根据这个类推,是内纯的占用量。 前言 我们的游戏上线之初,经常有玩家反馈卡,或者有网络延迟等现象,造成用户流失等现象,这时候我就想到是不是可能是之前的jvm配置有问题,或者存在内存泄露等问题。 现象 登入生产环境,使用命令,因为这时候并没有打gc的log,所以只能观察现象。 jstat -gcutil 270...

    sugarmo 评论0 收藏0
  • 一次线上频繁FGC事件和解决方式

    摘要:直接显示了一个疑似内存泄漏的问题。然后分析文件给出的信息,发现一个叫的类。文件里面说的内存泄漏的大概的意思就是说,这个类里面的存放的东西太多了,爆掉了。修改了代码将调用的地方改成了单例。修改完线上跑了一段日子,后来也没有出现过这样的问题。 问题描述:     早上去公司上班,突然就邮件一直报警,接口报异常,然后去查服务器的运行情况,发现java的cpu爆了.接着就开始排查问题 问题解决...

    Alliot 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<