真实还原定位java内存OOM步骤

本文详细记录了一次复杂的Java内存泄漏定位过程,从发现问题到获取内存dump,再到利用Eclipse Memory Analyzer进行深入分析,最终锁定问题根源为XML解析时未设置超时导致的线程阻塞。

abstract

自己做了很久的java开发了, 很久没有写关于内存泄漏/溢出相关的问题定位了. 本文会描述一个十分曲折的定位过程. 从本文里面可以学到:

  • jdk11的内存dump
  • 如何分析大对象
  • 如何结合OQL还原真实的问题现场

问题现象

产品某一台服务器发现内存(4G), 已经使用超过90%, 所以我就收到了相关alert如下图:

在这里插入图片描述
可以看出来内存一直在以非常缓慢的速率增加, 而且增长速率很慢. 拿到这个问题的第一想法:

  • 为什么其他服务器没有问题? 我们的这个服务本身是无状态的,还有大概12台机器, 只有另外一台也有相关问题.
  • 为啥内存增长如此缓慢? 内存泄漏应该会很快出现.
  • 都~4个月没有改过代码了

头大的分析步骤

既然问题已经发生了, 那么第一步应该是获取内存dump (因为服务还在运行, 这是个好信号).

如何获取内存dump?

尝试1

一般的应用都会打开HeapDumpOnOOM选项, 但是此时进程还是okay的, 所以不行.

尝试2

用jmap试试. 因为我们已经开始使用JDK11了 (小版本11.0.3), 结果发现不行.

/usr/java/jdk11.0.3.7.1/bin/jmap -dump:format=b,file=test.heap 32065
Exception in thread "main" com.sun.tools.attach.AttachNotSupportedException: Unable to open socket file /proc/32065/root/tmp/.java_pid32065: target process 32065 doesn't respond within 10500ms or HotSpot VM not loaded
	at jdk.attach/sun.tools.attach.VirtualMachineImpl.<init>(VirtualMachineImpl.java:100)
	at jdk.attach/sun.tools.attach.AttachProviderImpl.attachVirtualMachine(AttachProviderImpl.java:58)
	at jdk.attach/com.sun.tools.attach.VirtualMachine.attach(VirtualMachine.java:207)
	at jdk.jcmd/sun.tools.jmap.JMap.executeCommandForPid(JMap.java:128)
	at jdk.jcmd/sun.tools.jmap.JMap.dump(JMap.java:196)
	at jdk.jcmd/sun.tools.jmap.JMap.main(JMap.java:114)

同样的用jcmd也是上面的错误.

尝试3

突然发现, 既然现在程序还在运行, 试试JMX中的heapdump bean是否可行呢? 结果是可以的. 而且我们的程序内置了一个groovy 执行框架(支持在jvm上运行groovy脚本), 所以我们就可以用如下的代码来执行heapdump:

import com.sun.management.HotSpotDiagnosticMXBean

import javax.management.MBeanServer
import java.lang.management.ManagementFactory

class HeapDumper {
    private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic";
    private static volatile HotSpotDiagnosticMXBean hotspotMBean;


    static void dumpHeap(String fileName, boolean live) {
        // initialize hotspot diagnostic MBean
        initHotspotMBean();
        try {
            hotspotMBean.dumpHeap(fileName, live);
        }
        catch (RuntimeException re) {
            throw re;
        }
        catch (Exception exp) {
            throw new RuntimeException(exp);
        }
    }

    // initialize the hotspot diagnostic MBean field
    private static void initHotspotMBean() {
        if (hotspotMBean == null) {
            synchronized (HeapDumper.class) {
                if (hotspotMBean == null) {
                    hotspotMBean = getHotspotMBean();
                }
            }
        }
    }

