Reload class in Groovy

落花浮王杯 提交于 2021-01-03 06:47:27

问题


I have a custom ClassLoader extending GroovyClassLoader which compiles the source code to .class files on disk and then loads the resulting 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'

Now if I change the source code, call myClassLoader.removeClassCacheEntry​(name) and try myClassLoader.loadClass() again I'm getting:

java.lang.LinkageError: loader (instance of com/my/MyClassLoader): attempted duplicate class definition for name some/pckg/SomeClass

I read the greater half of the Internet and found a "solution" to initialize a class-loader for each class:

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

This seems to be working but raises performance concerns of mine...

What is the proper way to reload classes? How can I reuse the same class-loader? What am I missing?


回答1:


Actually there is a trick that could be used

Originally, when you call

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

It calls java native method defineClass1 that actually calls loadClass method.

So, possible to intercept this method and process it a bit different then original.

In the folder that contains cached class files I have the following groovy compiled to class: A.class

println "Hello World!"

B.class to check dependent class loading

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

and C.class to check multi-level class loading

i used this jar to compile following class and run the class re-loading example

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

the following class + code loads and reloads the same class:

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

result:

---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: not sure priviledged access required




回答2:


JVM does not allow to just unload some class, the only way to unload a class is to GC it. And class can be GC just like every other object -> all reachable references must be removed and GC run.
The tricky part is... class loader hold references to all classes. So the only way to unload a class is to get rid of both class and class loader.

You can find more information in language specification: https://docs.oracle.com/javase/specs/jvms/se13/jvms13.pdf 12.7 Unloading of Classes and Interfaces

An implementation of the Java programming language may unload classes. A class or interface may be unloaded if and only if its defining class loader may be reclaimed by the garbage collector as discussed in §12.6. Classes and interfaces loaded by the bootstrap loader may not be unloaded.

And class unloading does not need to be implemented at all in some JVM implementations:

Class unloading is an optimization that helps reduce memory use. [...] system chooses to implement an optimization such as class unloading. [...] Consequently, whether a class or interface has been unloaded or not should be transparent to a program.

There is also explanation why class loader can't be reachable to unload class, as class might contain static variables and blocks of code that would be reset and executed again if this same class would be later loaded again. It's quite long and already a bit off topic so I will not paste it here.

So each your script should just use own class loader as that's the only way to actually not waste memory, so class can be GC later. Just make sure that you don't use any libraries that might cache references to your class - like many serialization/ORM libraries might do this for data types, or some other reflection libraries.
Another solution would be to use different scripting language that does not create java classes and just execute some kind of AST structure.

There is also one more solution to this problem, but it is very tricky and it's not something you should use on production, it even requires you to provide special JVM arguments or JVM from JDK that contains all needed modules. As java supports instrumentation API that can allow you to change bytecode of class at runtime, but if class is already loaded you can only change bytecode of methods, you can't add/remove/edit method/field/class signatures. So it could be very bad idea to use it for such scripts, so I will stop here.



来源:https://stackoverflow.com/questions/58373661/reload-class-in-groovy

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!