现象

当我们还沉浸在神机平台上线了第一个推理服务喜悦中,一个个报警打破了这个美丽的早上。伴随着早上的业务高峰,JVM频繁出现了FullGC的问题。大家都明白,对于Java程序猿来说,频繁FullGC是不能接受的,于是开启了我们问题排查之旅。

aviator1
aviator1

结合着堆外内存起伏以及GC日志,不难发现是MetaSpace触发的FullGC。

aviator2
aviator2

问题定位

可以看到经过FullGC后MetaSpace内存是有回落的,那MetaSpace中的类满足什么条件才能够被当成垃圾被卸载回收呢?

  1. 该类所有的实例都已经被回收
  2. 加载该类的ClassLoader已经被回收
  3. 该类对应的java.lang.Class对象没有被任何地方引用

依据上面的思路,我们dump出MetaSpace和heap

aviator3
aviator3

着重观察了一下Class对象的Incoming references,如下图:

aviator4
aviator4

可以看到存在接近8万个由Scrip_${timestamp}_${idx}类型的Class,且都指向了com.googlecode.aviator.ClassExpression,看到这里我们就看到了希望,由于AB分流的模块中使用了表达式引擎AviatorEvaluator,推测是有不停的动态创建类的过程,且类没有被回收,我们回到我们代码中继续探寻问题的根源。

这是编译表达式的入口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 编译流量框定表达式
*
* @param express 流量表达式字符串
* @return 流量表达式
*/
@Override
public Expression checkExpress(String express) {
Expression expression = null;
try {
expression = AviatorEvaluator.compile(express);
} catch (Exception e) {
log.error("编译流量框定表达式出现异常", e);
}
return expression;
}

public static Expression compile(String expression) {
// 可以看到默认是使用无缓存的策略进行编译的
return compile(expression, false);
}

继续跟进来compile方法,主要分为缓存和无缓存两个策略,我们先看下无缓存的入口innerCompile方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Expression compile(final String expression, final boolean cached) {
if (expression != null && expression.trim().length() != 0) {
if (cached) {
FutureTask<Expression> task = (FutureTask)this.cacheExpressions.get(expression);
if (task != null) {
return this.getCompiledExpression(expression, task);
} else {
task = new FutureTask(new Callable<Expression>() {
public Expression call() throws Exception {
return AviatorEvaluatorInstance.this.innerCompile(expression, cached);
}
});
FutureTask<Expression> existedTask = (FutureTask)this.cacheExpressions.putIfAbsent(expression, task);
if (existedTask == null) {
existedTask = task;
task.run();
}
return this.getCompiledExpression(expression, existedTask);
}
} else {
// 走无缓存的方式
return this.innerCompile(expression, cached);
}
} else {
throw new CompileExpressionErrorException("Blank expression");
}
}

编译过程主要分为文法分析、初始化编码生成器、预发解析器生成、预发解析这四个过程,不过和我们排查问题相关的主要是初始化编码生成器和预发解析两个阶段,所以我们也只看这个两个过程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private Expression innerCompile(String expression, boolean cached) {
// 文法分析
ExpressionLexer lexer = new ExpressionLexer(this, expression);
// 初始化编码生成器
CodeGenerator codeGenerator = this.newCodeGenerator(cached);
// 预发解析器生成
ExpressionParser parser = new ExpressionParser(this, lexer, codeGenerator);
// 预发解析,实例化Class,最终的Expression对象
Expression exp = parser.parse();
if (this.getOptionValue(Options.TRACE_EVAL).bool) {
((BaseExpression)exp).setExpression(expression);
}
return exp;
}

我们直接先看一下编码生成器的初始化阶段,这个过程主要是生成一个类加载器且封装成一个ASMCodeGenerator:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public CodeGenerator newCodeGenerator(boolean cached) {
AviatorClassLoader classLoader = this.getAviatorClassLoader(cached);
return this.newCodeGenerator(classLoader);
}

public AviatorClassLoader getAviatorClassLoader(boolean cached) {
// 无缓存策略都会创建一个AviatorClassLoader类加载器
return cached ? this.aviatorClassLoader : new AviatorClassLoader(Thread.currentThread().getContextClassLoader());
}

public CodeGenerator newCodeGenerator(AviatorClassLoader classLoader) {
switch (this.getOptimizeLevel()) {
case 0:
ASMCodeGenerator asmCodeGenerator = new ASMCodeGenerator(this, classLoader, this.traceOutputStream, this.getOptionValue(Options.TRACE).bool);
asmCodeGenerator.start();
return asmCodeGenerator;
case 1:
// 默认走EVAL
return new OptimizeCodeGenerator(this, classLoader, this.traceOutputStream, this.getOptionValue(Options.TRACE).bool);
default:
throw new IllegalArgumentException("Unknow option " + this.getOptimizeLevel());
}
}

public OptimizeCodeGenerator(AviatorEvaluatorInstance instance, ClassLoader classLoader, OutputStream traceOutStream, boolean trace) {
this.instance = instance;
this.codeGen = new ASMCodeGenerator(instance, (AviatorClassLoader)classLoader, traceOutStream, trace);
this.trace = trace;
}

public ASMCodeGenerator(AviatorEvaluatorInstance instance, AviatorClassLoader classLoader, OutputStream traceOut, boolean trace) {
this.currentLabel = START_LABEL;
this.l0stack = new Stack();
this.l1stack = new Stack();
this.methodMetaDataStack = new ArrayDeque();
this.classLoader = classLoader;
this.instance = instance;
this.compileEnv = new Env();
this.compileEnv.setInstance(this.instance);
// 看到这有没有觉得很熟悉了呢,没错,这就是我们在MAT中看到的class
this.className = "Script_" + System.currentTimeMillis() + "_" + CLASS_COUNTER.getAndIncrement();
this.classWriter = new ClassWriter(2);
this.visitClass();
}

紧跟着看下语法解析阶段,这也是创建Class和Expression对象的地方了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public Expression parse() {
this.statement();
if (this.lookhead != null) {
this.reportSyntaxError("Unexpect token '" + this.currentTokenLexeme() + "'");
}
// 主要看下这个实例化方法
return this.codeGenerator.getResult();
}

public Expression getResult() {
this.end();
byte[] bytes = this.classWriter.toByteArray();

try {
// 这里就是生成Class对象的地方了
Class<?> defineClass = ClassDefiner.defineClass(this.className, Expression.class, bytes, this.classLoader);
Constructor<?> constructor = defineClass.getConstructor(AviatorEvaluatorInstance.class, List.class);
ClassExpression exp = (ClassExpression)constructor.newInstance(this.instance, new ArrayList(this.varTokens.keySet()));
exp.setLambdaBootstraps(this.lambdaBootstraps);
exp.setFuncsArgs(this.funcsArgs);
return exp;
} catch (ExpressionRuntimeException var5) {
throw var5;
} catch (Throwable var6) {
if (var6.getCause() instanceof ExpressionRuntimeException) {
throw (ExpressionRuntimeException)var6.getCause();
} else {
throw new CompileExpressionErrorException("define class error", var6);
}
}
}

柳暗花明又一村!可以得出我们的结论了,无缓存的策略下,每次编译表达式都会创建一个ClassLoader和Class,因而MetaSpace的空间会逐步被占满。

问题解决

问题的解决方案也很简单,我们使用上带缓存的编译方法就可以了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/**
* 编译流量框定表达式
*
* @param express 流量表达式字符串
* @return 流量表达式
*/
@Override
public Expression checkExpress(String express) {
Expression expression = null;
try {
// 看这里!
expression = AviatorEvaluator.compile(express, true);
} catch (Exception e) {
log.error("编译流量框定表达式出现异常", e);
}
return expression;
}

public static Expression compile(String expression, boolean cached) {
return getInstance().compile(expression, cached);
}
我们继续刨根问底,看看使用缓存的策略是如何生成Expression的:
public Expression compile(final String expression, final boolean cached) {
if (expression != null && expression.trim().length() != 0) {
if (cached) {
// 从缓存中取
FutureTask<Expression> task = (FutureTask)this.cacheExpressions.get(expression);
if (task != null) {
return this.getCompiledExpression(expression, task);
} else {
task = new FutureTask(new Callable<Expression>() {
public Expression call() throws Exception {
// 还是调用无缓存策略的innerCompile方法
return AviatorEvaluatorInstance.this.innerCompile(expression, cached);
}
});
// 将结果放入缓存中
FutureTask<Expression> existedTask = (FutureTask)this.cacheExpressions.putIfAbsent(expression, task);
if (existedTask == null) {
existedTask = task;
task.run();
}

return this.getCompiledExpression(expression, existedTask);
}
} else {
return this.innerCompile(expression, cached);
}
} else {
throw new CompileExpressionErrorException("Blank expression");
}
}

在使用了缓存的策略后,MetaSpace保持稳定,也没有出现频繁FullGC的情况了,至此我们的问题排查,优化过程就告一段落啦~

aviator5
aviator5

aviator6
aviator6

总结

在我们开发的过程中,不可避免的要接触一些陌生的框架,我们并不排斥新的东西,但我们在使用前要了解其是如何实现的、有什么优劣势,即使做不到知其所以然,我们也可以度娘一下其他人是如何使用的,有哪些可能会踩什么的坑!汲取前人的教训,我们会在保障系统稳定性的道路上,走的更加的安心和舒心~