JVM内存分析:Aviator低版本内存泄漏问题分析

news2024/9/27 12:15:50

目录

1.频繁FullGC告警

2.堆转储操作生成dump文件

3.利用MAT工具分析dump文件

3.1 大对象视图分析内存泄漏原因

3.2 Aviator框架中什么地方用到ThreadLocal?

3.3 fnLocal为什么存在内存泄漏?

3.4 LambdaFunctionBootstrap为什么没有释放?

3.5 老年代内存占用曲线中,为什么内存占用越来越多(FullGC回收的低点逐渐抬高)?

4 解决方案


1.频繁FullGC告警

在实际项目开发过程中,我们会使用Aviator表达式引擎针对配置化的表达式进行求值计算;

项目在线上平稳运行一段时间后,收到一台机器频繁FullGC的告警,观察线上机器老年代使用情况如下:

 部署机器的jvm配置参数为: -Xmx4096m -Xms4096m -XX:MaxPermSize=512m -Xmn1512m

堆内存(4G),新生代(1512M),SurvivorRatio=8,所以Eden(1209.6M),

One Survivor(151.2M),老年代(2584M)

也可通过/usr/local/java8/bin/jmap -heap <pid> 命令查看内存分配情况

这里可以看到老年代为2584M

随着项目的持续运行,老年代逐步耗尽,且FullGC无法回收,出现内存泄漏问题,下面对该内存泄漏问题进行具体分析;

附:Aviator执行过程源码详见:Aviator源码:Aviator表达式引擎执行过程源码分析,可以较好的理解后续的引用链分析过程以及涉及的LambdaFunctionBootstrap和LambdaFunction对象含义。

2.堆转储操作生成dump文件

收到机器频繁FullGC告警之后,因为是分布式集群部署,首先禁用该问题机器的流量,规避对线上业务的影响,同时保存机器内存现场;然后通过堆转储命令生成dump文件,方便后续dump内存分析;

堆转储命令为:

/usr/local/java8/bin/jmap -dump:format=b,file=/tmp/heapdump.phrof <pid>

进行堆转储操作之前,也可以直接通过jmap命令查看大对象的具体情况,命令如下(简单的内存问题可以直接通过该命令分析出原因):

usr/local/java8/bin/jmap -histo <pid> | head -n20

3.利用MAT工具分析dump文件

3.1 大对象视图分析内存泄漏原因

首先在大对象分析视图中,可以看到存在3个比较大的业务线程,共占据了45%以上的内存空间,将近1G的内存占用;

经验分析,对于Thread空间占用较大,一般都是因为线程本地变量较大造成的,下面进行具体分析;

跟踪Thread的引用链,可以看到的确是由于ThreadLocal引起的内存泄漏,这里ThreadLocalMap包含3万多个Entry,且每一个Entry的占用近32K的空间;

同时也可以看到Entry的值对象是LambdaFunction,也及系统执行流程中包含了ThreadLocal<LambdaFunction> 对象的使用,因为在业务代码中没有相关逻辑,且LambdaFunction是Aviator框架中定义的类型,排查范围扩展到Aviator框架源码;

注:这里最终的大对象是存放到Env中的业务定义对象(大小近30k),且由于数量比较多,导致最终占用内存较多;

3.2 Aviator框架中什么地方用到ThreadLocal<LambdaFunction>?

项目中应用的aviator版本为5.2.3,具体引用的地方是在LambdaFunctionBootstrap类中,如下:

且类LambdaFunctionBootstrap对该成员变量的使用方法如下:

附源码:

package com.googlecode.aviator.runtime;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.googlecode.aviator.BaseExpression;
import com.googlecode.aviator.Expression;
import com.googlecode.aviator.parser.VariableMeta;
import com.googlecode.aviator.runtime.function.LambdaFunction;
import com.googlecode.aviator.utils.Env;

/**
 * A lambda function creator.
 *
 * @author dennis
 *
 */
public class LambdaFunctionBootstrap {
  // the generated lambda class name
  private final String name;
  // The compiled lambda body expression
  private final BaseExpression expression;
  // The method handle to create lambda instance.
  // private final MethodHandle constructor;
  // The arguments list.
  private final List<FunctionParam> params;
  private final boolean inheritEnv;

  private final ThreadLocal<LambdaFunction> fnLocal = new ThreadLocal<>();


