【问题标题】:Reload class in Groovy在 Groovy 中重新加载类
【发布时间】:2020-02-10 21:27:59
【问题描述】:

我有一个扩展 GroovyClassLoader 的自定义 ClassLoader,它将源代码编译为磁盘上的 .class 文件,然后加载生成的类:

class MyClassLoader extends GroovyClassLoader {

  File cache = new File( './cache' )
  Compiler compiler

  MyClassLoader() {
    CompilerConfiguration cc = new CompilerConfiguration( targetDirectory:cache )
    compiler = new Compiler( cc )
    addClasspath cache.path
  }

  @Override
  Class findClass( name ) {
    try{
      parent.findClass name
    }catch( ClassNotFoundException e ){
      compiler.compile name, getBodySomehow()
      byte[] blob = loadFromFileSystem name
      Class c = defineClass name, blob, 0, blob.length
      setClassCacheEntry c
      c
    }
  }

  @Override
  void removeClassCacheEntry​(String name) {
    Class c = cache[ name ]
    super.removeClassCacheEntry​(name)
    GroovySystem.metaClassRegistry.removeMetaClass c
    deleteFiles name
  }
}

Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'

现在,如果我更改源代码,请致电myClassLoader.removeClassCacheEntry​(name) 并再次尝试myClassLoader.loadClass(),我得到:

java.lang.LinkageError: loader (com/my/MyClassLoader 的实例): 尝试为名称 some/pckg/SomeClass 重复类定义

我阅读了互联网的大半部分,找到了一个“解决方案”来为每个类初始化一个类加载器:

MyClassLoader myClassLoader = new MyClassLoader()
Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'

这似乎有效,但引起了我的性能问题......

重新加载类的正确方法是什么?如何重用相同的类加载器?我错过了什么?

【问题讨论】:

  • 我认为removeClassCacheEntry​删除了作为groovy加载的类,findClass使用java类加载器加载.class的问题
  • @daggett 需要classCache​ 以避免一遍又一遍地重新编译相同的源代码。从技术上讲,我可以使用相同的源代码多次调用gcl.parseClass(),但问题仍然存在:旧类没有从 JVM 中显式删除
  • 那么不要使用java本地方式从缓存(类路径等)加载.class文件。你必须加载它们,因为它是 groovy 加载的,然后 groovy lethod removeClassCacheEntry​ 应该可以工作
  • @daggett 我很确定gcl.parseClass() 在每次运行时都会实例化一个新的内部加载器。如果我多次运行println gcl.parseClass( body ).classLoader,它会打印出不同的哈希码
  • 我现在倾向于认为我的代码是完美的,或者换句话说,它是我能摆脱最糟糕的最好的代码

标签: java class groovy


【解决方案1】:

其实有一个技巧可以使用

原来,当你调用

classLoader.defineClass(className, classBytes, 0, classBytes.length)

它调用java本地方法defineClass1,实际上调用loadClass方法。

所以,可以截取这个方法,处理起来和原来的有点不同。

在包含缓存类文件的文件夹中,我将以下 groovy 编译为类:A.class

println "Hello World!"

B.class 检查依赖类加载

class B extends A {
    def run(){
        super.run()
        println "Hello from ${this.getClass()}!"
    }
}

C.class 检查多级类加载

我使用这个jar 来编译下面的类并运行类重新加载示例

class C extends org.apache.commons.lang3.RandomUtils {
    def rnd(){ nextInt() }
}

以下类+代码加载并重新加载同一个类:

import java.security.PrivilegedAction;
import java.security.AccessController;
import org.codehaus.groovy.control.CompilationFailedException;

@groovy.transform.CompileStatic
class CacheClassLoader extends GroovyClassLoader{
    private File cacheDir = new File('/11/tmp/a__cache')

    private CacheClassLoader(){throw new RuntimeException("default constructor not allowed")}

    public CacheClassLoader(ClassLoader parent){
        super(parent)
    }
    public CacheClassLoader(Script parent){
        this(parent.getClass().getClassLoader())
    }

    @Override
    protected Class getClassCacheEntry(String name) {
        Class clazz = super.getClassCacheEntry(name)
        if( clazz ){
            println "getClassCacheEntry $name -> got from memory cache"
            return clazz
        }
        def cacheFile = new File(cacheDir, name.tr('.','/')+'.class')
        if( cacheFile.exists() ){
            println "getClassCacheEntry $name -> cache file exists, try to load it"
            //clazz = getPrivelegedLoader().defineClass(name, cacheFile.bytes)
            clazz = getPrivelegedLoader().defineClass(name, cacheFile.bytes)
            super.setClassCacheEntry(clazz)
        }
        return clazz
    }

    private PrivelegedLoader getPrivelegedLoader(){
        PrivelegedLoader loader = AccessController.doPrivileged(new PrivilegedAction<PrivelegedLoader>() {
            public PrivelegedLoader run() {
                return new PrivelegedLoader();
            }
        });
    }
    public class PrivelegedLoader extends CacheClassLoader {
        private final CacheClassLoader delegate

