Significant performance regression of code loaded in user.clj in Java 8u202 / 11.0.2
Description
The recent releases of Java 8u201/8u202 and 11.0.2 include a fix for a 0-day vulnerability that could occur while running a class static initializer. The fix included in these releases prevents JVM optimization during the static initializer (before the class is initialized). Doing significant work in the static initializer can see dramatic reduction in performance from the previous versions 8u192 / 11.0.1.
Clojure supports a well-known file user.clj that is loaded during runtime initialization (in the clojure.lang.RT class). This is sometimes used to load tools during development. Currently user.clj is loaded in the scope of RT's static inititializer, so any Clojure code run in user.clj is significantly slower.
One common pattern popularized by Stuart Sierra is the "reloaded" pattern which will load and initialize the entire server from the user.clj. Users of this pattern are likely to see dramatic startup time performance issues with these Java releases.
Move the clojure.core init from doInit() into the RT static initializer, and then force explicit invocation of RT.doInit() in all expected RT entry points:
clojure.main/main - main launcher stub
clojure.java.api.Clojure<clinit> - Java API (this path will still incur the cost via Clojure<clinit>)
clojure.lang.Compile/main - used only in Clojure's own build afaik
clojure.lang.Util/loadWithClass - gen-class/AOT class static initializer
Patch: clj-2484-5.patch
Other alternatives considered:
1) Another option is to take the hints from the scope of the issue at https://cl4es.github.io/2019/02/21/Cljinit-Woes.html and avoid calling static methods on an uninitialized class. The basic idea here is to move all of the RT static methods into an inner class RT.Impl. That class can fully load so all methods are initialized. The RT static init can then just invoke doInit() on it. All references to RT static methods in Clojure must be rerouted from RT to RT.Impl. The existing RT methods are left and forward to RT.Impl - this allowed previously compiled Clojure classes to continue to work (or any stray RT calls made by advanced Clojure code).
Patch: clj-2484-nested-class.patch - this works, but is an enormous patch as it touches all of RT and every call to RT in Clojure.
2) Do nothing and wait for Java to mitigate the perf impact. At this point, it does not look like a simple fix.
Environment
Java 8u201 and Java 11.0.2
Attachments
5
19 Mar 2019, 04:02 PM
15 Mar 2019, 05:44 PM
27 Feb 2019, 09:41 PM
21 Feb 2019, 09:31 PM
21 Feb 2019, 07:02 AM
Activity
Alex Miller
March 19, 2019 at 4:02 PM
Added -5 that keeps RT.doInit() private and re-activates existing RT.init().
Alex Miller
March 15, 2019 at 5:39 PM
New -4 patch uses sneakyThrow to reduce exception handling stuff.
Alex Miller
February 20, 2019 at 7:15 PM
The Java bug http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8219233 is accumulating a lot of additional information if you are looking to follow updates on the Java side. Seems to be narrowing specifically to static initializers that make calls to static methods in same class (this is particularly why user.clj loading in RT.init is impacted - loading all that Clojure code makes tons of self calls into the runtime).
The recent releases of Java 8u201/8u202 and 11.0.2 include a fix for a 0-day vulnerability that could occur while running a class static initializer. The fix included in these releases prevents JVM optimization during the static initializer (before the class is initialized). Doing significant work in the static initializer can see dramatic reduction in performance from the previous versions 8u192 / 11.0.1.
Clojure supports a well-known file user.clj that is loaded during runtime initialization (in the clojure.lang.RT class). This is sometimes used to load tools during development. Currently user.clj is loaded in the scope of RT's static inititializer, so any Clojure code run in user.clj is significantly slower.
One common pattern popularized by Stuart Sierra is the "reloaded" pattern which will load and initialize the entire server from the user.clj. Users of this pattern are likely to see dramatic startup time performance issues with these Java releases.
Links:
https://groups.google.com/d/msg/mechanical-sympathy/lflljWsKw0M/ubROHyvTDAAJ
https://bugs.openjdk.java.net/browse/JDK-8215634
http://thinkrelevance.com/blog/2013/06/04/clojure-workflow-reloaded
http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8219233 - bug filed for this issue
https://cl4es.github.io/2019/02/21/Cljinit-Woes.html
Repro: see https://github.com/puredanger/slow-user-load
Proposed:
Move the clojure.core init from doInit() into the RT static initializer, and then force explicit invocation of RT.doInit() in all expected RT entry points:
clojure.main/main - main launcher stub
clojure.java.api.Clojure<clinit> - Java API (this path will still incur the cost via Clojure<clinit>)
clojure.lang.Compile/main - used only in Clojure's own build afaik
clojure.lang.Util/loadWithClass - gen-class/AOT class static initializer
Patch: clj-2484-5.patch
Other alternatives considered:
1) Another option is to take the hints from the scope of the issue at https://cl4es.github.io/2019/02/21/Cljinit-Woes.html and avoid calling static methods on an uninitialized class. The basic idea here is to move all of the RT static methods into an inner class RT.Impl. That class can fully load so all methods are initialized. The RT static init can then just invoke doInit() on it. All references to RT static methods in Clojure must be rerouted from RT to RT.Impl. The existing RT methods are left and forward to RT.Impl - this allowed previously compiled Clojure classes to continue to work (or any stray RT calls made by advanced Clojure code).
Patch: clj-2484-nested-class.patch - this works, but is an enormous patch as it touches all of RT and every call to RT in Clojure.
2) Do nothing and wait for Java to mitigate the perf impact. At this point, it does not look like a simple fix.