问题
一次线上发版过程中发现应用的接口出现大量的超时现象.通过告警和链路追踪系统发现是有一个应用的dubbo请求大量超时.速度进入服务器查看日志发现出现大量的OOM:permGen space.执行jstat命令发现有些机器的perm区域使用率达到了100%,频繁的出现fullgc的情况.
因为没有保留现场的全部命令执行查看过程,以下以问题复现的代码执行过程的查找过程来演示.
问题排查
- 问题出现后通过jstat查看jvm的回收情况:
C:\Program Files (x86)\Java\jdk1.7.0_80\bin>jstat.exe -gcutil 6968 3000
S0 S1 E O P YGC YGCT FGC FGCT GCT
65.51 0.00 73.79 79.43 84.92 528 2.940 33 1.125 4.065
7.56 0.00 66.49 79.52 84.92 530 2.947 33 1.125 4.072
100.00 0.00 16.75 88.73 84.92 532 2.958 33 1.125 4.083
0.00 100.00 89.65 89.34 84.92 533 2.963 33 1.125 4.089
0.00 28.91 39.27 89.45 84.92 535 2.970 33 1.125 4.095
0.00 100.00 12.80 99.16 84.92 537 2.985 33 1.125 4.110
0.00 0.00 61.73 60.18 84.92 538 2.990 34 1.168 4.159
61.35 0.00 32.58 60.94 84.92 540 3.000 34 1.168 4.169
0.00 31.89 79.17 61.02 84.92 541 3.004 34 1.168 4.172
0.00 100.00 72.83 63.37 85.01 543 3.013 34 1.168 4.182
0.00 100.00 20.57 72.12 85.01 545 3.028 34 1.168 4.196
90.98 0.00 91.15 72.49 85.01 546 3.032 34 1.168 4.201
30.64 0.00 38.54 72.77 85.01 548 3.040 34 1.168 4.208
100.00 0.00 8.84 82.44 85.01 550 3.061 34 1.168 4.229
0.00 100.00 57.00 89.63 85.01 551 3.071 34 1.168 4.239
0.00 85.90 27.64 91.46 85.01 553 3.084 34 1.168 4.252
56.09 0.00 75.19 91.57 85.01 554 3.087 34 1.168 4.255
7.61 0.00 65.82 91.65 85.01 556 3.093 34 1.168 4.262
0.00 0.00 14.92 60.00 85.01 558 3.127 35 1.243 4.371
0.00 100.00 83.44 62.88 85.01 559 3.133 35 1.243 4.376
0.00 64.12 30.94 68.97 85.01 561 3.146 35 1.243 4.389
39.64 0.00 100.00 69.05 85.01 562 3.150 35 1.243 4.394
100.00 0.00 46.22 71.22 85.01 564 3.166 35 1.243 4.409
100.00 0.00 16.05 78.76 85.01 566 3.182 35 1.243 4.425
0.00 100.00 62.86 79.89 85.01 567 3.186 35 1.243 4.429
- 通过jmap查看堆对象信息没有什么有用的信息:
C:\Program Files (x86)\Java\jdk1.7.0_80\bin>jmap.exe -histo 6968
num #instances #bytes class name
----------------------------------------------
1: 187986 6015552 java.util.TreeMap$Entry
2: 1232 2802872 [I
3: 58438 1504824 [Ljava.lang.Object;
4: 30284 1453632 java.util.TreeMap
5: 77037 1232592 java.lang.Long
6: 15572 1134400 [C
7: 9408 1072320 <constMethodKlass>
8: 9408 678976 <methodKlass>
9: 18381 588192 java.io.ObjectStreamClass$WeakClassKey
10: 1565 586112 <instanceKlassKlass>
11: 1565 567648 <constantPoolKlass>
12: 30257 484112 java.util.TreeMap$KeySet
13: 30246 483936 javax.management.openmbean.CompositeDataSupport
14: 12543 401376 java.util.LinkedHashMap$Entry
15: 1490 319552 <constantPoolCacheKlass>
16: 18754 300064 java.lang.Boolean
17: 12330 295920 java.lang.String
18: 1787 263528 [B
19: 15227 251720 [Ljavax.management.openmbean.CompositeData;
20: 10802 172832 java.util.Collections$UnmodifiableRandomAccessList
21: 10799 172784 java.util.Arrays$ArrayList
22: 1709 157776 java.lang.Class
23: 9688 155008 java.lang.Integer
24: 1742 147016 [Ljava.util.HashMap$Entry;
25: 4045 129440 java.lang.ref.SoftReference
26: 3938 126016 java.util.TreeMap$KeyIterator
- 使用工具arthas 查看classloader发现fastjson的ASMClassLoader对象数异常:
-
速度做一次堆dump看完整的对象分配情况,通过memoryAnalyzer查看堆信息,找到一个非常有意思的情况:
从这个堆信息中可以发现有很多Fastjson_ASM生成的动态ASM字节码类信息,但是缺大量的没有关联到实际的堆内对象.好吧,那就开始跟下代码,看看为什么会有这么多的动态对象出现.
问题解决
我们以如下的代码来解析整个过程:
UserBean bean = JSON.parseObject(json, UserBean.class);
跟进去我们可以发现默认的fastjson使用的一个全局的反序列化解析器:
默认的全局反序列化只对以下类型做了全局的反序列,肯定是没有我们的pojo对象的反序列化解析器的:
primitiveClasses.add(boolean.class);
primitiveClasses.add(Boolean.class);
primitiveClasses.add(char.class);
primitiveClasses.add(Character.class);
primitiveClasses.add(byte.class);
primitiveClasses.add(Byte.class);
primitiveClasses.add(short.class);
primitiveClasses.add(Short.class);
primitiveClasses.add(int.class);
primitiveClasses.add(Integer.class);
primitiveClasses.add(long.class);
primitiveClasses.add(Long.class);
primitiveClasses.add(float.class);
primitiveClasses.add(Float.class);
primitiveClasses.add(double.class);
primitiveClasses.add(Double.class);
primitiveClasses.add(BigInteger.class);
primitiveClasses.add(BigDecimal.class);
primitiveClasses.add(String.class);
primitiveClasses.add(java.util.Date.class);
primitiveClasses.add(java.sql.Date.class);
primitiveClasses.add(java.sql.Time.class);
primitiveClasses.add(java.sql.Timestamp.class);
derializers.put(SimpleDateFormat.class, DateFormatDeserializer.instance);
derializers.put(java.sql.Timestamp.class, TimestampDeserializer.instance);
derializers.put(java.sql.Date.class, SqlDateDeserializer.instance);
derializers.put(java.sql.Time.class, TimeDeserializer.instance);
derializers.put(java.util.Date.class, DateDeserializer.instance);
derializers.put(Calendar.class, CalendarCodec.instance);
derializers.put(JSONObject.class, JSONObjectDeserializer.instance);
derializers.put(JSONArray.class, JSONArrayDeserializer.instance);
derializers.put(Map.class, MapDeserializer.instance);
derializers.put(HashMap.class, MapDeserializer.instance);
derializers.put(LinkedHashMap.class, MapDeserializer.instance);
derializers.put(TreeMap.class, MapDeserializer.instance);
derializers.put(ConcurrentMap.class, MapDeserializer.instance);
derializers.put(ConcurrentHashMap.class, MapDeserializer.instance);
derializers.put(Collection.class, CollectionDeserializer.instance);
derializers.put(List.class, CollectionDeserializer.instance);
derializers.put(ArrayList.class, CollectionDeserializer.instance);
derializers.put(Object.class, JavaObjectDeserializer.instance);
derializers.put(String.class, StringCodec.instance);
derializers.put(StringBuffer.class, StringCodec.instance);
derializers.put(StringBuilder.class, StringCodec.instance);
derializers.put(char.class, CharacterCodec.instance);
derializers.put(Character.class, CharacterCodec.instance);
derializers.put(byte.class, NumberDeserializer.instance);
derializers.put(Byte.class, NumberDeserializer.instance);
derializers.put(short.class, NumberDeserializer.instance);
derializers.put(Short.class, NumberDeserializer.instance);
derializers.put(int.class, IntegerCodec.instance);
derializers.put(Integer.class, IntegerCodec.instance);
derializers.put(long.class, LongCodec.instance);
derializers.put(Long.class, LongCodec.instance);
derializers.put(BigInteger.class, BigIntegerCodec.instance);
derializers.put(BigDecimal.class, BigDecimalCodec.instance);
derializers.put(float.class, FloatCodec.instance);
derializers.put(Float.class, FloatCodec.instance);
derializers.put(double.class, NumberDeserializer.instance);
derializers.put(Double.class, NumberDeserializer.instance);
derializers.put(boolean.class, BooleanCodec.instance);
derializers.put(Boolean.class, BooleanCodec.instance);
derializers.put(Class.class, ClassDerializer.instance);
derializers.put(char[].class, CharArrayDeserializer.instance);
derializers.put(AtomicBoolean.class, BooleanCodec.instance);
derializers.put(AtomicInteger.class, IntegerCodec.instance);
derializers.put(AtomicLong.class, LongCodec.instance);
derializers.put(AtomicReference.class, ReferenceCodec.instance);
derializers.put(WeakReference.class, ReferenceCodec.instance);
derializers.put(SoftReference.class, ReferenceCodec.instance);
derializers.put(UUID.class, UUIDCodec.instance);
derializers.put(TimeZone.class, TimeZoneCodec.instance);
derializers.put(Locale.class, LocaleCodec.instance);
derializers.put(Currency.class, CurrencyCodec.instance);
derializers.put(InetAddress.class, InetAddressCodec.instance);
derializers.put(Inet4Address.class, InetAddressCodec.instance);
derializers.put(Inet6Address.class, InetAddressCodec.instance);
derializers.put(InetSocketAddress.class, InetSocketAddressCodec.instance);
derializers.put(File.class, FileCodec.instance);
derializers.put(URI.class, URICodec.instance);
derializers.put(URL.class, URLCodec.instance);
derializers.put(Pattern.class, PatternCodec.instance);
derializers.put(Charset.class, CharsetCodec.instance);
derializers.put(Number.class, NumberDeserializer.instance);
derializers.put(AtomicIntegerArray.class, AtomicIntegerArrayCodec.instance);
derializers.put(AtomicLongArray.class, AtomicLongArrayCodec.instance);
derializers.put(StackTraceElement.class, StackTraceElementDeserializer.instance);
derializers.put(Serializable.class, JavaObjectDeserializer.instance);
derializers.put(Cloneable.class, JavaObjectDeserializer.instance);
derializers.put(Comparable.class, JavaObjectDeserializer.instance);
derializers.put(Closeable.class, JavaObjectDeserializer.instance);
所有后续的任何调用,使用:
ObjectDeserializer derializer = derializers.get(type);
这个方式来获取我们对象的反序列化解析器的结构都应该是为null的.最后会走到代码块1中:
derializer = createJavaBeanDeserializer(clazz, type);
并最终通过asmFactory来进行对象的asm动态对象生成:
return asmFactory.createJavaBeanDeserializer(this, clazz, type);
重点来了,这个asm生成的动态对象会在方法区中生成我们的对象信息,所以我们看到FastJsaon_ASM对象.但是为什么会生成多呢?起始答案已经呼之欲出了.
我们前面的图片中看到asmFactory创建完成动态代理对象后会返回反序列化解释器并放入解析器的Map集合中,即如下语句(上图的代码块2):
putDeserializer(type, derializer);
这个就意味着,fastJson是先去调用asm生成动态代理对象,然后再把解释器设置到解析器的Map集合中,并发情况就有可能会出现多个线程进来解析器的Map集合中都没有我们对象的解析器的情况,所以就会出现多次调用asm的情况.
注意: 我们使用的比较老的fastjson版本
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.6</version>
</dependency>
在这个版本中,fastjson会对我们的pojo对象和这个对象的每个属性都生成asm的动态对象,所以当你的并发高,对象属性又多的时候,在方法区中的对象会呈现一种爆炸式的上涨.
在fastjson的新版本中,已经去掉了给方法生成asm动态对象,所以相对会安全很多.但是依然会有无用的asm动态对象在方法区中.