        public PrivelegedLoader(){ 
            super(CacheClassLoader.this)
            this.delegate = CacheClassLoader.this
        }

        public Class loadClass(String name, boolean lookupScriptFiles, boolean preferClassOverScript, boolean resolve) throws ClassNotFoundException, CompilationFailedException {
            Class c = findLoadedClass(name);
            if (c != null) return c;
            return delegate.loadClass(name, lookupScriptFiles, preferClassOverScript, resolve);
        }
    }
}

def c=null
//just to show intermediate class loaders could load some classes that will be used in CacheClassLoader
def cl_0 = new GroovyClassLoader(this.getClass().getClassLoader())
cl_0.addClasspath('/11/tmp/a__cache/commons-lang3-3.5.jar')
//create cache class loader
def cl = new CacheClassLoader(cl_0)

println "---1---"
c = cl.loadClass('A')
c.newInstance().run()

println "---2---"
c = cl.loadClass('A')
c.newInstance().run()

println "---3---"
cl.removeClassCacheEntry('A')
c = cl.loadClass('A')
c.newInstance().run()

println "---4---"
c = cl.loadClass('B')
c.newInstance().run()

println "---5---"
cl.removeClassCacheEntry('A')
cl.removeClassCacheEntry('B')
c = cl.loadClass('B')
c.newInstance().run()

println "---6---"
c = cl.loadClass('C')
println c.newInstance().rnd()

结果:

---1---
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
---2---
getClassCacheEntry A -> got from memory cache
Hello World!
---3---
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
---4---
getClassCacheEntry B -> cache file exists, try to load it
getClassCacheEntry A -> got from memory cache
Hello World!
Hello from class B!
---5---
getClassCacheEntry B -> cache file exists, try to load it
getClassCacheEntry A -> cache file exists, try to load it
Hello World!
Hello from class B!
---6---
getClassCacheEntry C -> cache file exists, try to load it
226399895

PS:不确定是否需要特权访问

【讨论】:

  • 这个 cacheDir 是什么,什么时候保存的?如果我只是复制该代码,则根本不会使用它
  • 它会改变什么?最后,无论如何每个类都会有单独的类加载器
  • 这是一个存放已编译类的目录。可能是此类通过覆盖createCollector 来拦截常规解析。或调用 groovyc 的并行进程。无论如何,这不是这个问题的主题 :-) 我可以通过修改响应来展示如何做到这一点。
  • 就在defineClass 调用GroovyClassLoader.ClassCollector.createClass 之前,可以将其存储到文件缓存中。 github.com/groovy/groovy-core/blob/master/src/main/groovy/lang/…
  • @daggett 如果该类扩展了另一个类,我会在加载过程中收到关于超类的NoClassDefFoundError,但编译工作得很好。我该如何解决?
【解决方案2】:

JVM 不允许只卸载某个类,卸载一个类的唯一方法是对它进行 GC。并且类可以像其他所有对象一样被 GC -> 必须删除所有可访问的引用并运行 GC。
棘手的部分是......类加载器保存对所有类的引用。所以卸载一个类的唯一方法就是去掉类和类加载器。

您可以在语言规范中找到更多信息:https://docs.oracle.com/javase/specs/jvms/se13/jvms13.pdf 12.7 类和接口的卸载

Java 编程语言的实现可以卸载类。 一个类或接口可以被卸载当且仅当它的定义类 loader 可以由垃圾收集器回收,如 §12.6。引导加载程序加载的类和接口可能不会 被卸载。

并且在一些JVM实现中根本不需要实现类卸载:

类卸载是一种有助于减少内存使用的优化。 [...] 系统选择实施优化,例如类 卸货。 [...]因此,是否已卸载类或接口 或不应该对程序透明。

还解释了为什么无法访问类加载器来卸载类,因为类可能包含静态变量和代码块,如果稍后再次加载相同的类,它们将被重置并再次执行。它很长,已经有点离题了,所以我不会在这里粘贴它。

所以你的每个脚本都应该使用自己的类加载器,因为这是真正不浪费内存的唯一方法,所以类可以稍后进行 GC。只要确保您不使用任何可能缓存对您的类的引用的库 - 就像许多序列化/ORM 库可能会为数据类型或其他一些反射库执行此操作一样。
另一种解决方案是使用不同的脚本语言,它不创建 java 类,只执行某种 AST 结构。

这个问题还有另一种解决方案,但它非常棘手,不应该在生产中使用,它甚至需要你提供特殊的 JVM 参数或来自 JDK 的 JVM,其中包含所有需要的模块。由于 java 支持检测 API,可以让您在运行时更改类的字节码,但如果类已经加载,您只能更改方法的字节码,您不能添加/删除/编辑方法/字段/类签名。因此,将它用于此类脚本可能是一个非常糟糕的主意,所以我将在这里停止。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2014-03-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-01
    • 2011-11-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多