背景
问题定位
验证和解决
总结
随机找了台问题机器ssh上去,用top命令查看CPU情况,在10-20%正常范围内波动。用arthas的dashboard和thread命令查看JVM的内存和线程情况,也没有发现蛛丝马迹。同时发现其他机器的内存也在涨,从30%多涨到50%。
在线分析找不到原因,接下来就dump堆信息,分析哪些对象大量占着内存。这里用arthas的heapdump /tmp/dump.hprof
命令,把文件dump到tmp目录,再下载到本地。
拿到dump文件后,怎么来分析呢?
用的比较多的内存分析工具有MAT和VisualVM。网上的介绍很多就不展开了。
如果使用MAT,要调整配置文件中的-Xmx大小,参数要大于dump文件大小,不然在加载到一半会报错。
Apache的Groovy是Java平台上设计的面向对象编程语言。
这门动态语言拥有类似Python、Ruby和Smalltalk中的一些特性,可以作为Java平台的脚本语言使用.
简单来说就是动态执行传入的表达式并返回执行结果,我们的项目中用它来做业务监控校验。http或dubbo接口的监控返回内容都不相同,使用groovy动态特性对返回值校验是最合适不过。
groovy
关键字,搜索到相关类和代码。private void dubboMonitor(InterfaceMonitorDTO monitorDTO) {
......
Binding groovyBinding = new Binding();
groovyBinding.setVariable("object", result);
GroovyShell groovyShell = new GroovyShell(groovyBinding);
Boolean evaluate = (Boolean) groovyShell.evaluate(monitorDTO.getCheckScript());
if (!evaluate) {
sendError(monitorDTO, result);
}
......
}
GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。使用Binding对象输入参数给表达式,并最终通过GroovyShell返回Groovy表达式的计算结果。
代码中groovyShell.evaluate
就是计算表达式结果。但是evaluate里到底隐藏有什么逻辑呢?我们来看groovy源码:
public Class parseClass(final GroovyCodeSource codeSource, boolean shouldCacheSource){
String cacheKey = genSourceCacheKey(codeSource);
return ((ConcurrentCommonCache<String, Class>) sourceCache).getAndPut(
cacheKey,
new EvictableCache.ValueProvider<String, Class>() {
public Class provide(String key) {
return doParseClass(codeSource); //调用doParseClass
}
},
shouldCacheSource
);
}
protected String generateScriptName() {
return "Script" + counter.incrementAndGet() + ".groovy";
}
generateScriptName
来产生脚本name,也就是脚本类的名称。有了内容和名称,通过GroovyCodeSource
对象传入到parseClass方法。doParseClass
对传入的codeSource做解析,根据脚本名称和脚本内容转成脚本类。private Class doParseClass(GroovyCodeSource codeSource) {
......
SourceUnit su = null;
File file = codeSource.getFile();
if (file != null) {
su = unit.addSource(file);
else {
URL url = codeSource.getURL();
if (url != null) {
su = unit.addSource(url);
else {
su = unit.addSource(codeSource.getName(), codeSource.getScriptText());
}
}
ClassCollector collector = createCollector(unit, su);
unit.setClassgenCallback(collector);
int goalPhase = Phases.CLASS_GENERATION;
if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT;
unit.compile(goalPhase);
answer = collector.generatedClass;
String mainClass = su.getAST().getMainClassName();
for (Object o : collector.getLoadedClasses()) {
Class clazz = (Class) o;
String clazzName = clazz.getName();
definePackageInternal(clazzName);
//缓存到GroovyClassLoader的clsCache里
if (clazzName.equals(mainClass)) answer = clazz;
}
return answer;
}
createCollector(unit, su)
方法中,每一次会new一个InnerLoader去加载脚本对象。setClassCacheEntry(classz)
方法中,会把脚本类对象缓存到clsCache中。以便运行时可以快速地调度它们。而MetaMethodIndex就是groovy在内存中存储脚本类和方法的映射。InvokerHelper.createScript
方法创建。//创建script
public Script parse(final GroovyCodeSource codeSource) throws CompilationFailedException {
return InvokerHelper.createScript(parseClass(codeSource), context);
}
public static Script newScript(Class<?> scriptClass, Binding context) throws InstantiationException, IllegalAccessException, InvocationTargetException {
Script script;
try {
Constructor constructor = scriptClass.getConstructor(Binding.class);
script = (Script) constructor.newInstance(context);
} catch (NoSuchMethodException e) {
// Fallback for non-standard "Script" classes.
script = (Script) scriptClass.newInstance();
script.setBinding(context);
}
return script;
}
createScript
中会调用newScript
方法。它的入参是scriptClass和binding,在调用constructor.newInstance
方法中,通过反射的方式产生Script对象。然后调用script.run()
完成自定义脚本的执行和结果返回。
该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例。 加载该类的ClassLoader已经被GC。 该类的java.lang.Class 对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法.
setClassCacheEntry
缓存到GroovyClassLoader:classCache
,因为存在引用,所以无法在GC的时候被回收,运行一段时间后内存会被逐渐占满,然后频繁触发FullGC。protected void setClassCacheEntry(Class cls) {
classCache.put(cls.getName(), cls);
}
编写测试用例模拟线上问题:
public void test(){
do{
GroovyShell groovyShell = null;
Binding binding = null;
try{
binding = new Binding();
binding.setVariable("object", "\"result\": {\"test\":1}");
groovyShell = new GroovyShell(binding);
Boolean result = (Boolean) groovyShell.evaluate("return true;");
}catch (Exception e){
log.error("failed!",e);
}finally {
// groovyShell.getClassLoader().clearCache();
}
}while (true);
}
dashboard
观察GC情况: 老年代已经100%,而且频繁FullGC:shell.getClassLoader().clearCache()
,在finally里直接调用。为了提升执行效率,对脚本类做缓存。使用MD5值作为Key,script类作为value缓存起来。下一个请求过来先从缓存中获取,如果没有再通过GroovyClassLoader加载。代码示例:
String key = fingerKey(monitorDTO.getCheckScript());
//加载类信息,放入本地缓存中
Class<Script> script = classMaps.get(key);
if (script == null) {
synchronized (key.intern()) {
// Double Check
script = classMaps.get(key);
if (script == null) {
GroovyClassLoader classLoader = new GroovyClassLoader();
script = classLoader.parseClass(monitorDTO.getCheckScript());
classMaps.put(key, script);
}
}
}