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

目录

1.频繁FullGC告警

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

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

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

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

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

3.4 LambdaFunctionBootstrap为什么没有释放?

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

4 解决方案


本文通过实际线上项目中频繁FullGC告警的场景,利用MAT内存分析工具,重点分析Aviator低版本内存泄漏问题的排查过程,并深入分析Aviator框架源码深层次的实质根因,最后结合高版本的修复方案,解决内存泄漏问题;

1.频繁FullGC告警

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

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

JVM内存分析:Aviator低版本内存泄漏问题分析_第1张图片部署机器的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 命令查看内存分配情况

JVM内存分析:Aviator低版本内存泄漏问题分析_第2张图片

JVM内存分析:Aviator低版本内存泄漏问题分析_第3张图片

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

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

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

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

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

堆转储命令为:

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

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

usr/local/java8/bin/jmap -histo | head -n20

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

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

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

经验分析,对于Thread空间占用较大,一般都是因为线程本地变量较大造成的,下面进行具体分析;
JVM内存分析:Aviator低版本内存泄漏问题分析_第4张图片

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

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

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

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

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

JVM内存分析:Aviator低版本内存泄漏问题分析_第5张图片

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

JVM内存分析:Aviator低版本内存泄漏问题分析_第6张图片

附源码:

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 params;
  private final boolean inheritEnv;

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


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

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


  public Collection getClosureOverFullVarNames() {
    Map fullNames = this.expression.getFullNameMetas();

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

    Iterator> it = fullNames.entrySet().iterator();
    while (it.hasNext()) {
      Map.Entry 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内存引用链分析也可以得出)如下:

JVM内存分析:Aviator低版本内存泄漏问题分析_第7张图片

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

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对象实例视图中也可以看到:

JVM内存分析:Aviator低版本内存泄漏问题分析_第8张图片

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

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

4 解决方案

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

JVM内存分析:Aviator低版本内存泄漏问题分析_第9张图片

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

JVM内存分析:Aviator低版本内存泄漏问题分析_第10张图片

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

你可能感兴趣的:(aviator,JVM,jvm,java,aviator)