Groovy脚本的OOM源码分析

本文探讨Groovy脚本语言如何与Java结合使用,包括GroovyClassLoader、GroovyShell和GroovyScriptEngine三种方式。重点讲解GroovyClassLoader的内部工作原理,包括类的解析、加载过程以及潜在的内存泄漏问题。同时,提供了解决方案,如使用缓存和clearCache()方法,以避免长时间运行导致的老年代内存溢出。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

   Groovy脚本是应用比较广泛的一种基于JVM的动态语言,Groovy可以补充Java静态方面能力的不足。一般语java结合的时候会有三种方式:GroovyClassLoader、GroovyShell和GroovyScriptEngine。

  这三种方式用起来差不多,GroovyShell底层也是通过GroovyClassLoader实现的,GroovyScriptEngine是侧重多个脚本。

1.GroovyClassLoader

  一般这种方式的用法是,调用parse()生成一个groovyClass,然后再去执行脚本。

Class<?> groovyClass = groovyClassLoader.parseClass(textCode);
// 获得Groovy的实例
GroovyObject groovyTempObject = (GroovyObject)groovyClass.newInstance();
Object object = groovyObject.invokeMethod(methodName, objects);
 public Class parseClass(String text) throws CompilationFailedException {
        return parseClass(text, "script" + System.currentTimeMillis() +
                Math.abs(text.hashCode()) + ".groovy");
    }

public Class parseClass(final String text, final String fileName) throws CompilationFailedException {
        GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
            public GroovyCodeSource run() {
                return new GroovyCodeSource(text, fileName, "/groovy/script");
            }
        });
        gcs.setCachable(false);
        return parseClass(gcs);
    }


 public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {
        return parseClass(codeSource, codeSource.isCachable());
    }

  public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException {
        synchronized (sourceCache) {
            Class answer = sourceCache.get(codeSource.getName());
            if (answer != null) return answer;
            answer = doParseClass(codeSource);
            if (shouldCacheSource) sourceCache.put(codeSource.getName(), answer);
            return answer;
        }
    }

   可以看到parseClass的流程,如果没有传入脚本名称,groovy会生成一个新的脚本名称,这个名称是和时间戳绑定的。

      fileName = System.currentTimeMillis() +Math.abs(text.hashCode()) + ".groovy")

之后会生成一个GroovyCodeSource对象,在doParseClass中去做加载类的动作。所以每次调用parseClass都会生成一个新的class文件。

   

  private Class doParseClass(GroovyCodeSource codeSource) {
        validate(codeSource);
        Class answer; 
        CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource());
        if (recompile!=null && recompile || recompile==null && config.getRecompileGroovySource()) {
            unit.addFirstPhaseOperation(TimestampAdder.INSTANCE, CompilePhase.CLASS_GENERATION.getPhaseNumber());
        }
        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());
            }
        }
        //生成一个InnerLoader加载器
        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();
            definePackage(clazzName);
            //缓存class对象
            setClassCacheEntry(clazz);
            if (clazzName.equals(mainClass)) answer = clazz;
        }
        return answer;
    }

   可以看到在doParseClass方法中,每次都会生成一个InnerLoader加载器,并且会无条件的缓存class对象。

   这里就要提一下groovy的类加载方式了。一般java的双亲委派方式是,Bootstrap <- Extension <-System  <-User ClassLoader,所以在groovy这层,在此基础上会变成Bootstrap <- Extension <-System <- RootLoader <- GroovyClassLoader  <-InnerLoader ,当然如果直接用 GroovyClassLoader的方式,就不会有RootLoader加载器。

    当项目中大量调用groovy脚本的时候,很容易会出现OOM的问题,这种情况大概率是因为InnerLoader和class文件都无法在gc的时候被回收,运行较长一段时间后将老年代占满,一直触发fullgc。

  我们都知道,一个Class只有满足以下三个条件,才能被GC回收:

1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;

2. 加载该类的ClassLoader已经被GC;

3. 该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。

 显然classCache被GroovyClassLoader持有,每个class对象都存在引用,无法被gc清理掉。

解决:

(1)外层加个缓存,每次拿groovyClass的时候先从缓存中拿。

       这样可以在一定程度上解决问题。对于同一个名称的脚本,每次调用缓存中的数据。但是如果时间比较长,不同名称的脚本数量就非常多了,还是会有gc的问题。

(2)clearCache()

      以效率的代价,来解决gc的问题。所以clearCache的时机很重要,不可以太频繁。

例子:


