目录
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框架源码深层次的实质根因,最后结合高版本的修复方案,解决内存泄漏问题;
在实际项目开发过程中,我们会使用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
命令查看内存分配情况
这里可以看到老年代为2584M
随着项目的持续运行,老年代逐步耗尽,且FullGC无法回收,出现内存泄漏问题,下面对该内存泄漏问题进行具体分析;
附:Aviator执行过程源码详见:Aviator源码:Aviator表达式引擎执行过程源码分析,可以较好的理解后续的引用链分析过程以及涉及的LambdaFunctionBootstrap和LambdaFunction对象含义。
收到机器频繁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个比较大的业务线程,共占据了45%以上的内存空间,将近1G的内存占用;
经验分析,对于Thread空间占用较大,一般都是因为线程本地变量较大造成的,下面进行具体分析;
跟踪Thread的引用链,可以看到的确是由于ThreadLocal引起的内存泄漏,这里ThreadLocalMap包含3万多个Entry,且每一个Entry的占用近32K的空间;
同时也可以看到Entry的值对象是LambdaFunction,也及系统执行流程中包含了ThreadLocal
注:这里最终的大对象是存放到Env中的业务定义对象(大小近30k),且由于数量比较多,导致最终占用内存较多;
项目中应用的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 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);
// }
}
}
在fnLocal的引用方法中可以看到,方法newInstance中只对fnLocal进行了设置,没有显式进行remove操作(这个是该类的设计理念决定的,fnLocal用作线程内缓存实现方案,在运行期间有效,没有进行显式remove),且因为值对象强引用且fnLocal一直被LambdaFunctionBootstrap对象持有引用,同时LambdaFunctionBootstrap未GC,这样在FullGC时,fnLocal的值对象LambdaFunction所占用内存无法回收;
所以fnLocal设置的线程本地缓存,既没有显式清除,值对象强引用且fnLocal一直被LambdaFunctionBootstrap对象持有引用,同时LambdaFunctionBootstrap未GC,存在内存泄漏风险;当缓存值对象是大对象时,容易导致频繁FullGC,但又回收不掉的情况,和前述的现象一致;
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.4节的分析中,我们知道未开启aviator LRU缓存或者开启缓存但未命中缓存的情况下,表达式脚本就会重新编译,生成新的LambdaFunctionBootstrap和LambdaFunction实例对象,随着项目的持续运行,LambdaFunctionBootstrap和LambdaFunction实例对象会越来越多,且都无法回收;
LambdaFunctionBootstrap和LambdaFunction实例数在MAT对象实例视图中也可以看到:
而实际项目中包含的脚本远远达不到这么多;
因此 LambdaFunctionBootstrap和LambdaFunction实例对象越来越多,且都无法回收,导致内存泄漏情况越来越严重,FullGC回收后的低点逐渐抬高。
该内存泄漏问题存在于aviator低版本(version<5.3.3)中,最新高版本已经进行了修复,修复改动如下:
这里主要是将ThreadLocal的值对象的强引用改为了软引用,这样在FullGC的时候LambdaFunction就可以被正常回收,本质是因为上述引用环中的结构被打破了,如下:
通过改为软引用,FullGC时,LambdaFunction就可以被正常回收,释放线程本地变量内存占用,内存泄漏问题得到解决;