线上一个项目,每次机器重启时项目都会报出大量的Timeout,同时每一个集群节点都被监控到较为频繁的Full GC。以后同事虽然尝试过JVM调优并适当调大了老年代空间,但依然不能根本上解决问题。当时该问题被初步归咎于系统中整合的Groovy,但并未证明。问题汇总以下:java
随后,我着手作另一个项目GLUE,该项目一样须要整合Groovy,在作并发测试时,我发现了一样的问题。缓存
通过排查并作出优化,新项目GLUE在并发测试下基本不存在Full GC的问题,在此将问题处理过程记录以下,但愿能够给你们一点参考。数据结构
新系统GLUE底层基于Groovy实现,系统经过执行 “groovy.lang.GroovyClassLoader.parseClass(groovyScript)” 进行Groovy代码解析,Groovy为了保证解析后执行的都是最新的脚本内容,每进行一次解析都会生成一次新命名的Class文件,底层代码以下图:并发
所以,若是Groovy类加载器设置为单例,当对脚本(即便同一段脚本)屡次执行该方法时,会致使 “GroovyClassLoader” 装载的Class愈来愈多。若是此处临时加载的类不可以被及时释放,最终将会致使PermGen OutOfMemoryError。即便状况没有那么糟糕,也会引发频繁的full GC,从而影响稳定运行时的性能。app
而后,我翻阅了线上启动时大量Timeout以及Full GC的项目代码。发现该项目一样适用“GroovyClassLoader”进行groovy脚本解析,断点接入以下:异步
首先,我发现该项目中的Groovy类加载器是单例; 其次,该项目中的加载一次页面,将会调用多达31次“groovy.lang.GroovyClassLoader.parseClass(groovyScript)”方法进行groovy脚本解析。这很震惊,可是庆幸的是,该系统对解析后的Class作了缓存。tcp
通过分析,该项目启动是被报大量Timeout和运行Full GC的问题基本锁定,缘由以下:ide
启动时Timeout缘由:项目启动完成后,该节点通过健康检查无误被切到线上集群环境,接收线上流量。可是,因为该项目上单个页面模块太多,上文中一张页面加载须要执行解析函数多达31次,并且该项目还托管这许多其余的页面,这致使这些页面的预热时间比较久。可是不幸的是,项目已经经过了健康检查,大量流量涌入阻塞等待页面加载完成,所以致使项目启动时被报大量Timeout。函数
频繁Full GC缘由:该项目中Groovy类加载使用单例,当对脚本(即便同一段脚本)屡次执行该方法时,会致使 “GroovyClassLoader” 装载的Class愈来愈多。若是此处临时加载的类不可以被及时释放,最终将会致使PermGen OutOfMemoryError。即便状况没有那么糟糕,也会引发频繁的full GC,从而影响稳定运行时的性能。性能
为了对上述猜测进行验证,设计了一下三段代码进行简单测试。代码逻辑分别为:
本文中测试方法为,启动下面三段测试代码中的Main方法,经过查看各自JVM的GC状况从而验证GroovyClassLoader对JVM的影响。
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import groovy.lang.GroovyClassLoader; public class Test { public static void main(String[] args) throws InterruptedException, IOException { final String code = readAll("DemoHandlerAImpl.groovy"); final AtomicInteger count = new AtomicInteger(0); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } Object object = parseClass(code); System.out.println("COUNT1=" + count.incrementAndGet() + ", " + object.hashCode()); } } }); } } static GroovyClassLoader classLoader = new GroovyClassLoader(); public static Object parseClass(String code){ return classLoader.parseClass(code); } public static String readAll(String logFile){ try { InputStream ins = null; BufferedReader reader = null; try { ins = new FileInputStream(Thread.currentThread().getContextClassLoader().getResource(logFile).getPath()); reader = new BufferedReader(new InputStreamReader(ins, "utf-8")); if (reader != null) { String content = null; StringBuilder sb = new StringBuilder(); while ((content = reader.readLine()) != null) { sb.append(content).append("\n"); } return sb.toString(); } } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import groovy.lang.GroovyClassLoader; public class Test2 { public static void main(String[] args) throws InterruptedException, IOException { final String code = readAll("DemoHandlerAImpl.groovy"); final AtomicInteger count = new AtomicInteger(0); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } Object object = parseClass(code); System.out.println("COUNT2=" + count.incrementAndGet() + ", " + object.hashCode()); } } }); } } static GroovyClassLoader classLoader = new GroovyClassLoader(); public static Object parseClass(String code){ classLoader = new GroovyClassLoader(); return classLoader.parseClass(code); } public static String readAll(String logFile){ try { InputStream ins = null; BufferedReader reader = null; try { ins = new FileInputStream(Thread.currentThread().getContextClassLoader().getResource(logFile).getPath()); reader = new BufferedReader(new InputStreamReader(ins, "utf-8")); if (reader != null) { String content = null; StringBuilder sb = new StringBuilder(); while ((content = reader.readLine()) != null) { sb.append(content).append("\n"); } return sb.toString(); } } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import groovy.lang.GroovyClassLoader; public class Test3 { public static void main(String[] args) throws InterruptedException, IOException { final String code = readAll("DemoHandlerAImpl.groovy"); final Object object = parseClass(code); final AtomicInteger count = new AtomicInteger(0); ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 100; i++) { executorService.execute(new Runnable() { @Override public void run() { while (true) { try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("COUNT3=" + count.incrementAndGet() + ", " + object.hashCode()); } } }); } } static GroovyClassLoader classLoader = new GroovyClassLoader(); public static Object parseClass(String code){ classLoader = new GroovyClassLoader(); return classLoader.parseClass(code); } public static String readAll(String logFile){ try { InputStream ins = null; BufferedReader reader = null; try { ins = new FileInputStream(Thread.currentThread().getContextClassLoader().getResource(logFile).getPath()); reader = new BufferedReader(new InputStreamReader(ins, "utf-8")); if (reader != null) { String content = null; StringBuilder sb = new StringBuilder(); while ((content = reader.readLine()) != null) { sb.append(content).append("\n"); } return sb.toString(); } } finally { if (ins != null) { try { ins.close(); } catch (IOException e) { e.printStackTrace(); } } if (reader != null) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } catch (Exception e) { e.printStackTrace(); } return null; } }
import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * 场景A:托管 “配置信息” ,尤为适用于数据结构比较复杂的配置项 * 优势:在线编辑;推送更新;+ 直观; * @author xuxueli 2016-4-14 15:36:37 */ public class DemoHandlerAImpl { public Object handle(Map<String, Object> params) { // 【基础类型配置】 boolean ifOpen = true; // 开关 int smsLimitCount = 3; // 短信发送次数阀值 String brokerURL = "failover:(tcp://127.0.0.1:61616,tcp://127.0.0.2:61616)"; // 套接字配置 // 【列表配置】 Set<Integer> blackShops = new HashSet<Integer>(); // 黑名单列表 blackShops.add(15826714); blackShops.add(15826715); blackShops.add(15826716); blackShops.add(15826717); blackShops.add(15826718); blackShops.add(15826719); // 【KV配置】 Map<Integer, String> emailDispatch = new HashMap<Integer, String>(); // 不一样BU标题文案配置 emailDispatch.put(555, "淘宝"); emailDispatch.put(666, "天猫"); emailDispatch.put(777, "聚划算"); // 【复杂集合配置】 Map<Integer, List<Integer>> openCitys = new HashMap<Integer, List<Integer>>(); // 不一样城市推荐商户配置 openCitys.put(11, Arrays.asList(15826714, 15826715)); openCitys.put(22, Arrays.asList(15826714, 15651231, 86451231)); openCitys.put(33, Arrays.asList(48612323, 15826715)); return smsLimitCount; } }
从日志能够发现,共解析groovy达38694次。
从日志能够发现,共解析groovy达39100次。
从日志能够发现,共解析groovy达40000次。
经过观察内存曲线图,能够获取测试结果:
从上述测试结果能够获得结论:
PermGen中对象回收规则:ClassLoader能够被回收,其下的全部加载过的没有对应实例的类信息(保存在PermGen)可被回收。所以,JVM回收以后,能够将GroovyClassLoader加载的冗余新信息回收掉。
可是。GC在JVM中一般是由一个或一组进程来实现的,它自己也和用户程序同样占用heap空间,运行时也占用CPU。所以,当GC运行时间较长时,用户可以感到 Java程序的停顿。所以,尽可能避免GC。