又是一个程序员粗心的代码引起频繁FullGC的案例

简介:
这是笨神JVMPocket群里一位名为" 云何*住"的同学提出来的问题,问题现象是 CPU飙高并且 频繁FullGC
重现问题

这位同学的业务代码比较复杂,为了简化业务场景,笔者将其代码压缩成如下的代码片段:

 
  1. public class FullGCDemo {


  2. private static ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(50,

  3. new ThreadPoolExecutor.DiscardOldestPolicy());


  4. public static void main(String[] args) throws Exception {

  5. executor.setMaximumPoolSize(50);


  6. // 模拟xxl-job 100ms 调用一次, 原代码没有这么频繁

  7. for (int i=0; i<Integer.MAX_VALUE; i++){

  8. buildBar();

  9. Thread.sleep(100);

  10. }

  11. }


  12. private static void buildBar(){

  13. List<FutureContract> futureContractList = getAllFutureContract();

  14. futureContractList.forEach(contract -> {

  15. // do something

  16. executor.scheduleWithFixedDelay(() -> {

  17. try{

  18. doFutureContract(contract);

  19. }catch (Exception e){

  20. e.printStackTrace();

  21. }

  22. }, 2, 3, TimeUnit.SECONDS);

  23. });

  24. }


  25. private static void doFutureContract(FutureContract contract){

  26. // do something with futureContract

  27. }


  28. private static List<FutureContract> getAllFutureContract(){

  29. List<FutureContract> futureContractList = new ArrayList<>();

  30. // 问题代码这里每次只会new不到10个对象, 我这里new了100个是为了更快重现问题

  31. for (int i = 0; i < 100; i++) {

  32. FutureContract contract = new FutureContract(i, ... ...);

  33. futureContractList.add(contract);

  34. }

  35. return futureContractList;

  36. }

  37. }

说明,为了更好的还原问题,FutureContract.java 的定义建议尽量与问题代码保持一致:

  • 16个BigDecimal类型属性

  • 3个Long类型属性

  • 3个String类型属性

  • 4个Integer类型属性

  • 2个Date类型属性

问题代码运行时的JVM参数如下(JDK8):

 
  1. java -Xmx256m -Xms256m -Xmn64m FullGCDemo

你也可以先自己独立思考一下这块代码问题何在。

CPU飙高

这是第一个现象,top命令就能看到,找到我们的进程ID,例如91782。然后执行命令 top-H-p91782查看进程里的线程情况:

 
  1. PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND

  2. 91784 yyapp 20 0 2670m 300m 12m R 92.2 7.8 4:14.39 java

  3. 91785 yyapp 20 0 2670m 300m 12m R 91.9 7.8 4:14.32 java

  4. 91794 yyapp 20 0 2670m 300m 12m S 1.0 7.8 0:09.38 java

  5. 91799 yyapp 20 0 2670m 300m 12m S 1.0 7.8 0:09.39 java

由这段结果可知线程91784和91785很消耗CPU。将91784和91785分别转为16进制,得到16688和16689。接下来通过执行命令命令 jstack-l91782>91782.log导出线程栈信息(命令中是进程ID),并在线程dump文件中寻找16进制数16688和16689,得到如下两条信息:

 
  1. "GC task thread#0 (ParallelGC)" os_prio=0 tid=0x00007f700001e000 nid=0x16688 runnable

  2. "GC task thread#1 (ParallelGC)" os_prio=0 tid=0x00007f7000020000 nid=0x16689 runnable

由这两行结果可知,消耗CPU的是ParallelGC线程。因为问题代码搭配的JVM参数没有指定任何垃圾回收期,所以用的是默认的PS垃圾回收,所以这个JVM实例应该在频繁FullGC,通过命令 jstat-gcutil917825s查看GC表现可以验证,由这段结果可知,Eden和Old都占满了,且不再发生YGC,但是却在频繁FGC,此时的应用已经不能处理任务,相当于假死了,好可怕:

 
  1. S0 S1 E O M CCS YGC YGCT FGC FGCT GCT

  2. 0.00 0.00 100.00 99.98 78.57 83.36 5 0.633 366 327.647 328.281

  3. 0.00 0.00 100.00 99.98 78.57 83.36 5 0.633 371 331.965 332.598

  4. 0.00 0.00 100.00 99.98 78.57 83.36 5 0.633 376 336.996 337.629

  5. 0.00 0.00 100.00 99.98 78.57 83.36 5 0.633 381 340.795 341.428

  6. 0.00 0.00 100.00 99.98 78.57 83.36 5 0.633 387 346.268 346.901

揪出真凶

到这里基本可以确认是有对象没有释放导致即使发生FullGC也回收不了引起的,准备dump进行分析看看Old区都是些什么妖魔鬼怪,执行命令 jmap-dump:format=b,file=91782.bin91782,用MAT分析时,强烈建议开启 keep unreachable objects

835e2b956b2a95dae6041d675e95844b01654373

接下来点击Actions下的Histogram,查找大对象:

b32f4777d222d68b6746a9357478907fcf4b4264

下面贴出的是原图,而不是笔者的Demo代码跑出来的:

e4f25f15291850d6a2949beed026b3f365fdceaf

由这段代码可知,大量的FutureContract和BigDecimal(说明:因为FutureContract中有多达16个BigDecimal类型的属性),FutureContract占了120MB,BigDecimal占了95MB。那么就可以断定问题是与FutureContract相关的代码造成的,如果是正常的JVM示例,Histogram 试图最占内存的是byte[]和char[]两个数组,两者合计一般会占去80%左右的内存,远远超过其他对象占用的内存。

接下来通过FutureContract就找到上面这块buildBar方法代码,那么为什么是这块代码无法释放呢?单独把这块代码拧出来看看,这里用到了ScheduledThreadPoolExecutor定时调度,且每3秒执行一次,然而定时器中需要的参数来自外面的 List<FutureContract>,这就会导致 List<FutureContract>这个对象一致被一个定时任务引用,永远无法回收,从而导致FutureContract不断晋升到Old区,直到占满Old区然后频繁FullGC。

 
  1. private static void buildBar(){

  2. List<FutureContract> futureContractList = getAllFutureContract();

  3. futureContractList.forEach(contract -> {

  4. // do something

  5. executor.scheduleWithFixedDelay(() -> {

  6. try{

  7. doFutureContract(contract);

  8. }catch (Exception e){

  9. e.printStackTrace();

  10. }

  11. }, 2, 3, TimeUnit.SECONDS);

  12. });

  13. }

那么为什么会出现这种情况呢?我相信一个程序员不应该犯这样的低级错误,后来看到原生代码,我做出一个比较合理的猜测,其本意可能是想通过调用 Executorexecutor来异步执行,谁知小手一抖,在红色框那里输入了taskExecutor,而不是executor:

834cbf29470319101cd59e70d8eb6a454f01e889
解决问题

OK,知道问题的根因,想解决问题就比较简单了,将taskExecutor改成executor即可:

 
  1. private static ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(128));

  2. private static void buildBar(){

  3. List<FutureContract> futureContractList = getAllFutureContract();

  4. futureContractList.forEach(contract -> {

  5. // do something

  6. executor.execute(() -> {

  7. try{

  8. doFutureContract(contract);

  9. }catch (Exception e){

  10. e.printStackTrace();

  11. }

  12. });

  13. });

  14. }

或者将这一块直接改成同步处理,不需要线程池:

 
  1. private static void buildBar(){

  2. List<FutureContract> futureContractList = getAllFutureContract();

  3. futureContractList.forEach(contract -> {

  4. // do something

  5. try{

  6. doFutureContract(contract);

  7. }catch (Exception e){

  8. e.printStackTrace();

  9. }

  10. });

  11. }


原文发布时间为: 2018-11-08
本文作者: Java技术驿站
本文来自云栖社区合作伙伴“
Java技术驿站”,了解相关信息可以关注“Java技术驿站”。

相关文章
|
3月前
|
前端开发 JavaScript
【面试题】如何避免使用过多的 if else?
【面试题】如何避免使用过多的 if else?
|
5月前
|
缓存 Java 程序员
如何写出高性能代码(三)优化内存回收(GC)
可复用性在这里指的是,大多数的对象都是可以被复用的,这些可以被复用的对象就没必要每次都新建出来,浪费内存空间了。 处了巧用数据特性 中的例子,我这里再个Java中已经被用到的例子,这个还得从一段奇怪的代码说起。
31 0
|
8月前
|
缓存 算法 Java
透彻理解JVM中垃圾回收GC生产参数,停顿时间+执行效率相关参数
停顿时间相关参数 部分垃圾回收器实现了GC执行时应用最大停顿时间的功能,所以提供参数用于应用控制停顿时间。另外,GC为了满足停顿时间,会设计和实现一些动态算法来调整堆空间,从而满足停顿时间这个目标。本节介绍相关参数。 该参数表示GC的最大的停顿时间。不同GC对于该参数的行为不一致,具体来说: 1)若Parallel GC中GC执行的时间超过该值,将导致调整新生代和老生代的大小(参数UseAdaptiveSizePolicy设置为true)。参数的默认值为4294 967 295,大约为50天(所以通常不会触发这个调整策略)。 2)若G1中GC执行的时间超过该值,将导致调整新生代的大小和
|
11月前
代码优雅之道——如何干掉过多的if else
代码优雅之道——如何干掉过多的if else
92 0
|
11月前
|
算法 Java
一文搞懂Y-GC和Full GC的触发条件
1 Young GC触发时机 一般在新生代Eden区满后触发,采用复制算法回收新生代垃圾。
777 0
|
Java
结合代码和内存变化图一步步弄懂JVM的FullGC
一步步结合代码去验证jvm的内存变化,并画出内存变化的示意图,从而探索出jvm fullGC的原因。
352 0
|
算法 安全 Java
【JVM性能优化】CMS回收器的Full-GC流程分析以及问题探究
【JVM性能优化】CMS回收器的Full-GC流程分析以及问题探究
543 0
【JVM性能优化】CMS回收器的Full-GC流程分析以及问题探究
一次性讲清楚 Handler 使用不当导致的内存泄露?
一次性讲清楚 Handler 使用不当导致的内存泄露?
一次性讲清楚 Handler 使用不当导致的内存泄露?
|
运维 监控 Java
MyBatisPlus的in方法入参数量过多导致的JVM频繁FullGC案例
MyBatisPlus的in方法入参数量过多导致的JVM频繁FullGC案例
440 0
MyBatisPlus的in方法入参数量过多导致的JVM频繁FullGC案例
|
缓存 算法 安全
如何写出高性能代码之优化内存回收(GC)
同一份逻辑,不同人的实现的代码性能会出现数量级的差异; 同一份代码,你可能微调几个字符或者某行代码的顺序,就会有数倍的性能提升;同一份代码,也可能在不同处理器上运行也会有几倍的性能差异;十倍程序员 不是只存在于传说中,可能在我们的周围也比比皆是。十倍体现在程序员的方法面面,而代码性能却是其中最直观的一面。
196 0
如何写出高性能代码之优化内存回收(GC)