UP | HOME

Once Upon a Class

This is a story about classes. A story that harks back to early days, both historically and metaphorically, and it begins with a mystery.

I’ve been trying some more things, and something fairly unexpected is that with a CIDER/nREPL setup, each evaluation adds an extra classloader 🙈

⸺ Arne, Clojureverse, 2018

An account of this confounding observation was made back in 2012 on the Clojure mailing list. Vladimir started a thread titled class loaders stack constant grow in REPL, and concluded:

I think, the main problem is nobody has ever tried to write an article «Class loading in Clojure». If such article existed, it would make life much easier for many developers.

⸺ Vladimir, Clojure mailing list, Dec 10, 2012

Our exploration of the topic makes heavy use of a REPL, I invite you to fire one up and play along. Let’s start with examining the class loader hierarchy.

(->> (.. java.lang.Thread currentThread getContextClassLoader)
     (iterate #(.getParent %))
     (take-while identity))

You’ll notice Clojure’s DynamicClassLoader, plus the troika of built-in class loaders at the top. If you see only two out of the three triumvirs, don’t fret. The Primordial class loader is responsible for bootstrapping Java’s core classes. It is written in native code, out of reach, represented by nil.

(nil? (.getClassLoader java.lang.Class))
true

Object, String, Long and List are also core classes.

(every? #(nil? (.getClassLoader (class %))) [4 "hello" (java.lang.Object.) (java.util.Collections/EMPTY_LIST)])
true
  

Other classes are loaded by the Platform class loader.

(.getName (.getClassLoader (class (java.sql.Time. 1 1 1))))
"platform"

The classes that you care most about, those that you declare as dependencies, are being loaded by the Application class loader. It is the one that load jars and resources on the class path. Clojure’s classes are loaded by the Application class loader.

(str (.getName (class {})) " was loaded by " (.getName (.getClassLoader (class {}))))
"clojure.lang.PersistentArrayMap was loaded by app"

Finally, at the REPL we are creating new classes all the time. We may not notice it, but we do.

(defn foo [x] (+ x x))
#'user/foo

To us, foo is a function, but to the JVM it is a class.

(class foo)
user$foo

Let’s have a look at its class hierarchy.

(->> (class foo)
  (iterate #(.getSuperclass %))
  (take-while identity))
(user$foo clojure.lang.AFunction clojure.lang.AFn java.lang.Object)

Contrary to the previous examples, foo was not present when the JVM started. It’s a brand new class loaded at runtime. This is an important observation. Indeed, the Java class model is designed in such a way that it need not know ahead of time the classes it is going load and run.

Note: Java was born as Greentalk, a nod to Smalltalk because that was the state of the art in terms of virtual machine and JIT compilation. For a time the system was projected to be running on set-top boxes. The need for runtime class loading was anticipated as classes were going to travel across the wire.

So who is responsible for loading Clojure code on-the-fly?

(class (.getClassLoader (class foo)))
clojure.lang.DynamicClassLoader

When you create foo at the REPL, Clojure’s compiler emits bytecode for consumption by DynamicClassLoader. It will create a new class with the defineClass method before linking it.

Note: Linking is the process of taking a class and combining it into the run-time state of the Java Virtual Machine so that it can be executed.

Once a class loader links a class, it is final. Attempting to link a new definition of the class does nothing. Imagine if your first attempt at writing foo was the last one allowed! To work around this limitation, a new DynamicClassLoader is created for each evaluation. This is the hat trick that Clojure pulls off to ensure that the user is able to override existing classes, not merely creating new ones.

The compiler tracks the current instance of DynamicClassLoader in clojure.lang.Compiler/LOADER, while DynamicClassLoader tracks its classes via a cache. The latter is backed by a reference queue, helping the garbage collector do its job. We can peek into it via the Reflection API.

(defn inspect-cache []
  (let [cache (.getDeclaredField clojure.lang.DynamicClassLoader "classCache")]
    (.setAccessible cache true)
    (.get cache nil)))

This will reveal a mapping between the names of the generated classes and soft references. If you redefine foo at the REPL, the soft reference associated with foo in the cache will be updated.

A new class loader instance is used for every top-level form.

(defn foo [x] (identity x))
(defn bar [y] (identity y))
(= (.getClassLoader (class foo)) (.getClassLoader (class bar)))
| #'user/foo |
| #'user/bar |
| false      |

Compare and contrast.

(let [foo (fn  [x] (identity x))
      bar (fn [y] (identity y))]
  (= (.getClassLoader (class foo)) (.getClassLoader (class bar))))
true

We’ve shown how class loader instances are being repeatedly created at the REPL, and it sounds like an explanation for the mystery we mentioned at the start. It is not. Let’s take a closer look at the observation that has befuddled inquisitive developers since 2012. It is worth reproducing the experiment in a plain Clojure REPL and a nREPL client side by side.

Upon launching a REPL, Clojure sets the context class loader with a class loader of its own. The following is the first line of clojure.main/repl’s source code.

(let [cl (.getContextClassLoader (Thread/currentThread))]
  (.setContextClassLoader (Thread/currentThread) (clojure.lang.DynamicClassLoader. cl)))

This translates to: instead of setting the default Application loader class on the REPL thread, use mine.

The expected behavior is that it is set once, unlike the per-evaluation class loader stored in clojure.lang.Compiler/LOADER.

(hash (.getContextClassLoader (Thread/currentThread)))
1979207367

Now rinse and repeat. In the default REPL, one instance of DynamicClassLoader stays associated with the REPL throughout the session. In a nREPL client, instances of DynamicClassLoader keep piling up.

(count (->> (.. java.lang.Thread currentThread getContextClassLoader)
   (iterate #(.getParent %))
   (take-while #(instance? clojure.lang.DynamicClassLoader % ))))
45

Clojure’s entry point to the REPL is clojure.main/repl. However, due to historical reasons, nREPL runs clojure.main/repl for every evaluation. It isn’t a long running process like Clojure’s native REPL. Since clojure.main/repl starts with setting the context class loader with a new instance of DynamicClassLoader, we end up with an unbounded stack of class loaders.

This is unfortunate, but remember that nREPL is a major community effort to elevate Clojure’s REPL experience. In a native REPL, it is not possible to interrupt an evaluation. nREPL brings that capability. At the cost of the quirk that we’ve described. Colin Jones reported the issue in 2012. Naturally, solutions were envisaged.

Yes, this should be fixed upstream; a new DynamicClassLoader should only be set as the thread-context classloader if one is not already in place…

⸺ Chas Emerick, issue 8, nREPL repository.

A downstream solution, maybe.

I think nREPL will end up having to stop using clojure.main/repl, and maintain a modified version of it itself (something I wanted to avoid exactly so as to benefit from the changes to clojure.main/repl from version to version of Clojure).

⸺ Chas Emerick, NREPL-31, Jira.

Ultimately, pragmatism prevailed.

Some years on, and it’s clear that this is fundamentally a minor problem (insofar as hardly anyone has complained AFAIK)…

⸺ Chas Emerick, issue 8, nREPL repository.

How come nobody is complaining? That’s because there are no side-effects apart from the redundant allocation of objects. At a cost of 112 bytes per instance of DynamicClassLoader, the increased memory usage isn’t immediately noticeable.

In the extended version of this article, I dwell a bit longer on the context class loader and what you can do with it. Hint: loading JARs missing from your dependencies without shutting down your REPL session.

In conclusion, Clojure leverages the built-in capabilities of the JVM to provide a dynamic runtime environment. Redefinition of classes is made possible because each top-level form gets its own instance of DynamicClassLoader. In a nREPL client, an extraneous instance is created, a quirk that should not obscure the fact that it provides a more capable REPL, backed by a long standing community effort.