import ch.qos.logback.classic.util.EnvUtil;
import com.cainiao.arrow.arrowcommon.constant.CacheKeysPrefixCenter;
import com.cainiao.arrow.arrowcommon.util.CreateHashAlgorithm;
import com.cainiao.arrow.arrowcommon.util.ExeLogUtil;
import com.cainiao.arrow.arrowcommon.util.LocalCacheUtil;
import com.cainiao.arrow.arrowcommon.util.SpringContextUtil;
import com.sun.javaws.CacheUtil;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class GroovyScriptBaseService {


    private static Map<String, String> keyToHashMap = new ConcurrentHashMap<>();

    @Resource
    private EnvUtil envUtil;

    private static GroovyClassLoader getGroovyClassLoader() {
        CompilerConfiguration config = new CompilerConfiguration();
        config.setSourceEncoding("UTF-8");
        // 设置该GroovyClassLoader的父ClassLoader为当前线程的加载器(默认)
        return new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
    }

    private static String getCacheKeyCode(String textCode) {
        return CacheKeysPrefixCenter.GROOVY_OBJECT_CACHE_PREFIX + CreateHashAlgorithm.toHash(textCode);
    }

    private static void invalidCache(String key, String textCode) {
        String newCacheKeyCode = getCacheKeyCode(textCode);
        if (keyToHashMap.containsKey(key)) {
            String oldCacheKeyCode = keyToHashMap.get(key);
            if (!newCacheKeyCode.equals(oldCacheKeyCode)) {
                LocalCacheUtil.clear(oldCacheKeyCode);
                LocalCacheUtil.clearExpired();
            }
        }
        keyToHashMap.put(key, newCacheKeyCode);
    }



    /**
     * 通过groovy脚本的text,执行的入口
     */
    public Object executeByCode(String textCode, String methodName, Object[] objects) {
        GroovyObject groovyObject = compiledCodeToObject(textCode);
        // 反射调用方法得到返回值
        return groovyObject.invokeMethod(methodName, objects);
    }

    /**
     * 把groovy脚本的文本,转成GroovyObject
     */
    private GroovyObject compiledCodeToObject(String textCode) {
        //LocalCacheUtil来缓存一个groovy文本对应的Groovy的实例
        return LocalCacheUtil.get(getCacheKeyCode(textCode), 60 * 60L, () -> {
            try {
                GroovyClassLoader groovyClassLoader = GroovyScriptBaseService.getGroovyClassLoader();
                groovyClassLoader.clearCache();
                // 替换包名,反正包冲突,在包名前面添加groovy.script.
                String finalTextCode = textCode.replaceFirst("public\\s+class\\s+", "public class Groovy");
                // 加载class类进入内存
                Class<?> groovyClass = groovyClassLoader.parseClass(finalTextCode);
                // 获得Groovy的实例
                GroovyObject groovyTempObject = (GroovyObject)groovyClass.newInstance();
                // 解决依赖注入问题
                this.solveDependInjection(groovyTempObject);
                return groovyTempObject;
            } catch (Throwable e) {
                throw new RuntimeException(e);
            }
        });
    }

    private void solveDependInjection(Object object) {
        Class<?> tempClass = object.getClass();

        List<Field> allFieldList = new ArrayList<>();
        while (tempClass != null && !tempClass.equals(Object.class)) {
            allFieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
            tempClass = tempClass.getSuperclass();
        }

        List<Field> dependFieldList = new ArrayList<>();
        List<String> dependResourceNameList = new ArrayList<>();
        for (Field field : allFieldList) {
            Annotation[] annotations = field.getDeclaredAnnotations();
            if (ArrayUtils.isEmpty(annotations)) {
                continue;
            }
            String resourceName = null;
            for (Annotation annotation : annotations) {
                Class<?> annotationType = annotation.annotationType();
                // 只检查Resource和Autowired两个注解,其他就不管了
                if (annotationType.equals(Resource.class) || annotationType.equals(Autowired.class)) {
                    // 如果是Resource注解,还要判断是否有name属性
                    if (annotationType.equals(Resource.class)) {
                        Resource resource = (Resource)annotation;
                        // name不为空,那么保存对应的name值
                        if (StringUtils.isNotBlank(resource.name())) {
                            resourceName = resource.name();
                        }
                    }
                    dependFieldList.add(field);
                    dependResourceNameList.add(resourceName);
                    break;
                }
            }
        }

        List<String> fieldNameList = new ArrayList<>();
        for (int i = 0; i < dependFieldList.size(); i++) {
            Field field = dependFieldList.get(i);
            String resourceName = dependResourceNameList.get(i);
            fieldNameList.add(field.getName());
            try {
                field.setAccessible(true);
                Object beanObject;
                if (resourceName == null) {
                    beanObject = SpringContextUtil.getBean(field.getType());
                } else {
                    beanObject = SpringContextUtil.getBean(resourceName);
                }
                if (beanObject == null) {
                    throw new RuntimeException(String.format("字段%s通过spring注入失败", field.getName()));
                }
                field.set(object, beanObject);
            } catch (Throwable e) {
                ExeLogUtil.executeAdd(String.format("solveDependInjection: fieldName: %s, error: %s", field.getName(), ""));
            }
        }
        ExeLogUtil.executeAdd(String.format("solveDependInjection: fieldNameList: %s", ""));
    }
   
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值