FlowBus.unloadScriptNode() 无法释放 Metaspace,liteflow-script-javax 动态编译的 Class 持续累积导致 OOM
- Dominant language
- Java
- Stars
- 3.9k
- Forks
- 525
- PR merge metrics
- No merged PRs in 30d
Description
## 版本信息
- LiteFlow: 2.15.2
- 脚本插件: liteflow-script-javax + liteflow-script-javax-pro
- Liquor: 1.5.8(由 liteflow-script-javax 传递依赖)
- JDK: 17
- 运行环境: K8S Pod,Metaspace 192-512MB
## 使用场景
我们的 ETL 调度系统通过定时任务周期性地执行数据管道:每次调度为多个任务动态创建 Script Node → 执行 → finally 中调用 `FlowBus.unloadScriptNode()` 清理。
```java
try {
// 动态创建脚本节点
for (DFC dfc : dfcList) {
LiteFlowNodeBuilder.createScriptNode()
.setId(beanName).setName(beanName)
.setScript(dfc.getScript()).setLanguage(dfc.getLanguage())
.build();
}
LiteFlowChainELBuilder.createChain().setChainId(chainId).setEL(el).build();
// 执行
flowExecutor.execute2FutureWithRid(chainId, null, requestId, flowContent);
} finally {
// 清理
FlowBus.removeChain(chainId);
for (DFC dfc : dfcList) {
FlowBus.unloadScriptNode(beanName);
}
}
```
## 问题现象
服务运行一段时间后(约 1-2 周),Metaspace OOM:
```
java.lang.OutOfMemoryError: Metaspace
```
通过 `jstat -gc` 监控发现 Metaspace Used (MU) 持续增长,从不下降,直到达到 MaxMetaspaceSize 后 OOM。
## 根因分析
通过反编译 LiteFlow 2.15.2 和 Liquor 1.5.8 的字节码,定位到以下问题链:
### 1. `unloadScriptNode()` 只清除 LiteFlow 层引用
反编译 `JavaxExecutor.class`:
```java
// JavaxExecutor.unLoad(String nodeId)
public void unLoad(String nodeId) {
compiledScriptMap.remove(nodeId); // ← 仅此一行
}
```
只从 `compiledScriptMap` 中移除了 `Execable` 引用,**不触及底层 Liquor 的 ClassLoader**。
### 2. Liquor 的 `tempClassLoader` 默认 10000 次编译才替换
反编译 `LiquorEvaluator.class`:
```java
// LiquorEvaluator 构造函数
public LiquorEvaluator(ClassLoader parent) {
this(parent, 10000); // cahceCapacity = 10000
}
// build() 方法(非缓存路径,即 liteflow-script-javax 的默认路径)
protected Class build(CodeSpec codeSpec) {
if (!codeSpec.isCached()) {
tempCount++;
if (tempCount > cahceCapacity) { // 10000 次后才替换
tempClassLoader = compiler.newClassLoader();
tempCount = 0;
}
compiler.setClassLoader(tempClassLoader); // 所有编译共享同一 ClassLoader
}
// ... compile and load class
}
```
`DynamicClassLoader` 继承 `java.lang.ClassLoader`。**JVM 规定 Class 只有在其 ClassLoader 被 GC 后才能从 Metaspace 卸载**。在 `tempClassLoader` 被替换之前,所有编译的 Class 共享同一个 ClassLoader,全部无法被 GC 回收。
### 3. `JavaxExecutor.isCache` 默认为 false
反编译 `JavaxExecutor.init()`:
```java
public ScriptExecutor init() {
String isCache = config.getScriptSetting().get("javax-is-cache");
this.isCache = Boolean.parseBoolean(isCache); // null → false
// ...
}
```
`Boolean.parseBoolean(null)` 返回 `false`,所以 `CodeSpec.cached(false)`,走 `tempClassLoader` 路径。
### 泄漏链路总结
```
FlowBus.unloadScriptNode(nodeId)
→ JavaxExecutor.unLoad(nodeId)
→ compiledScriptMap.remove(nodeId) ← 只断开 LiteFlow 的引用
✗ 不触及 LiquorEvaluator.tempClassLoader
✗ tempClassLoader 仍持有所有已编译 Class 的字节码(DynamicClassLoader.byteCodes Map)
✗ ClassLoader 未被替换 → Class 无法从 Metaspace 卸载
```
## 建议
`ScriptExecutor.unLoad()` / `cleanCache()` 应增加对底层脚本引擎 ClassLoader 的清理能力。以下是几种可能的方向:
### 方案 A:`unLoad` 后检查是否需要重置 ClassLoader
在 `JavaxExecutor` 中,当 `compiledScriptMap` 为空时(所有节点已卸载),主动替换 `LiquorEvaluator` 的 `tempClassLoader`,使旧 ClassLoader 可被 GC 回收。
### 方案 B:提供显式的 Metaspace 清理 API
在 `ScriptExecutor` 中增加类似 `resetClassLoader()` 的方法,供使用者在批量卸载脚本节点后调用。
### 方案 C:降低 Liquor 的 `cahceCapacity` 或使其可配置
当前 `LiquorEvaluator` 硬编码 `cahceCapacity=10000`,导致在动态创建/销毁场景下 ClassLoader 长期不替换。如果能通过 LiteFlow 的 `script-setting` 暴露此配置,使用者可以根据场景调整。
---
## 当前的临时 Workaround
通过反射在每次执行后强制替换 `tempClassLoader`:
```java
// 在 finally 块中,unloadScriptNode 之后调用
Object evaluator = LiquorEvaluator.getInstance();
Field compilerField = LiquorEvaluator.class.getDeclaredField("compiler");
compilerField.setAccessible(true);
DynamicCompiler compiler = (DynamicCompiler) compilerField.get(evaluator);
Field tempCLField = LiquorEvaluator.class.getDeclaredField("tempClassLoader");
tempCLField.setAccessible(true);
tempCLField.set(evaluator, compiler.newClassLoader());
Field tempCountField = LiquorEvaluator.class.getDeclaredField("tempCount");
tempCountField.setAccessible(true);
tempCountField.setInt(evaluator, 0);
```
这个方案有效但依赖反射,存在 Liquor 版本升级后字段名变更的风险,期望 LiteFlow 官方能提供正式的解决方案。
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.