关于classloader:Clojure类的重新加载工作方式如何?

How does clojure class reloading work?

我一直在阅读代码和文档,以尝试了解Clojure中类重载的工作方式。根据许多网站的介绍,例如http://tutorials.jenkov.com/java-reflection/dynamic-class-loading-reloading.html,每当您加载类时,基本上就可以获取字节码(通过任何数据机制),将字节码转换为字节码插入类Class的实例(通过defineClass),然后通过resolveClass解析(链接)该类。 (defineClass是否隐式调用resolveClass?)。任何给定的类加载器仅允许一次链接一个类。如果尝试链接现有的类,则不执行任何操作。由于无法链接新实例化的类,因此会产生问题,因此,每次重新加载类时,都必须创建一个新的类加载器实例。

回到clojure,我尝试检查加载类的路径。

在clojure中,您可以根据需要以多种方式定义新类:

匿名类:
整顿
代理

命名类:
deftype
defrecord(在后台使用deftype)
gen-class

最终,这些代码指向clojure / src / jvm / clojure / lang / DynamicClassLoader.java

其中DynamicClassLoader / defineClass使用super的defineClass创建一个实例,然后缓存该实例。当您要检索类时,请通过调用forName调用clojure加载,该调用将调用classloader和DynamicClassLoader / findClass,后者先在高速缓存中查找,然后委派给超类(这与大多数普通类加载器的工作方式相反)混淆的重要点在于:记录了forName在返回类之前链接该类,但这意味着您无法从现有DynamicClassLoader重新加载类,而需要创建一个新的DynamicClassLoader。 ,但是我在代码中看不到这一点。我知道代理和验证定义了匿名类,因此它们的名称不同,因此可以将其视为不同的类。但是,对于命名的类,这会崩溃。在实际的Clojure代码中,您可以同时具有对类的旧版本的引用和对类的新版本的引用,但是尝试创建新的类实例将是新版本。

请解释一下clojure如何能够在不创建DynamicClassLoader新实例的情况下重新加载类,如果我能理解重新加载类的机制,我想将此重新加载功能扩展到我可能使用javac创建的Java .class文件中。

笔记:
这个问题涉及到类RELOADING,而不仅仅是动态加载。重新加载意味着我已经实习了一个类,但想实习该实例的新更新版本。

我想重申一下,尚不清楚clojure如何能够重新加载deftype定义的类。调用deftype最终会导致调用clojure.lang.DynamicClassLoader / defineClass。再次执行此操作将导致另一个对defineClass的调用,但是手动执行此操作会导致链接错误。这下面发生了什么事情,使clojure可以使用deftypes做到这一点?


并非所有这些语言功能都使用相同的技术。

代理

proxy宏专门基于继承的接口的类和列表来生成类名称。此类中每个方法的实现都委托给存储在对象实例中的Clojure fn。这使得Clojure每次继承相同的接口列表时都可以使用完全相同的代理类,而不管宏的主体是否相同。没有实际的类重装发生。

整顿

对于reify,方法主体直接编译到类中,因此proxy使用的技巧不起作用。相反,在编译表单时会生成一个新类,因此,如果更改表单主体并重新加载它,则会得到一个全新的类(具有新生成的名称)。同样,没有实际的类重装发生。

gen-class

使用gen-class,您可以为生成的类指定一个名称,因此,proxyreify所使用的技术均无效。 gen-class宏仅包含类的一种规范,但没有方法主体。生成的类(类似于proxy)遵循方法主体的Clojure函数。但是由于名称与规范相关,所以与proxy不同,它不能更改gen-class的主体并重新加载,因此gen-class仅在提前编译(AOT编译)和如果不重新启动JVM,则不允许重新加载。

deftype和defrecord

这是真正的动态类重新加载的地方。我对JVM的内部知识并不十分熟悉,但是调试器和REPL的一些工作可以清楚地说明一点:每次需要解析类名时,例如在编译使用该类的代码时或调用类class的forName方法,使用Clojure的DynamicClassLoader/findClass方法。如您所见,这将在DynamicClassLoader的缓存中查找类名,并且可以通过再次运行deftype将其设置为指向新类。

请注意,您在本教程中提到的关于重载类是另一个类的警告,尽管名称相同,但仍适用于Clojure类:

1
2
3
4
5
(deftype T [a b])  ; define an original class named T
(def x (T. 1 2))   ; create an instance of the original class
(deftype T [a b])  ; load a new class by the same name
(cast T x)         ; cast the old instance to the new class -- fails
; ClassCastException   java.lang.Class.cast (Class.java:2990)

Clojure程序中的每个顶级窗体都会获得一个新的DynamicClassLoader,该动态ClassLoader可用于该窗体中定义的任何新类。这不仅包括通过deftypedefrecord定义的类,还包括reifyfn的类。这意味着上述x的类加载器与新的T不同。注意@之后的数字不同-每个都有自己的类加载器:

1
2
3
4
5
(.getClassLoader (class x))
;=> #<DynamicClassLoader clojure.lang.DynamicClassLoader@337b4703>

(.getClassLoader (class (T. 3 4)))
;=> #<DynamicClassLoader clojure.lang.DynamicClassLoader@451c0d60>

但是,只要不定义新的T类,新实例将具有相同的类并具有相同的类加载器。请注意,@之后的数字与上面的第二个数字相同:

1
2
(.getClassLoader (class (T. 4 5)))
;=> #<DynamicClassLoader clojure.lang.DynamicClassLoader@451c0d60>