  public String getName() {
    return this.name;
  }

  public LambdaFunctionBootstrap(final String name, final Expression expression,
      final List<FunctionParam> arguments, final boolean inheritEnv) {
    super();
    this.name = name;
    this.expression = (BaseExpression) expression;
    // this.constructor = constructor;
    this.params = arguments;
    this.inheritEnv = inheritEnv;
  }


  public Collection<VariableMeta> getClosureOverFullVarNames() {
    Map<String, VariableMeta> fullNames = this.expression.getFullNameMetas();

    for (FunctionParam param : this.params) {
      fullNames.remove(param.getName());
    }

    Iterator<Map.Entry<String, VariableMeta>> it = fullNames.entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry<String, VariableMeta> fullName = it.next();
      for (FunctionParam param : this.params) {
        if (fullName.getKey().startsWith(param.getName() + ".")) {
          it.remove();
          break;
        }
      }
    }

    return fullNames.values();
  }

  public Expression getExpression() {
    return this.expression;
  }

  /**
   * Create a lambda function.
   *
   * @param env
   * @return
   */
  public LambdaFunction newInstance(final Env env) {
    LambdaFunction fn = null;
    if (this.inheritEnv && (fn = this.fnLocal.get()) != null) {
      fn.setContext(env);
      return fn;
    }

    // try {
    fn = new LambdaFunction(this.name, this.params, this.expression, env);
    fn.setInheritEnv(this.inheritEnv);
    if (this.inheritEnv) {
      this.fnLocal.set(fn);
    }
    return fn;
    // final LambdaFunction fn =
    // (LambdaFunction) this.constructor.invoke(this.params, this.expression, env);

    // } catch (ExpressionRuntimeException e) {
    // throw e;
    // } catch (Throwable t) {
    // throw new ExpressionRuntimeException("Fail to create lambda instance.", t);
    // }
  }
}

3.3 fnLocal为什么存在内存泄漏?

在fnLocal的引用方法中可以看到,方法newInstance中只对fnLocal进行了设置,没有显式进行remove操作(这个是该类的设计理念决定的,fnLocal用作线程内缓存实现方案,在运行期间有效,没有进行显式remove),且因为值对象强引用且fnLocal一直被LambdaFunctionBootstrap对象持有引用,同时LambdaFunctionBootstrap未GC,这样在FullGC时,fnLocal的值对象LambdaFunction所占用内存无法回收;

所以fnLocal设置的线程本地缓存,既没有显式清除,值对象强引用且fnLocal一直被LambdaFunctionBootstrap对象持有引用,同时LambdaFunctionBootstrap未GC,存在内存泄漏风险;当缓存值对象是大对象时,容易导致频繁FullGC,但又回收不掉的情况,和前述的现象一致;

3.4 LambdaFunctionBootstrap为什么没有释放?

LambdaFunctionBootstrap是表达式脚本中lambda类型脚本编译后的类型表示,比如if、while、for语句等,整个脚本编译完成后,LambdaFunctionBootstrap对象存放在编译结果ClassExpression实例对象中,aviator源码中整体的引用链(通过dump内存引用链分析也可以得出)如下:

 对上图中的引用关系具体说明如下:

1)ClassExpression通过成员变量lambdaBootstraps持有了LambdaFunctionBootstrap的引用

2)LambdaFunctionBootstrap通过成员变量持有ThreadLocal的强引用,造成ThreadLocal无法GC回收

3)线程本地变量的值对象为LambdaFunction,且LambdaFunction通过成员变量context持有Env的引用

4)Env对象通过成员变量持有编译结果ClassExpression的引用

由此,上述对象之间存在一个引用环,都是持有的强引用,最后导致所有对象都无法被FullGC回收;

因此,只要表达式脚本中包含了lambda类型的脚本,且使用到了线程本地缓存(inheritEnv为true),就会存在内存泄漏的风险;

3.5 老年代内存占用曲线中,为什么内存占用越来越多(FullGC回收的低点逐渐抬高)?

在3.4节的分析中,我们知道未开启aviator LRU缓存或者开启缓存但未命中缓存的情况下,表达式脚本就会重新编译,生成新的LambdaFunctionBootstrap和LambdaFunction实例对象,随着项目的持续运行,LambdaFunctionBootstrap和LambdaFunction实例对象会越来越多,且都无法回收;

