Jackson JavaPropsMapper内存泄漏求助:大Map转嵌套对象OOM
问题描述
我正尝试将基于属性路径的Map<String, String>映射到一个深度嵌套的对象结构,该结构包含一个已增长至200k+对象的列表。使用的代码如下:
JavaPropsMapper.builder() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) .build() .readMapAs(parameters, clazz)
随着parameters Map规模不断增大,我遇到了OutOfMemoryError错误,堆栈信息如下:
Caused by: java.lang.OutOfMemoryError: Java heap space at java.base/java.lang.StringLatin1.newString(StringLatin1.java:752) at java.base/java.lang.String.substring(String.java:2839) at com.fasterxml.jackson.dataformat.javaprop.util.JPropPathSplitter$CharPathOnlySplitter.splitAndAdd(JPropPathSplitter.java:165) at com.fasterxml.jackson.dataformat.javaprop.util.JPropPathSplitter$FullSplitter.splitAndAdd(JPropPathSplitter.java:354) at com.fasterxml.jackson.dataformat.javaprop.util.JPropNodeBuilder.build(JPropNodeBuilder.java:31) at com.fasterxml.jackson.dataformat.javaprop.JavaPropsParser.nextToken(JavaPropsParser.java:232) at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:5004) at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4885) at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3036) at com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper.readMapAs(JavaPropsMapper.java:179) at com.fasterxml.jackson.dataformat.javaprop.JavaPropsMapper.readMapAs(JavaPropsMapper.java:211)
我无法控制代码运行的环境,因此需要修复该错误的根本原因或寻找替代方案。请问如何绕过这个问题?或许可以通过提供JavaPropsSchema,但我不知道如何构建它。
解决方案
1. 自定义路径拆分器减少内存开销
从堆栈信息可以看出,内存溢出发生在属性路径拆分阶段——JPropPathSplitter处理大量路径时会生成大量子字符串,导致堆内存耗尽。可以通过自定义拆分逻辑避免频繁的字符串复制:
JavaPropsMapper mapper = JavaPropsMapper.builder() .configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true) .propPathSplitter(new JPropPathSplitter() { @Override public void splitAndAdd(List<String> parts, String path) { int start = 0; int len = path.length(); for (int i = 0; i < len; i++) { char c = path.charAt(i); if (c == '.' || c == '[') { if (start < i) { parts.add(path.substring(start, i)); } start = i + 1; // 直接提取数组下标,跳过不必要的字符处理 if (c == '[') { while (i < len && path.charAt(i) != ']') { i++; } if (i > start) { parts.add(path.substring(start, i)); } start = i + 1; } } } if (start < len) { parts.add(path.substring(start)); } } }) .build();
2. 分批处理映射任务
如果无法一次性处理200k+对象的列表,可以将parameters按列表下标拆分,分批映射后再合并结果:
// 按列表下标分组属性 Map<Integer, Map<String, String>> batchGroups = new HashMap<>(); for (Map.Entry<String, String> entry : parameters.entrySet()) { String key = entry.getKey(); if (key.startsWith("list[")) { int idxEnd = key.indexOf(']'); int idx = Integer.parseInt(key.substring(5, idxEnd)); // 截取列表元素内部的属性路径 String subPath = key.substring(idxEnd + 2); batchGroups.computeIfAbsent(idx, k -> new HashMap<>()) .put(subPath, entry.getValue()); } else { // 单独处理根对象的非列表属性 } } // 分批映射并组装结果 List<NestedObject> nestedList = new ArrayList<>(batchGroups.size()); // 按下标排序确保顺序正确 List<Integer> sortedIndices = new ArrayList<>(batchGroups.keySet()); Collections.sort(sortedIndices); for (int idx : sortedIndices) { NestedObject obj = mapper.readMapAs(batchGroups.get(idx), NestedObject.class); nestedList.add(obj); } RootObject root = new RootObject(); root.setList(nestedList);
这种方式避免一次性加载所有路径节点到内存,大幅降低堆内存压力。
3. 构建JavaPropsSchema优化解析逻辑
JavaPropsSchema可以预先定义属性路径的结构,让Jackson提前知晓对象层级,减少动态生成路径节点的内存开销。构建示例如下:
// 定义嵌套对象的Schema JavaPropsSchema nestedSchema = JavaPropsSchema.builder() .type(NestedObject.class) .build(); // 定义根对象的Schema,指定列表属性"list"的元素Schema JavaPropsSchema rootSchema = JavaPropsSchema.builder() .withPropertySchema("list", JavaPropsSchema.builder() .arrayElementSchema(nestedSchema) .build()) .build(); // 使用Schema执行映射 RootObject root = mapper.readMapAs(parameters, RootObject.class, rootSchema);
预先定义Schema可以避免Jackson在解析时动态推断路径结构,减少内存占用。
4. 替换为自定义映射逻辑
如果Jackson的内存开销仍无法接受,可以直接遍历Map并手动赋值到嵌套对象,完全控制内存使用:
RootObject root = new RootObject(); List<NestedObject> nestedList = new ArrayList<>(200000); root.setList(nestedList); for (Map.Entry<String, String> entry : parameters.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (key.startsWith("list[")) { int idxEnd = key.indexOf(']'); int idx = Integer.parseInt(key.substring(5, idxEnd)); String subProp = key.substring(idxEnd + 2); // 确保列表容量足够,避免频繁扩容 while (nestedList.size() <= idx) { nestedList.add(new NestedObject()); } NestedObject target = nestedList.get(idx); // 根据属性名手动赋值 switch (subProp) { case "name": target.setName(value); break; case "age": target.setAge(Integer.parseInt(value)); break; // 其他属性依次处理 } } else { // 处理根对象的属性,比如root.setXXX(value) } }
这种方式没有Jackson的额外内存开销,但需要手动编写属性赋值逻辑,适合属性结构固定的场景。
内容的提问来源于stack exchange,提问作者Rokko_11
相关产品推荐
相关产品推荐

