本文目录导读:

这是一个典型的Java Swing组件JEditorPane配合HTMLEditorKit解析HTML时导致的内存溢出(OutOfMemoryError)问题,这类问题通常由畸形HTML、超大嵌套深度、或大量未关闭标签引发。
下面是针对该问题的系统性诊断与解决方案:
问题根因分析
HTMLEditorKit的底层解析器(ParserDelegator)是基于Swing的HTML 3.2/HTML 4.0的严谨解析器,它有以下弱点:
- 递归解析:遇到未闭合的嵌套标签(如
<div><div><div>...)会产生深度递归,迅速耗尽栈和堆。 - 内存分配策略:它会预分配大量
char[]和String对象来处理标记符号,尤其是大文件或长属性值。 - 非流式处理:整个HTML必须完全加载到内存中构建DOM树(Swing HTML Document),不支持SAX/流式解析。
立即修复方案
方案A:限制输入大小与标签深度
import javax.swing.text.html.*;
import javax.swing.text.html.parser.*;
// 自定义解析器,限制递归深度
public class SafeHtmlEditorKit extends HTMLEditorKit {
// 最大允许的嵌套深度
private static final int MAX_NESTING_DEPTH = 100;
@Override
public ViewFactory getViewFactory() {
// 返回自定义ViewFactory,用于限制渲染深度
return new SafeHTMLFactory();
}
private static class SafeHTMLFactory extends HTMLEditorKit.HTMLFactory {
@Override
public View create(Element elem) {
// 检查元素深度,超过阈值返回空View
int depth = getDepth(elem);
if (depth > MAX_NESTING_DEPTH) {
return new javax.swing.text.html.InlineView(elem) {
@Override
public float getPreferredSpan(int axis) {
return 0; // 不占用空间
}
};
}
return super.create(elem);
}
private int getDepth(Element elem) {
int depth = 0;
while (elem != null) {
depth++;
elem = elem.getParentElement();
}
return depth;
}
}
}
使用:
JEditorPane editor = new JEditorPane(); editor.setEditorKit(new SafeHtmlEditorKit());
方案B:使用外部流式HTML解析器预处理
最干净的解决方案——不要用HTMLEditorKit解析长HTML,而是用Jsoup(成熟、流式安全)预处理成安全片段:
import org.jsoup.Jsoup;
import org.jsoup.safety.Safelist;
import org.jsoup.nodes.Document;
// Maven依赖: org.jsoup:jsoup:1.17.1
public String sanitizeHtml(String rawHtml) {
// 1. 使用Jsoup解析(流式、非递归DOM构建)
Document doc = Jsoup.parse(rawHtml, "UTF-8");
// 2. 清理/转义(去掉危险标签和过度嵌套)
// basic() 保留: a, b, blockquote, br, cite, code, dd, dl, dt, em, i, li, ol, p, pre, q, small, span, strike, strong, sub, sup, u, ul
String safeHtml = Jsoup.clean(doc.body().html(), Safelist.basic());
// 3. 限制大小(比如前50KB)
if (safeHtml.length() > 50 * 1024) {
safeHtml = safeHtml.substring(0, 50 * 1024) + "...";
}
return safeHtml;
}
然后在JEditorPane中显示:
String safeHtml = sanitizeHtml(rawHtml); editor.setText(safeHtml);
运行时调优(临时缓解)
如果无法修改代码,可以尝试调整JVM参数:
# 增加堆内存(至少给到512MB以上) java -Xms512m -Xmx1024m -XX:+UseG1GC -XX:MaxInlineLevel=15 YourApp # 或启用并发GC减少Full GC停顿 -XX:+UseConcMarkSweepGC
针对性代码检查清单
-
[x] 是否使用
setText()而不是read()?read(Reader, ...)不会重置解析器状态,累计内存,优先用setText()。
-
[x] 是否反复更新同一个
JEditorPane- 每次更新前调用
editor.setText(null)清除旧文档,避免累积。
- 每次更新前调用
-
[x] 是否包含大量
<table>嵌套?- 表格解析是已知的重度递归区域,建议用
<div>+CSS替代。
- 表格解析是已知的重度递归区域,建议用
-
[x] 是否使用
<script>或<style>HTMLEditorKit不会执行脚本,但会解析其内容,可能导致内存膨胀,清理掉这些标签。
-
[x] 是否以字符串方式动态拼接HTML?
可能是拼接过程中产生了超大字符串(如循环拼接),检查拼接逻辑。
终极替代方案
如果以上方案都无法解决,建议彻底替换Swing控件:
| 方案 | 优点 | 缺点 |
|---|---|---|
| JavaFX WebView | 现代、基于WebKit、安全、支持CSS3/JS | 需要JavaFX运行时(JDK 11+内置) |
| SWT Browser | 原生浏览器内核 | 平台相关、增加了SWT依赖 |
| JTextPane + 自定义渲染 | 避免HTML解析,只支持RichText | 功能有限 |
推荐迁移到JavaFX WebView(代码量小):
// 在Swing中嵌入JavaFX
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.web.WebView;
JFXPanel fxPanel = new JFXPanel();
Platform.runLater(() -> {
WebView webView = new WebView();
webView.getEngine().loadContent(yourHtmlString);
fxPanel.setScene(new Scene(webView));
});
// 然后将fxPanel加入Swing容器
快速诊断工具
在本地复现时,可以开启JVM内存监控:
# 打印GC详情 -XX:+PrintGCDetails -XX:+PrintGCTimeStamps # 启用内存溢出自动Dump(分析堆) -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/heapdump.hprof
然后用 Eclipse MAT 或 VisualVM 分析heap dump,查找HTMLEditorKit.Parser相关的巨型对象。
| 优先级 | 行动 | 效果 |
|---|---|---|
| 使用Jsoup预处理HTML | 彻底解决递归与畸形HTML问题 | |
| 限制嵌套深度+大小截断 | 折衷,保留Swing组件 | |
| 迁移至JavaFX WebView | 长期可靠,但需要重构 | |
| 调高JVM堆内存 | 仅临时缓解,不治本 |
最佳实践组合:Jsoup清理HTML + JEditorPane显示 -> 或 -> 直接使用JavaFX WebView。