chararrayreader直接引用原始char[],零拷贝、无数组复制、无额外堆分配;stringreader则每次构造都隐式复制char[],产生新对象和gc压力。

StringReader 和 CharArrayReader 都能在内存中读取字符,不触发磁盘或网络 I/O,但它们在堆内存分配和底层行为上存在关键差异——这直接影响高频、低延迟场景下的微观开销。
StringReader 会隐式创建 char[] 副本
StringReader 构造时接收一个 String 对象,而 String 内部虽持有一个 final char[],但 StringReader 并不直接复用它。JDK 实现中,StringReader 会将 String 的内容拷贝到自己的内部缓冲区(通过 value 字段 + offset/length 截取逻辑),这个过程涉及一次数组复制和一次新 char[] 分配。
- 即使传入的 String 已经由 StringBuilder.toString() 生成,StringReader 仍会额外分配一块等长 char[]
- 该副本无法复用,每次 new StringReader 都产生新对象、新数组
- 对 10KB 字符串,意味着每调用一次就多分配约 20KB 堆内存(char 占 2 字节)
CharArrayReader 直接引用原始 char[],零拷贝
CharArrayReader 接收的是 char[] 引用本身,内部仅保存该数组的引用、当前读取位置(pos)、结束位置(buf.length)等几个字段,不新建任何数组。
- read() 方法本质就是
buf[pos++],纯下标访问,无对象创建、无内存拷贝 - 若 char[] 来自 StringBuilder 的内部数组(通过
StringBuilder.getChars()或反射获取),可完全避免 String 中间态 - 适合循环复用场景:同一 char[] 多次包装为 CharArrayReader,无额外 GC 压力
GC 与逃逸分析的实际影响
StringReader 创建的临时 char[] 属于“短生命周期逃逸对象”——通常只在单次解析中使用,方法退出即不可达。JVM 的逃逸分析可能将其栈上分配(标量替换),但该优化有前提:
- 必须开启 -XX:+DoEscapeAnalysis(默认开启,但复杂调用链下易失效)
- 不能被同步块、异常处理、Lambda 捕获等破坏逃逸判定
- 即便栈分配成功,仍需执行一次数组复制,CPU 缓存行写入开销不可忽略
而 CharArrayReader 完全绕过这一层:无新数组、无复制、无逃逸判定依赖,行为稳定可预测。
何时选哪个?看数据源头
选择依据不是“哪个更简单”,而是“你的字符数据最初以什么形态存在”:
- 源头是 String(如配置项、HTTP header、JSON 字符串字面量)→ StringReader 足够,语义清晰,开发成本低
- 源头是 char[] 或 StringBuilder(如模板引擎拼接、协议解析缓存、日志批量组装)→ 必须用 CharArrayReader,否则白白浪费复制与 GC
- 需要频繁 reset / mark / skip 且对吞吐敏感(如语法分析器预读)→ CharArrayReader 的指针操作更轻量,无字符串边界校验开销