    public stat
<think> 我们正在讨论的是Spring AI项目中的`TikaDocumentReader`,特别是它在文本预处理方面的功能。根据之前的对话历史和引用资料,我们可以总结以下几点: 1. `TikaDocumentReader`的核心作用是统一解析多种格式的文档(如PDF、Word、PPT等)并提取纯文本内容。 2. 它在Spring AI生态中作为文档加载器(Document Reader)使用,将异构文档转换为标准化的文本格式,以供后续AI处理(如嵌入生成、RAG应用等)。 3. 它封装了Apache Tika库的功能,自动处理格式识别、文本提取等任务。 现在用户特别关注“文本预处理”相关内容。这里需要明确:`TikaDocumentReader`本身主要完成的是文本提取(Text Extraction),而非文本预处理(Text Preprocessing)。但提取后的文本通常需要经过预处理才能用于AI任务。因此,我们可以从两个层面来回答: ### 一、`TikaDocumentReader`内置的文本处理能力(提取阶段) 虽然主要功能是提取文本,但Apache Tika在提取过程中会进行一些基础处理: - **去除二进制内容**:过滤图片、嵌入对象等非文本元素 - **基础格式化**:保留段落分隔(通过换行符或空格)、列表标记等 - **字符编码转换**:确保输出UTF-8编码文本 - **元数据提取**:可获取作者、标题等属性(但`TikaDocumentReader`默认不返回这些) *示例代码:提取文本并打印* ```java TikaDocumentReader reader = new TikaDocumentReader("file.pptx"); List<Document> docs = reader.get(); System.out.println(docs.get(0).getContent()); // 输出纯文本 ``` ### 二、与预处理流程的衔接(提取后处理) 提取后的文本通常需要进一步预处理才能用于AI模型。常见的后续预处理步骤包括: 1. **文本清洗**:移除特殊字符、冗余空格 2. **分段(Chunking)**:将长文本切割为适合模型处理的片段 3. **标记化(Tokenization)**:将文本拆分为单词/子词(但通常在模型嵌入时自动完成) 4. **停用词过滤/词干提取**:传统NLP处理(在RAG系统中较少使用) *Spring AI中的典型处理链示例:* ```java TikaDocumentReader reader = new TikaDocumentReader("report.pdf"); // 1. 提取文档 List<Document> rawDocs = reader.get(); // 2. 创建文本分割器(预处理关键步骤) TextSplitter splitter = new TokenTextSplitter(...); // 3. 分段处理 List<Document> processedDocs = splitter.apply(rawDocs); // 4. 后续操作(如生成嵌入向量) vectorStore.add(processedDocs); ``` ### 三、自定义预处理扩展 若需要对提取文本进行深度处理,可扩展`DocumentReader`接口: ```java public class CustomTikaReader implements DocumentReader { private final TikaDocumentReader tikaReader; public CustomTikaReader(String filePath) { this.tikaReader = new TikaDocumentReader(filePath); } @Override public List<Document> get() { List<Document> rawDocs = tikaReader.get(); return rawDocs.stream() .map(doc -> { String processedText = cleanText(doc.getContent()); // 自定义清洗逻辑 return new Document(processedText, doc.getMetadata()); }) .collect(Collectors.toList()); } private String cleanText(String text) { // 实现自定义预处理逻辑,例如正则替换 return text.replaceAll("\\s+", " ").trim(); } } ``` ### 关键注意事项 1. **性能考量**:Tika解析大文件可能较慢,建议异步处理 2. **内容完整性**:复杂格式(如表格)可能被转换为纯文本丢失结构 3. **错误处理**:加密/损坏文档会抛出`TikaException`,需捕获处理 --- ### 引用依据 - `TikaDocumentReader`提取后返回标准`Document`对象,其内容为纯文本[^1] - Spring AI推荐使用`TextSplitter`实现分段预处理[^2] - 自定义文档处理器可通过实现`DocumentReader`接口扩展[^3] --- §§ 相关问题 §§ 1. 如何用`TikaDocumentReader`处理加密的PDF文件? 2. `TokenTextSplitter`和`RecursiveCharacterTextSplitter`在预处理中有何区别? 3. 能否在文本提取阶段保留表格或图片的结构化信息? </think> ### 📚 Java TikaDocumentReader 文本预处理详解 `TikaDocumentReader` 的核心预处理能力体现在文本提取后的标准化处理流程中。虽然它主要专注于文档解析和基础文本提取,但通过与其他工具链结合,可实现完整的预处理流水线。以下是关键处理环节: --- #### 1. **基础文本提取(内置预处理)** `TikaDocumentReader` 通过 Apache Tika 实现以下基础处理: - **格式标准化** 将所有文档(PDF/DOCX/PPT等)转为 UTF-8 纯文本,消除格式差异[^2]: ```java Document doc = new TikaDocumentReader("report.docx").get().get(0); String text = doc.getContent(); // 标准化UTF-8文本 ``` - **噪声过滤** 自动移除: - 排版标签(如 `<b>`、分页符) - 隐藏元数据(注释、修订记录) - 非文本元素(图片、表格边框) *引用[1]:* > "提取纯文本内容,过滤非文本元素" - **结构保留** 基础段落分隔(通过换行符 `\n` 保持原文段落结构)[^4]。 --- #### 2. **扩展预处理流程(需结合其他组件)** 提取后的文本通常需额外处理才能用于 AI 任务: | **预处理步骤** | **实现方式** | **示例代码** | |----------------------|-----------------------------------------------------------------------------|-----------------------------------------------------------------------------| | **分段切割** | 使用 `TextSplitter` 处理长文本 | ```new TokenTextSplitter().apply(tikaReader.get())``` | | **关键信息归一化** | 正则表达式替换日期/货币/单位等 | ```text.replaceAll("\\d{4}-\\d{2}-\\d{2}", "[DATE]")``` | | **停用词过滤** | 集成 Lucene 等工具 | ```new StopFilter(tokenStream, StopAnalyzer.ENGLISH_STOP_WORDS_SET)``` | | **词形还原** | 接入 OpenNLP 或 Stanford NLP | ```lemmatizer.lemmatize("running") // 返回 "run"``` | --- #### 3. **自定义预处理扩展** 可通过继承 `DocumentReader` 实现定制逻辑: ```java public class CustomTikaReader extends TikaDocumentReader { @Override public List<Document> get() { List<Document> rawDocs = super.get(); return rawDocs.stream() .map(doc -> { String processedText = customClean(doc.getContent()); return new Document(processedText, doc.getMetadata()); }).collect(Collectors.toList()); } private String customClean(String text) { // 自定义规则:保留表格结构标记 text = text.replaceAll("\\t+", "[TABLE_ROW]"); // 移除特殊字符 return text.replaceAll("[^\\p{L}\\p{N}\\s.,;:!?]", ""); } } ``` --- #### 4. **预处理最佳实践** - **性能优化** - 大文件处理:启用 Tika 的 `SAX` 解析模式避免 OOM ```java new TikaDocumentReader(ResourceUtils.getFile("large.pdf"), ParserMode.SAX) ``` - 并行处理:对多文档使用 `parallelStream()` - **质量保障** - 添加后处理验证: ```java if (doc.getContent().contains("[TABLE_ROW]")) logger.info("保留表格结构成功"); ``` - 异常处理加密文档: ```java try { tikaReader.get(); } catch (EncryptedDocumentException e) { // 触发解密流程 } ``` --- ### 💡 典型预处理流水线 ```mermaid graph LR A[TikaDocumentReader] --> B[提取原始文本] B --> C[TextSplitter 分段] C --> D[正则清洗] D --> E[停用词过滤] E --> F[词干提取] F --> G[向量嵌入模型] ``` > *输出可直接输入向量数据库或 LLM 模型(如 RAG 应用)[^4]* --- ### 引用依据 [^1]: Tika 基础文本提取与过滤功能 [^2]: 格式标准化与编码处理说明 [^4]: 分段处理与向量化应用场景 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值