爱凑网

IT技术日记

Jackson JSON 巧遇 G1 回收器的 Bug

上一篇blog介绍了NMT分析堆外内存分析,但是并未有详细解释为什么我们会遇到JDK8的bug,这篇blog主要来解释清楚,为什么我们会遇到了整个问题。

JDK Bug分析

先对这个JDK Bug做个分析,JDK-8180048 : Interned string and symbol table leak memory during parallel unlinking

注:此bug已经在下面的JDK版本中被修复

问题原因是JDK8 G1回收器并未对已经没有被使用的的intern String进行回收,从而导致堆外内存不断增加,主要就是Symbol域 (参见上一篇关于NMT的介绍);使用CMS不会有这个问题。

可以通过下面2组不同的测试来对比并重现这个问题:

  • 测试源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StringInterner {
public static volatile String lastString;

public static void main(String[] args) {
for (int iterations = 0; iterations < 40;) {
String baseName = UUID.randomUUID().toString();
for (int i = 0; i < 1_000_000; i++) {
lastString = (baseName + i).intern();
}
if (++iterations % 10 == 0) {
System.gc();
}
LockSupport.parkNanos(500_000_000);
}
}
}

  • 不使用G1 Collector
1
2
java -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m -XX:+UseStringDeduplication -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -Xloggc:gc-jdk8-marksweep.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms256m -Xmx256m -cp classes StringInterner

  • 使用G1 Collector重现问题
1
2
java -XX:NativeMemoryTracking=detail -XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics -XX:+UseG1GC -XX:MetaspaceSize=100m -XX:MaxMetaspaceSize=100m -Xloggc:gc-jdk8-g1.log -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xms256m -Xmx256m -cp classes StringInterner

检查NMT报告并且搜索Symbol,可以发现使用CMS和G1 Collector的Symbol域内存差异很大,而且G1 Collector的Symbol区域会持续增加。

1
2
3
Symbol (reserved=27549KB, committed=27549KB)
Symbol (reserved=424789KB, committed=424789KB)

Jackson JSON 处理 其实到这里已经比较清楚了,为什么Jackson JSON处理会触发JDK这个问题,基本上可以确定的是Jackson一定是在某个地方使用了Intern String导致的。

从Jackson Parser的Feature定义中找到一个选项

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
/**
* Feature that determines whether JSON object field names are
* to be canonicalized using {@link String#intern} or not:
* if enabled, all field names will be intern()ed (and caller
* can count on this being true for all such names); if disabled,
* no intern()ing is done. There may still be basic
* canonicalization (that is, same String will be used to represent
* all identical object property names for a single document).
*<p>
* Note: this setting only has effect if
* {@link #CANONICALIZE_FIELD_NAMES} is true -- otherwise no
* canonicalization of any sort is done.
*<p>
* This setting is enabled by default.
*/
INTERN_FIELD_NAMES(true),

/**
* Feature that determines whether JSON object field names are
* to be canonicalized (details of how canonicalization is done
* then further specified by
* {@link #INTERN_FIELD_NAMES}).
*<p>
* This setting is enabled by default.
*/