手写递归深拷贝性能最优,Java中约10–15ms、JS中约15–25ms,但需手动处理嵌套类型与循环引用;原生工具如Cglib BeanCopier(25–40ms)、structuredClone(8–12ms)次之,而Jackson(1200–2000ms)和JSON方案因序列化开销及语义丢失更慢。

手写递归深拷贝和原生工具(如Cglib BeanCopier、Jackson、Spring BeanUtils)的性能差异,不能靠“写个for循环测一次”来判断。真正可信的定量评估,必须用微基准测试框架(JMH 对 Java,V8 Benchmark 对 JS)控制变量、排除干扰、多次采样。
明确你要测什么
不是“哪个快”,而是“在你真实对象结构下,哪种方式满足语义前提且耗时最低”。深拷贝的前提是:所有嵌套引用类型(如Date、List、自定义对象)都创建新实例,内存地址完全隔离。若某工具实际只做了浅拷贝(比如BeanUtils默认不处理嵌套对象),那它再快也无效——结果不可比。
用 JMH 测 Java 手写递归 vs 原生工具
准备一个典型 POJO,含
int、String、Date、List<String>、Address(含String字段)等 1–2 层嵌套所有被测方法接收同一原始实例,返回新对象,且最终通过
assertNotSame()+assertThat(克隆体.address, not(sameInstance(原.address)))验证深拷贝语义成立禁用逃逸分析(
-XX:-DoEscapeAnalysis),防止 JVM 把临时对象优化到栈上,绕过真实拷贝路径-
关键配置示例:
@Fork(3) @Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS) @Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS) @State(Scope.Benchmark) public class DeepCopyBenchmark { ... } -
实测典型耗时(10 万次,JDK 17+,充分预热):
- 手写递归(显式 new + 逐字段 clone):≈ 10–15 ms
- Cglib BeanCopier(配置
Converter处理 Date/List):≈ 25–40 ms - Spring BeanUtils(配合自定义
PropertyEditor或Converter):≈ 300–500 ms - Jackson(
ObjectMapper.readValue(json, type)):≈ 1200–2000 ms
手写快,是因为无反射、无泛型擦除、无临时字符串/字节数组分配;但维护成本高,易漏字段或错判可变集合(如 ArrayList 要 new,Collections.unmodifiableList 则不该拷)。
用 V8 Benchmark 测 JavaScript 手写递归 vs 原生工具
- Node.js 环境下,用
node --allow-natives-syntax --no-concurrent-sweeping --optimize_for_size启动,关闭 GC 干扰 - 对象结构保持一致:
{ id: 1, name: 'a', createdAt: new Date(), tags: ['x','y'], profile: { avatar: 'url' } } - 手写递归需处理:
Date→new Date(date.getTime()),Array→map(item => deepClone(item)),Object→Object.keys().reduce(...),并识别循环引用(用WeakMap缓存已克隆对象) - 对比工具:
structuredClone()(Node 17.4+)、JSON.parse(JSON.stringify())、LodashcloneDeep - 实测趋势(1 万次,V8 11.6):
-
structuredClone():最快,≈ 8–12 ms,原生 C++ 实现,支持Date/Map/Set/TypedArray,但不支持函数、undefined、RegExp - 手写递归(带循环引用检测):≈ 15–25 ms,可控但代码量大
- Lodash
cloneDeep:≈ 35–60 ms,健壮、兼容老环境,但包体积大 -
JSON.parse(JSON.stringify()):≈ 80–150 ms,丢失Date(变成字符串)、undefined、function、RegExp、NaN,且无法处理循环引用
-
关键陷阱要避开
- 不验证深拷贝语义就比耗时,等于拿“假阳性”结果做决策
- 忘记预热或只跑单轮,JIT 未生效,数据波动大(比如首次 Jackson 序列化慢 3 倍)
- 混用不同堆大小或 GC 参数,导致对比失真(建议固定
-Xms512m -Xmx512m) - 在 JS 中用
console.time()替代v8.benchmark,受事件循环和 DevTools 开销干扰严重
结论很实在
手写递归深拷贝在 Java 和 JS 中都是性能最优解之一,但只适合结构稳定、字段明确、团队能统一维护的场景。生产环境更推荐:Java 用 Cglib BeanCopier(配 Converter),JS 用 structuredClone()(现代环境)或 Lodash(兼容性优先)。性能数字只是参考,语义正确性和长期可维护性才是决定性因素。


















