【问题标题】:How does clojure class reloading work?clojure 类重新加载是如何工作的?
【发布时间】:2011-09-19 12:58:01
【问题描述】:

我一直在阅读代码和文档,试图了解类重载在 clojure 中的工作原理。根据许多网站,例如http://tutorials.jenkov.com/java-reflection/dynamic-class-loading-reloading.html,无论何时加载一个类,本质上都是获取字节码(通过任何数据机制),将字节码转换为类的实例(通过defineClass),然后解析(链接)该类通过解析类。 (defineClass 是否隐式调用 resolveClass?)。任何给定的类加载器只允许链接一个类一次。如果它试图链接一个现有的类,它什么也不做。这会产生一个问题,因为您无法链接新实例化的类,因此每次重新加载类时都必须创建类加载器的新实例。

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

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

匿名类: 物化 代理

命名类: 定义类型 defrecord(在后台使用 deftype) 创类

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

其中 DynamicClassLoader/defineClass 使用 super 的 defineClass 创建一个实例,然后缓存该实例。当你想检索类时,clojure 调用 forName 来调用类加载器和 DynamicClassLoader/findClass,它首先在缓存中查找,然后再委托给超类(这与大多数普通类加载器的工作方式相反,它们在首先委托,而不是自己尝试。)混淆的重点如下:forName 被记录为在类返回之前链接类,但这意味着您不能从现有的 DynamicClassLoader 重新加载类,而是需要创建一个新的 DynamicClassLoader,但是我在代码中没有看到这一点。 我知道代理和 reify 定义了匿名类,因此它们的名称不同,因此可以将其视为不同的类。但是,对于命名的类,这会发生故障。在实际的 clojure 代码中,您可以同时引用旧版本的类和引用新版本的类,但尝试创建新的类实例将是新版本的。

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

注意事项: 这个问题是指类 RELOADING,而不是简单的动态加载。重新加载意味着我已经实习了一个类,但想实习该实例的新更新版本。

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

【问题讨论】:

    标签: clojure classloader


    【解决方案1】:

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

    代理

    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 做一点工作就清楚地表明了一点:每次需要解析类名时,例如在编译使用该类的代码时或当类类的forName方法被调用,Clojure的DynamicClassLoader/findClass方法被使用。正如您所注意到的,这会在 DynamicClassLoader 的缓存中查找类名,并且可以通过再次运行 deftype 将其设置为指向新类。

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

    (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,用于该表单中定义的任何新类。这不仅包括通过deftypedefrecord 定义的类,还包括reifyfn。这意味着上面x 的类加载器与新的T 不同。注意@s 后面的数字是不同的——每个都有自己的类加载器:

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

    但是只要我们不定义一个新的T 类,新的实例就会有同一个类和同一个类加载器。注意这里@后面的数字和上面第二个是一样的:

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

    【讨论】:

    • 您好 Chouser,感谢您详细介绍代理、reify 和 gen-class 的机制。不过感觉deftype的解释还是不完整的。我做了一个简单的测试,我创建了一个简单的 helloworld.java 文件,手动读取 helloworld.class 文件并自己调用 DynamicClassLoader/defineClass。我能够使用该类没问题,但是当我再次重新编译 helloworld.java,手动重新加载它并调用 defineClass 时,我得到了一个 LinkageError 加载器......据我了解,deftype 正在做同样的事情,但有些如何不能得到那个错误。你能解释一下这个结果吗?
    • 我同意更多细节对 deftype 有帮助。我将添加我在 Google 小组中的观察结果。
    • 我认为不再为每个顶级表单创建一个新的 DynamicClassLoader 是正确的。 (我尝试在 1.5.1 REPL 中运行您上面给出的表达式。)
    猜你喜欢
    • 1970-01-01
    • 2016-09-03
    • 1970-01-01
    • 2011-02-12
    • 2012-02-28
    • 1970-01-01
    • 2011-12-01
    • 2012-05-22
    • 1970-01-01
    相关资源
    最近更新 更多