禁止直接调用 getClob().getSubString(),因其强制全量加载CLOB至堆内存,易引发OOM;应改用 try-with-resources 包裹 getCharacterStream() 按块读取,并禁用 implicitCachingEnabled。
直接调用 getClob().getSubString() 必须禁止
这是最常见也最危险的操作:哪怕 clob 只有 10mb,getsubstring(1, (int) clob.length()) 就会强制驱动把整段内容加载进堆内存,再转成 string。java 中 char 占 2 字节,实际堆占用接近 20mb —— 而批量查 100 条,就是 2gb,java.lang.outofmemoryerror: java heap space 几乎必然发生。
更隐蔽的坑是:clob.length() 在某些旧版 Oracle JDBC 驱动(如 11g 早期驱动)中本身就会触发全量 fetch,还没读内容,内存已经涨了。
- 永远不要在循环里对每个 CLOB 都调一次
length()+getSubString() - 别信“我只查几条,应该没事”——连接池复用、GC 延迟会让 OOM 表现滞后且难定位
- MyBatis 默认
#{field}如果没指定jdbcType=CLOB,可能悄悄回退到getString(),同样踩坑
getCharacterStream() 是唯一安全的读取入口
Oracle JDBC 驱动真正支持 LOB 指针语义的地方,就藏在 getCharacterStream() 返回的 Reader 里。它不加载全文,只按需从数据库拉块数据(底层走 LOB locator + DBMS_LOB.READ),内存占用稳定在 KB 级。
但必须配对使用 try-with-resources,否则流不关,LOB 句柄泄漏,Oracle 侧会持续 hold 连接和临时段。
- 缓冲区大小设
8192足够,再大不提速,反而增加单次分配压力 - 避免
BufferedReader.readLine()—— CLOB 可能无换行符,且readLine()内部缓冲不可控,易隐式放大内存 - 正确写法是直接
reader.read(char[] buf),按块处理,不拼接全文 - 字符集必须与数据库一致;若库用 AL32UTF8,JDBC URL 加
&useUnicode=true&characterEncoding=UTF-8
ResultSet 的游标类型决定流是否有效
Oracle CLOB 流的生命期绑定在当前 ResultSet 行和底层连接上。默认 TYPE_FORWARD_ONLY 下,只要调一次 rs.next(),前一行的 Reader 立即失效,再读就抛 SQLException: Stream has already been closed。
立即学习“Java免费学习笔记(深入)”;
这不是代码 bug,是 JDBC 游标语义使然。强行 catch 并重试只会掩盖问题。
- 最简方案:所有 CLOB 处理逻辑必须在
while(rs.next()) { ... }当前行内完成 - 若需跨行访问(如分页时缓存多行 CLOB),必须显式创建
ResultSet时传入ResultSet.HOLD_CURSORS_OVER_COMMIT(Oracle 12.1+ 驱动才可靠支持) - 别依赖
Connection.setHoldability()全局设置——MyBatis/Druid 等框架可能覆盖它
隐式 LOB 缓存会悄悄破坏流式语义
Oracle JDBC 驱动默认开启 implicitCachingEnabled=true,这意味着哪怕你只调了一次 clob.getSubString(1,1) 或 clob.length(),驱动就可能把整个 LOB 缓存在本地内存里,后续 getCharacterStream() 实际读的是缓存副本 —— 内存占用照涨,流式白做。
现象是:jmap -histo 显示大量 [C(char[])实例,但代码里根本没存 String。
- 解决办法:JDBC URL 显式关闭,加
&implicitCachingEnabled=false - 验证方式:在测试中注释掉所有
length()和getSubString()调用,观察内存曲线是否平稳 - MyBatis 用户注意:自定义
TypeHandler里若用了clob.length()判空,必须删掉,改用clob == null直接判


















