java内存泄露分析实战

问题背景描述

数据服务是通过SQL对外提供数据查询的服务平台,底层存储支持HBase和MySQL两种。用户首先在管理平台上配置好接口的SQL详情


SQL语句配置

业务方通过微服务接口根据生成的ID以及接口参数来完成数据的查询,由于HBase不支持SQL引擎的查询,我们基于calcite实现了一套简单的SQL On HBase解析逻辑。
服务发布以来已经支持了近两百个数据服务接口,在此期间系统运行均没有出现任何问题,响应延迟、吞吐量、并发量、机器资源利用率均在预期范围内。最近接了一个大的线上场景,高峰期内系统经常Full GC,请求响应经常超时,更有甚者,系统常常因为GC出现假死。
短期内先通过加大内存可以缓解问题发生的频率,但是偶尔也会有些机器出现频繁FGC。问题虽然得到缓解,但毕竟是个定时炸弹,需要分析其根本原因进行修复才能一劳永逸。

逻辑主要描述

逻辑主要分为下面步骤:

  1. 连接池中维护了HBase的连接(五分钟连接超时重建)
  2. 每次请求根据接口ID和输入参数拼接成完整的SQL语句
  3. 利用Calcite的Statement来执行每次请求
    连接代码如下
  Class.forName("org.apache.calcite.jdbc.Driver");
        info = new Properties();
        info.setProperty("lex", "JAVA");
        log.info("Init HBase connection manager, refresh period [{}].", refreshPeriod);
        connCache = CacheBuilder.newBuilder()
                .refreshAfterWrite(refreshPeriod, TimeUnit.MINUTES)
                .build(new CacheLoader<Integer, ConnectionWrapper>() {
                    @Override
                    public ConnectionWrapper load(Integer key) throws Exception {
                        long startTime = System.currentTimeMillis();
                        Connection connection = DriverManager.getConnection("jdbc:calcite:", info);
                        CalciteConnection calciteConn = connection.unwrap(CalciteConnection.class);
                        SchemaPlus rootSchema = calciteConn.getRootSchema();
                        int udfSize;
                        // 完成udf的schema注册
                        synchronized (spiFetcher.getUdfMap()) { // TODO 遍历与put操作之间的并发安全性待查
                            for (SpiFetcher.UDFInfo udfInfo : spiFetcher.getUdfMap().values()) {
                                // 注意,这里会将udfInfo.getName() + function的参数列表形成function signature,以便在sql中使用
                                rootSchema.add(udfInfo.getName(), udfInfo.getFunction());
                            }
                            udfSize = spiFetcher.getUdfMap().size();
                        }

                        VirtualTable<HbaseExtra> virtualTable = virtualTableService.select(key, HbaseExtra.class);
                        if (StringUtils.isBlank(virtualTable.getDb())) {
                            rootSchema.add(virtualTable.getTb(), new HBaseFilterableTable(HBaseConnManager.this.connection, virtualTable));
                        } else {
                            SchemaPlus newSchema = rootSchema.add(virtualTable.getDb(), new AbstractSchema());
                            newSchema.add(virtualTable.getTb(), new HBaseFilterableTable(HBaseConnManager.this.connection, virtualTable));
                        }
                        log.info("HBase connection cache load connection, key: {}, db: {}, tb: {}, elapse: {}ms.", key, virtualTable.getDb(), virtualTable.getTb(), System.currentTimeMillis() - startTime);
                        return ConnectionWrapper.builder()
                                .connection(connection)
                                .version(virtualTable.getGmtModify().getTime())
                                .udfSize(udfSize)
                                .build();
                    }

                   

查询过程

 public <T> T query(Integer tbId, String queryStr, Handler handler) throws SQLException {
        Connection connection = connCache.getUnchecked(tbId).getConnection();
        try (Statement st = connection.createStatement();
             ResultSet rs = st.executeQuery(queryStr)) {
            return handler.handle(rs);
        } catch (SQLException e) {
            throw e;
        }

上面的代码符合正常的逻辑,资源也会及时释放,按理不会出现内存泄露的问题。

问题定位

内存发生泄露,经过相关jtools工具定位无果后,决定采用MAT工具进行分析。
MAT(Memory Analysis Tool)是一款专门用来Java内存分析检测的工具,具体大家可以去官网查看更详细的使用说明。
MAT使用详情
通过jmap -dump命令生成程序的内存镜像(此操作对线上程序有一定影响)。利用MAT工具打开下载生成的hprof文件,文件打开时候开启内存泄露检测选项。

内存自动分析

内存检测报告

内存检测

从图中可以发现VolcanoPlanner对象占用内存空间最多,占用近93.72%。点击overview页面Dominator Tree按钮来分析对象的引用关系
对象层次

对象引用链路
搜索出VolcanoPlanner对象,右键选择查看所有的引用关系。

对象引用链路

对象引用详情分析

对象详情分析

左侧加粗的字体是字段的名称,如第一行的planner表名VolcanoPlanner是RelOptCluster对象中planner字段引用的,以此类推,可以找到最上层的引用在CalciteJdbc41Factoryxx中的statementMap。这时候我们基本上梳理出了对象的引用路径关系。
问题在于为什么这个map还持有这个引用?
下载calcite的源代码来分析相关情况,找到statementMap什么时候会释放相关的Statement。

  protected void close_() {
    if (!closed) {
      closed = true;
      if (openResultSet != null) {
        AvaticaResultSet c = openResultSet;
        openResultSet = null;
        c.close();
      }
      try {
        // inform the server to close the resource
        connection.meta.closeStatement(handle);
      } finally {
        // make sure we don't leak on our side
        connection.statementMap.remove(handle.id);
        connection.flagMap.remove(handle.id);
      }
      // If onStatementClose throws, this method will throw an exception (later
      // converted to SQLException), but this statement still gets closed.
      connection.driver.handler.onStatementClose(this);
    }
  }

程序会在close_方法中释放对statement的引用,继续往上找什么场景下这个方法会被调用,我们很简单的就能找到其调用的地方。AvaticaStatement.close_:

protected void close_() {
    if (!closed) {
      closed = true;
      if (openResultSet != null) {
        AvaticaResultSet c = openResultSet;
        openResultSet = null;
        c.close();
      }
      try {
        // inform the server to close the resource
        connection.meta.closeStatement(handle);
      } finally {
        // make sure we don't leak on our side
        connection.statementMap.remove(handle.id);
        connection.flagMap.remove(handle.id);
      }
      // If onStatementClose throws, this method will throw an exception (later
      // converted to SQLException), but this statement still gets closed.
      connection.driver.handler.onStatementClose(this);
    }
  }

在前面的查询中我们可以看到利用try-catch的相关特性会关闭相关的连接数。那为什么此处没有调用呢?左思右想都觉得不可能,根据经验,很有可能是其子类复写了相关的方法。
最终可以找到在CalciteStatement类中有该方法的override

  @Override protected void close_() {
    if (!closed) {
      closed = true;
      final CalciteConnectionImpl connection1 =
          (CalciteConnectionImpl) connection;
      connection1.server.removeStatement(handle);
      if (openResultSet != null) {
        AvaticaResultSet c = openResultSet;
        openResultSet = null;
        c.close();
      }
      // If onStatementClose throws, this method will throw an exception (later
      // converted to SQLException), but this statement still gets closed.
      connection1.getDriver().handler.onStatementClose(this);
    }
  }

可以看到,此处并没有释放相关的statement对象,怀疑就是此处的问题,我们自己代码中手动释放statement,然后观察整个内存的变化,发生每次GC后内存均会回到40%左右的水位线,测试一天后也没有出现内存泄露的问题,问题得到解决。
去calcite issuse中没有找到相关的issue,查看下最新的版本,该问题得到了解决(override方法去掉了,其实不知道当初作者为什么要重新这个方法,其实这个方法里面释放的资源都是父类中创建的资源,本着谁创建谁释放的原则,此处无必要重写)。

回顾总结

上述内容主要的目的是希望借助这个场景说明下怎么来定位常见的内存泄露的问题,希望读者能够从中学习常见的定位方法和思路,真正能够应用到自己遇到的问题中。同时在工作中,不要轻易的放过一个问题,一定要做到知根知底,找到问题的root cause。
思考题

  1. 既然出现内存泄露,为什么之前运行这么长时间均没有出现?
  2. 代码中还有哪些可以进一步优化的地方?
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,227评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,755评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,899评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,257评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,617评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,757评论 1 221
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,982评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,715评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,454评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,666评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,148评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,512评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,156评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,112评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,896评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,809评论 2 279
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,691评论 2 272

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,296评论 18 399
  • 关于Mongodb的全面总结 MongoDB的内部构造《MongoDB The Definitive Guide》...
    中v中阅读 31,793评论 2 89
  • 三重:代码、底层内存、源码 第一阶段:开发常用JavaSE基础、IDE、Maven、Gradle、SVN、Git、...
    guodd369阅读 15,915评论 1 44
  • 第一天 我心心念念要退回朋友位置的第一天 心不在焉 办公室哭了 此刻楼下的车里哭 刚刚一个模糊 车要开去哪里 我能...
    葓藻阅读 105评论 0 0
  • 与你相识春天的巧合 相识于梦里的风 三个月的相册记忆都是与你的种种 打开对话框 身边的世界都开始安静如水 听你的声...
    郑南鹿阅读 272评论 0 0