LambdaFunctionBootstrap和LambdaFunction实例数在MAT对象实例视图中也可以看到:

而实际项目中包含的脚本远远达不到这么多;

因此 LambdaFunctionBootstrap和LambdaFunction实例对象越来越多,且都无法回收,导致内存泄漏情况越来越严重,FullGC回收后的低点逐渐抬高。

4 解决方案

 该内存泄漏问题存在于aviator低版本(version<5.3.3)中,最新高版本已经进行了修复,修复改动如下:

这里主要是将ThreadLocal的值对象的强引用改为了软引用,这样在FullGC的时候LambdaFunction就可以被正常回收,本质是因为上述引用环中的结构被打破了,如下:

通过改为软引用,FullGC时,LambdaFunction就可以被正常回收,释放线程本地变量内存占用,内存泄漏问题得到解决; 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/657092.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

002mavenSettings配置

Maven –Settings 官网说明https://maven.apache.org/guides/mini/guide-mirror-settings.html 官网说明 模板 <?xml version"1.0" encoding"UTF-8"?> <settings xmlns"http://maven.apache.org/SETTINGS/1.0.0"xmlns:xsi"htt…

【Redis】持久化机制

一、三种持久化机制 RDF快照&#xff1a;通过某一时刻的快照&#xff0c;将数据以二进制形式永久存储在磁盘中。AOF&#xff1a;将每个修改命令通过write函数追加到文件中。混合持久化&#xff1a;AOF重写时&#xff0c;将重写这一刻之前的内存做RDB快照存储&#xff0c;并将此…

前端vue上拉加载下拉刷新组件,支持列表分页 本地分页

前端vue上拉加载下拉刷新组件,支持列表分页 本地分页, 下载完整代码请访问uni-app插件市场地址: https://ext.dcloud.net.cn/plugin?id12942 喜欢我的前端组件分享文章的&#xff0c;帮忙加个关注&#xff0c;非常感谢&#x1f64f;&#x1f64f; 效果图如下: #### 使用方法…

StarRocks 文章收集

StarRocks在58的实践 StarRocks在58的实践 - 墨天轮StarRocks在58的实践 --2022-06-08https://www.modb.pro/db/639611 StarRocks之系统架构 StarRocks之系统架构 - 墨天轮https://www.modb.pro/db/610300 StarRocks小规模集群部署最佳实践(1/2) 0016.S StarRocks小规模集…

JDBC 2023年最新学习笔记!

课程笔记说明 我的笔记是来源于尚硅谷的赵伟风老师的2023年最新版jdbc的课程 视频链接&#xff1a; 尚硅谷JDBC实战教程&#xff08;2023最新版jdbc&#xff0c;JDK17MySQL8&#xff09;_哔哩哔哩_bilibili 课程资料&#xff1a; 关注“尚硅谷教育”&#xff0c;后台回复J…

中年“难”人

对于每个打工人而言&#xff0c;人生无外乎两种状态&#xff0c;要么已是中年人&#xff0c;要么走在成为中年人的路上&#xff0c;可是当下横空出世的中产作死三件套&#xff0c;让每个打工人都不寒而栗 - 房贷近千万&#xff1b; 配偶不上班&#xff1b; 两娃上国际。 这个…

走在时代前沿:宝洁的柔性供应链战略及其对全球快消品市场的影响

01 先进的供应链计划不断推动宝洁引领全球快速消费品市场 宝洁公司&#xff08;Procter & Gamble&#xff09;成立于1837年&#xff0c;是全球知名的消费品公司&#xff0c;公司拥有多个知名品牌&#xff0c;如宝洁、海飞丝、汰渍、奥妙、潘婷、威露士等&#xff0c;这些产…

程序员必须掌握的消息中间件-RabbitMQ

一、Rabbit 概述 RabbitMQ 是一个开源的消息代理和队列服务器&#xff0c;用来通过普通协议在完全不同的应用中间共享数据&#xff0c;RabbitMQ 是使用 Erlang 语言来编写的&#xff0c;并且 RabbitMQ 是基于 AMQP 协议的。 特点&#xff1a; 开源、性能优秀 Erlang 语言最初用…

【ChatGPT+XMind超级详细的保姆级思维导图教程】

&#x1f680; AI &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;CSDN…

Spring Boot 异常报告器解析

