Calcite使用过程中一次OOM定位过程

背景

 项目中需要对用户写的sql进行校验,打算用Apache Calcite实现。实现方式参考了这篇文章 Apache Calcite简介

 这篇文章中注册Table时需要创建一个对应的Class,因为在我们的业务场景中Table是不确定的,所以这个Class需要动态生成,动态生成Class的方式参考了这篇文章:

 https://www.cnblogs.com/benpaodexiaojue/p/6972509.html

 根据这两篇文章,基本的功能都实现了,但在写的过程中就有点担心生成生成的Class能不能正常卸载,如果不能,可能会导入方法区(元空间)OOM,于是特地验证了一下,发现确实有问题。

 

问题一: 

 内存中大量SharedNameTable$NameImpl实例,占用过高。

 测试过程中发现堆内存持续增长,堆dump查看主要为SharedNameTable$NameImpl[]对象

 

 但也不会一直增加,jvisualvm和jmc工具可以看到在某个时间之后开始回收,但回收程度不高,实例数和占用内存仍非常多,且最终趋于平稳

 

 在网上搜索到这篇文章https://blog.csdn.net/j_java1/article/details/82024944,大致是说为了加速编译,Java7引入了SharedNameTable,但存在bug导致不会被释放,据说会在Java9中修复(我的测试环境为Java8)。

 解决办法是添加编译器参数-XDuseUnsharedTable,禁用这一功能。

JavaCompiler.CompilationTask task = compiler.getTask(null, manager, null, Arrays.asList("-XDuseUnsharedTable"), null, Arrays.asList(javaFileObject));

问题二:

 动态生成的Class在注册到Calcite的Schema后无法卸载,导致元空间占用持续增长。 

   

 从类的监视图中看到,也有大量的类被卸载了,因为我不光注册了Table,也注册了UDF,注册UDF时生成的类是可以正常卸载的。

 然而类的总体数理在稳步增长,我这里将元空间大小故意设置到较小-XX:MaxMetaspaceSize=80M,以尽快达到元空间OOM。

 最终,OOM出现时,最初加载的序号为1的类也没有被卸载。

 类要被卸载肯定是要满足以下三个条件:

 1)该类所有实例都被回收;

 2)加载该类的ClassLoader被回收;

 3)该类的Class对象没有被引用;

 上图中可以看到类的实例数为0。

 ClassLoader和Class相互引用,肯定都没有被回收,jvisualvm工具中可以看出ClassLoader的引用都是这几个Class

那么,类没有被卸载的原因肯定是Class对象被其他对象引用了

使用jhat命令可查看到类的引用关系,参考https://www.cnblogs.com/baihuitestsoftware/articles/6406271.html

最终定位到RelDataTypeFactoryImpl类中的静态属性CACHE

它是一个LoadingCache实例,从代码看这里用了软引用以防止OOM,甚至还写的注释“Uses soft values to allow GC.”

 可从实际测试结果看,GC时CACHE中的内容并没有被回收,不知道是不是Calcite的bug,这里并没有深入研究,定位到这里问题就可以解决了。

 这个CACHE虽然是private的,不过还是可以通过反射来调用它的invalidateAll()方法来手动清除缓存。修改后可以看到类的数量和元空间占用都很稳定,问题解决。

 

 

 

 此外,还可以通过另一种方式解决,使用new AbstractTable()的方式,从开始就避免动态生成Class,不过这个API是Deprecated的。

schemaPlus.add(table.getTableName(), new AbstractTable() {
    @Override
    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
        RelDataTypeFactory.FieldInfoBuilder builder = typeFactory.builder();
        table.getColumnMap().entrySet().stream().forEach(entry -> {
            builder.add(entry.getKey(), typeFactory.createSqlType(string2SqlTypeName(entry.getValue())));
        });
        return builder.build();
    }
});

 

posted @ 2019-03-28 20:18  GradyYang  阅读(1155)  评论(0编辑  收藏  举报