最近遇到一项目,需要在手机端存储用户数据,实现离线访问。其中用户数据处理的逻辑如下图:
服务端从亚马逊S3上下载用户JSON文本数据库
反序列化用户数据
更新用户数据
将用户数据序列化为JSON文本
保存到亚马逊S3上
由于项目设计缺陷,用户所有的数据都存储一个Map对象里,导致Map对象过大,在项目运行过程中出现了内存不足的异常。为了解决内存不足问题,服务端采用了JacksonStreamingApi 优化了JSON序列及反序列化步骤,避免将整个用户文件载入到内存中,至此内存不足的异常就再也没有发生。
其实这里有个问题,如果是用户数据过大,内存不足异常会在步骤3结束后就会发生,为什么偏偏在步骤4序列化为JSON时抛出呢?这里就要说到 Groovy的LazyMap 了。
LazyMap代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 public class LazyMap extends AbstractMap <String, Object> { static final String JDK_MAP_ALTHASHING_SYSPROP = System.getProperty("jdk.map.althashing.threshold" ); private Map<String, Object> map; private int size; private String[] keys; private Object[] values; public LazyMap () { keys = new String [5 ]; values = new Object [5 ]; } ... public Object put (String key, Object value) { if (map == null ) { for (int i = 0 ; i < size; i++) { String curKey = keys[i]; if ((key == null && curKey == null ) || (key != null && key.equals(curKey))) { Object val = values[i]; keys[i] = key; values[i] = value; return val; } } keys[size] = key; values[size] = value; size++; if (size == keys.length) { keys = grow(keys); values = grow(values); } return null ; } else { return map.put(key, value); } } public Object get (Object key) { buildIfNeeded(); return map.get(key); } private void buildIfNeeded () { if (map == null ) { if (Sys.is1_8OrLater() || (Sys.is1_7() && JDK_MAP_ALTHASHING_SYSPROP != null )) { map = new LinkedHashMap <String, Object>(size, 0.01f ); } else { map = new TreeMap <String, Object>(); } for (int index = 0 ; index < size; index++) { map.put(keys[index], values[index]); } this .keys = null ; this .values = null ; } } }
从代码中可以看出:对于未进行过读操作(get,containsKey等)的LazyMap对象,keys和values分别存在了两个数组中,一旦调用了读取方法,LazyMap会将数组转化成Map对象,就是这一步操作引起了内存占用变化。
拿大小约为35MB的用户文件测试,反序列化后,内存中LazyMap对象为73MB(line 1),一旦对对象进行toJson
操作(line 2),内存占用上升到了559MB(line 3)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void main(String[] args) { File jsonFile = new File('data-format.json' ) Object obj = new JsonSlurper().parse(jsonFile) showSize(obj) JsonOutput.toJson(obj) showSize(obj) } static void showSize(obj) { def size = ObjectSizeCalculator.getObjectSize(obj) / 1024 / 1024 println("Object memory size is $size MB" ) } Object memory size is 73.51363372802734375 MB Object memory size is 559.32244873046875 MB
由于用户数据文件较大且嵌套了多层Map,加之JsonOutput.toJson
方法会遍历LazyMap对象所有节点,相当于对所有节点进行了读操作,导致节点中的数组转换成Map对象,最终引起内存不足异常。