基于Spring Boot 3.1.0 系列文章 Spring Boot 源码阅读初始化环境搭建Spring Boot 框架整体启动流程详解Spring Boot 系统初始化器详解Spring Boot 监听器详解Spring Boot banner详解Spring Boot 属性配置解析Spring Boot 属性加载原理解析Spring Boot 异常报告器解析 创建自定…

怎么查投票人画展投票链接怎么做微信链接投票

近些年来&#xff0c;第三方的微信投票制作平台如雨后春笋般络绎不绝。随着手机的互联网的发展及微信开放平台各项基于手机能力的开放&#xff0c;更多人选择微信投票小程序平台&#xff0c;因为它有非常大的优势。 1.它比起微信公众号自带的投票系统、传统的H5投票系统有可以图…

C语言入门万字笔记

C语言一经出现就以其功能丰富、表达能力强、灵活方便、应用面广等特点迅速在全世界普及和推广。C语言不但执行效率高而且可移植性好&#xff0c;可以用来开发应用软件、驱动、操作系统等。C语言也是其它众多高级语言的鼻祖语言&#xff0c;所以说学习C语言是进入编程世界的必修…

机器学习、计算机视觉和深度学习

机器学习、计算机视觉和深度学习 1 什么是机器学习&#xff1f;2 机器学习的类型3 什么是计算机视觉&#xff1f;4 计算机视觉的机器学习应用5 总结参考 这篇博客将简要介绍&#xff1a;机器学习和用于计算机视觉的机器学习。 想象一下&#xff1a;你可以使用人脸检测算法在图…

【后端】SSM框架下REST风格代码注释详解

前言 最近学习了一下SSM&#xff0c;不得不说&#xff0c;spring不用注解真的是天打雷劈&#xff0c;就那个bean真的就是折磨人。 下面是我总结的spring注解。 Value 此注解可以用来获取导入的jdbc.properties文件的值。 Value("${jdbc.driver}")private String…

【C++】C++11新特性重点:可变参数+lambda

C11新特性第二篇重点 文章目录 上一篇的补充一、可变参数模板二、lambda函数总结 前言 上一篇我们重点讲解了右值引用移动语义&#xff0c;关于移动构造和移动赋值还有一些需要补充的知识&#xff1a; 如果你没有自己实现移动构造函数&#xff0c;且没有实现析构函数 、拷贝构…

dubbo 3.2.0 的filterChain 简要分析

dubbo 3.2.0 的filterChain 的核心类是DefaultFilterChainBuilder 。 Builder public class DefaultFilterChainBuilder implements FilterChainBuilder {的buildInvokerChain函数 对于consumer refer Overridepublic <T> Invoker<T> buildInvokerChain(final I…

Java自动化测试(web自动化测试框架 )

测试数据 测试地址 http://120.78.128.25:8765/ 投资人 13323234545 lemon123456 借款人 13323234444 lemonbest 后台地址 http://120.78.128.25:8765/Admin/Index/login.html lemon7 lemonbest Page Object PO简介 https://www.selenium.dev/documentation/en/g…

如何把在线K歌“玩起来”——专访撕歌音视频架构师程乐

编者按&#xff1a;在线K歌的业务已经发展了十年&#xff0c;程乐在音视频领域也闯荡了十年&#xff0c;甚至更久。为什么选择在线K歌领域&#xff1f;如何走过“漫长的季节”&#xff0c;迎来新的风景&#xff1f;如何在“在线K歌”这块难啃的骨头里分点肉&#xff1f;在这一连…

【存储】cache memory、primary memory and secondary memory

一、提要二、计算机的存储结构三、高速缓存&#xff1a;cache memory四、主存&#xff1a;Primary memory4.1 RAM4.11 SRAM 和 DRAM的概念4.12 SRAM 和 DRAM的应用场景 4.2 ROM4.21 PROM4.22 EPROM▶ EEPROM▶ UVEPROM 五、辅助存储器&#xff1a;secondary memory六、单片机的…

redis的4种模式,单机,哨兵、主从复制、集群

为了redis叫做redis服务器&#xff1f; 因为在运行时&#xff0c;在进行工作时是一个被注册一个进程(服务)&#xff0c;我们把他叫做一个redis服务器。(就是个应用程序而已。) 1.单机模式 我们安装redi,启动服务之后&#xff0c;默认就这个&#xff0c;只有一个redis服务器(…