Fastjson反序列化低版本使用ParameterizedTypeImpl导致内存泄露


01.29 同事反馈搜索系统不正常,所有接口响应都很慢,并且会超时

上应用监控平台看了下机器的数据

发现每台机器老年代内存占用比较高,一直在old gc,但是没有回收到内存,所以一直STW,导致接口反应极慢。

看了下发布记录,最近一次发布是在1-10号,排除是发布新版本的问题

尝试重启机器,恢复正常

image-20220222175454793

问题是恢复了,但是OOM的原因还没有找到

使用dump命令下一个dump文件下来分析

jmap -dump:live,format=b,file=/dump2021.hprof pid

gzip -c dump2019.hprof > dump.gz

分析dump发现com.alibaba.fastjson.util.IdentityHashMap$Entry对象比较多

企业微信截图_16443904106293

搜了下代码引用

image-20220222191501995

排查搜索组写的ES源码发现,在查询成功后直接返回非ApiResult封装的对象时,每次解析都会new一个ParameterizedTypeImpl进行处理。

这个时候并不角色有什么问题,因为就算new一个对象出来,解析完也会被回收,不会走到老年代中于是本地跑了个方法尝试重现这个问题

  /*
        -Xms30m
        -Xmx30m
        -XX:+PrintGCDateStamps
        -XX:+PrintGCDetails
        -XX:+PrintHeapAtGC
        -XX:+PrintGCApplicationStoppedTime
        -Xloggc:/tmp/gc_%p_%t_.log
        -XX:+HeapDumpOnOutOfMemoryError
        -XX:HeapDumpPath=/tmp/
         */public static void main(String[] args) {       final long start = System.currentTimeMillis();      final AtomicLong counter = new AtomicLong();
      Runtime.getRuntime().addShutdownHook(new Thread(() -> {
          System.out.println(counter.get());
          System.out.println(System.currentTimeMillis() - start);
      }));
       
      SomeInfo someInfo = new SomeInfo();
      someInfo.setName("jerry");
      EsBaseResult<SomeInfo> result = new EsBaseResult<>();
      result.setResult(someInfo);
      result.setCode(0);
      result.setMessage("Success");
      String json = JSON.toJSONString(result);

      while (true){
          EsBaseResult<SomeInfo> response = JSON.parseObject(json,
                  new ParameterizedTypeImpl(new Type[] { new ParameterizedTypeImpl(new Type[] { SomeInfo.class }, null, EsQueryResult.class) }, null, EsBaseResult.class));
          Objects.requireNonNull(response);
          counter.incrementAndGet();
      }
  }

  @Data
  public static class EsBaseResult<T> implements Serializable {

      private Integer           code;

      private String           message;

      private T                 result;

      public boolean isSuccess(){
          return code ==0;
      }

      public String getErrorCode(){
          return code+"";
      }
  }

  @Data
  public static class SomeInfo {
      private String name;
  }

不出意外,查询7k次后OOM了

Exception in thread "main" com.alibaba.fastjson.JSONException: GC overhead limit exceeded
at com.alibaba.fastjson.parser.DefaultJSONParser.parseObject(DefaultJSONParser.java:693)
at com.alibaba.fastjson.JSON.parseObject(JSON.java:396)

COUNT: 7832
COST: 6273ms

查看dump文件:

image-20220222191522197

从GC-Root上来看,ParserConfig占比了绝大部分.跟踪了代码,发现了问题所在

ParserConfig.class

image-20220222191605638

IdentityHashMap.class

image-20220222191615914

System.class

image-20220222191627707

翻译一下:

无论给定的x对象是否覆盖了hashCode()方法,都会调用默认的hashCode()方法返回hashCode,如果x == null, 返回0。

这个默认的hashCode()方法就是Object类中的hashCode方法。

这说明默认的hashCode方法是根据对象的地址转换所得到的。

每次new出来的新对象的地址都是不同,所以会出现这个问题

解决方案:

1.使用同一个对象,采用static对象引用

2.升级高版本fasjson,新版中已经修复了这个问题,原理是增加了一个cache存储类型

https://github.com/alibaba/fastjson/commit/38070ca5a2713795403334c78f43470a560684b7

采用了方案2,生产压测后恢复正常,问题解决

压测前:

image-20220222191701038

压测后:

image-20220222191647777

反思:
使用开源框架的时候,需及时更新新版本,避免已知问题


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM