clob.getSubString()会OOM是因为它将整个CLOB(如50MB)一次性加载进堆内存转String,char占2字节致实际占用近100MB;Oracle驱动还可能在length()等调用时触发全量LOB fetch,导致java.lang.OutOfMemoryError。
直接用 clob.getsubstring() 就会 oom,必须改用 getcharacterstream() 流式读取。
为什么 clob.getSubString() 一查就崩
它会把整个 CLOB 内容(比如 50MB 文本)一次性加载进堆内存,转成 String —— Java 中 char 占 2 字节,实际堆占用接近 100MB。更糟的是,某些 Oracle JDBC 驱动(如 ojdbc8)在调用 length() 或 getSubString(1,1) 时,也会触发全量 LOB fetch,哪怕你只是想“判断是否为空”。常见报错就是 java.lang.OutOfMemoryError: Java heap space,尤其批量查日志表时必现。
流式读取的正确写法和关键细节
用 getCharacterStream() 返回 Reader,不是 InputStream,避免编码错乱(Oracle CLOB 默认用数据库字符集,如 AL32UTF8):
- 缓冲区固定用
char[8192],别设更大——性能不涨,反而白占堆 - 别用
BufferedReader.readLine()或lines(),前者依赖换行符(CLOB 可能没有),后者会预加载全部内容,破坏流式语义 - 必须用 try-with-resources 包住
Reader,否则 LOB 句柄泄漏,连接池迟早耗尽 - 示例片段:
try (Reader reader = rs.getClob("log_content").getCharacterStream()) { char[] buf = new char[8192]; int n; while ((n = reader.read(buf)) != -1) { // 处理 buf[0..n),例如写入文件或解析 JSON 片段 processChunk(buf, n); } }
Oracle 驱动的两个隐藏坑必须关掉
ojdbc8 默认开启隐式 LOB 缓存(implicitCachingEnabled=true),即使你写了流式读,驱动仍可能偷偷缓存整块 CLOB,表现为内存随记录数线性上涨、jmap -histo 看到大量 [C 实例。
- 在 JDBC URL 里加参数:
?implicitCachingEnabled=false - 禁用任何试探性调用:删掉
clob != null && clob.length() > 0这类判空逻辑;真要判空,用rs.wasNull()更安全 - ResultSet 类型别用默认的
TYPE_FORWARD_ONLY:一旦rs.next(),前一行的 CLOB 流立刻失效,报Stream has already been closed;如需跨行访问,显式声明ResultSet.CONCUR_READ_ONLY | ResultSet.HOLD_CURSORS_OVER_COMMIT(注意 Oracle 支持情况)
超大 CLOB 根本不能转 String
别碰 clob.toString()(返回的是类名+哈希码),也别无脑用 IOUtils.toString(reader, UTF_8)——它内部仍会拼接成一个 String,而 String 最大长度受限于 Integer.MAX_VALUE(约 2GB),且堆内存翻倍风险仍在。
立即学习“Java免费学习笔记(深入)”;
- 如果业务真需要全文字符串(极少数场景),先用
reader.read()循环 +StringBuilder拼接,但务必加长度计数防无限循环 - 更合理的做法是:把处理逻辑下沉到流中——逐块解析 JSON、按行清洗日志、边读边写文件或 HTTP 响应体
- 记住:CLOB 的本质是“可流式访问的数据库大文本”,不是“超长字符串”
真正麻烦的不是代码怎么写,而是所有环节都要对齐:驱动参数关缓存、JDBC 调用不试探、流读取不落地为 String、连接生命周期匹配流使用范围——漏掉任意一环,OOM 就在下一次查询时等着你。


















