Java 在Groovy中重新加载类

Java 在Groovy中重新加载类,java,class,groovy,Java,Class,Groovy,我有一个扩展GroovyClassLoader的自定义类加载器,它将源代码编译到磁盘上的.class文件,然后加载生成的类: class MyClassLoader extends GroovyClassLoader { File cache = new File( './cache' ) Compiler compiler MyClassLoader() { CompilerConfiguration cc = new CompilerConfiguration( ta

我有一个扩展
GroovyClassLoader
的自定义类加载器,它将源代码编译到磁盘上的
.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.removeclasschecacheentry​(名称)
并再次尝试
myClassLoader.loadClass()
,我得到:

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

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

MyClassLoader myClassLoader = new MyClassLoader()
Class clazz = myClassLoader.loadClass 'some.pckg.SomeClass'
这似乎有效,但引起了我对性能的担忧


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

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

您可以在语言规范中找到更多信息:12.7类和接口的卸载

Java编程语言的实现可以卸载类。 当且仅当类或接口的定义类 如中所述,垃圾收集器可以回收装载器 §12.6. 引导加载程序加载的类和接口可能不可用 卸货

在某些JVM实现中根本不需要实现类卸载:

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

还有一种解释是,类加载器无法卸载类,因为类可能包含静态变量和代码块,如果以后再次加载同一个类,这些变量和代码块将被重置并再次执行。它相当长,已经有点离题了,所以我不会把它贴在这里

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


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

实际上有一个技巧可以使用

原来你打电话的时候

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
检查多级类加载

class B extends A {
    def run(){
        super.run()
        println "Hello from ${this.getClass()}!"
    }
}
我用它来编译下面的类并运行类重新加载示例

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()

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

我认为问题在于
删除了ClassCacheEntry​
删除作为groovy加载的类,
findClass
使用java类加载器加载
.class
@daggett
类缓存​以避免重复编译相同的源代码。从技术上讲,我可以使用相同的源代码多次调用
gcl.parseClass()
,但问题仍然存在:旧类没有从JVM中显式删除,因此不使用java本机方式从缓存(类路径等)加载
.class
文件。您必须按照groovy加载的方式加载它们,然后groovy lethod
removeClassCacheEntry​应该work@daggett我非常确定,
gcl.parseClass()
在每次运行时都会实例化一个新的内部加载程序。如果我多次运行
println gcl.parseClass(body).classLoader,它会打印不同的哈希代码,我现在倾向于认为我的代码是完美的,或者换句话说,这是我能从世界中得到的最好的东西。这个cacheDir是什么?什么时候保存了什么?如果我只是复制那个代码,它根本不用,它会改变什么?最后,每个类都有单独的类加载器,这是一个目录,其中存储编译后的类。这个类可以通过覆盖
createCollector
来拦截groovy解析。或者调用groovyc的并行进程。在任何情况下,这都不是这个问题的主题:-)我可以通过修改response来演示如何做到这一点。就在调用
GroovyClassLoader.ClassCollector.createClass
之前,可以将其存储到文件缓存中@daggett如果该类扩展了另一个类,我会在加载过程中得到关于超类的
NoClassDefFoundError
,但编译工作正常。我怎样才能修好它?