Note that there are some explanatory texts on larger screens.

plurals
  1. POCan a Custom Delegating Classloader Cache loadClass() results safely?
    primarykey
    data
    text
    <p>We've got a custom classloader, called here <code>MainClassLoader</code>, sitting on top of a Java web application (specifically on Tomcat 7 where the parent classloader is the <code>WebAppClassLoader</code>). This custom classloader is set as the TCCL for the web application, and its purpose is to delegate the lookup of classpath resources (including classes and non-class resources) to a set of other custom classloaders, each of which represents a pluggable module to the application. (<code>MainClassLoader</code> itself loads nothing.)</p> <p><code>MainClassLoader.loadClass()</code> will do parent-first delegation, and upon a <code>ClassNotFoundException</code>, go one by one through the pluggable child classloaders to see which of them will provide the result. If none of them can, it then throws the <code>ClassNotFoundException</code>.</p> <p>The logic here is a bit more complicated, however, and combining that with the fact that our end users may end up having several (in the 10s) of these child modules plugged in, we're finding that the classloader ends up being one of the more CPU-intensive parts of the application, given how reliant Java is today on reflection-based command pattern implementations. (By that I mean there are a lot of <code>Class.forName()</code> calls to load and instantiate classes at runtime.)</p> <p>We started noticing this first in periodic thread dumps of the application to catch the app "in action" to see what it is doing, plus profiling through JProfiler certain use cases that were known to be slower than desired.</p> <p>I've written a very simple caching approach for <code>MainClassLoader</code> where the results of a <code>loadClass()</code> (including a <code>ClassNotFoundException</code>) call are cached in a concurrent map with weak values (keyed by the String className), and the performance of this class went high enough to totally fall off the hot spots list of JProfiler.</p> <p>However, I'm concerned about whether we can really safely do this. Will such a cache get in the way of intended classloader logic? What are the pitfalls one might expect in doing this?</p> <p>Some obvious ones I anticipate:</p> <p>(1) Memory - obviously this cache consumes memory, and if left unbounded is a possible memory drain. We can address this using a limited cache size (we're using Google's Guava CacheBuilder for this cache).</p> <p>(2) Dynamic classloading, especially in development - So if a new or updated class/resource is added to the classpath after our cache has a stale result, this would confuse the system, probably resulting more often in <code>ClassNotFoundExceptions</code> being thrown when the class now should be loadable. A small TTL on the cached "not found" state elements might help here, but my bigger concern is, during development, what happens when we update a class and it gets hot-swapped into the JVM. This class would most likely be in one of the classloaders that <code>MainClassLoader</code> delegates to, and so its cache could conceivably have a stale (older) version of the class. However, since I'm using Weak values, would this help to mitigate this? My understanding of weak references are they don't go away even when eligible for collection until the GC runs a pass where it decides to reclaim them.</p> <p>These are my two known issues/concerns with this approach, but what scares me is that classloading is a bit of a black art (if not a dark science) that is full of gotchas when you do non-standard things here.</p> <p>So what am I not worried about that I should be worried about?</p> <p><strong>UPDATE/EDIT</strong></p> <p>We ended up opting NOT to do the local caching as I prototyped above (it just seems dangerous and redundant with the caching/optimization done by the JVM), but did some optimization within our loadClass() method. Basically the logic we have in this loadClass() method (see comments below) did not follow a "best case" path through the code when it could have, e.g. when there were no "customization" modules in place, we were still behaving as though there were, letting that classloader throw a ClassNotFoundException and catching it and doing the next checks. This pattern meant that a given class load operation would nearly always go through at least 3 try/catch blocks with a ClassNotFoundException being thrown in each. Quite expensive. Some extra code to determine whether there were any URLs associated with the classloaders being delegated to allowed us to bypass those checks (and the resultant exception throw/catch), giving us an almost 25000% boost in performance for this class.</p> <p>I'd still like comment on my original question, however, to help keep the issue alive to be answered.</p> <p><strong>What are the concerns in doing our own caching in a custom classloader, other than those I already listed?</strong></p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. COi'm a little confused by your implementation description. first of all, you don't typically override `loadClass()`, you usually override `findClass()` (assuming you are maintaining parent-first delegation order). second, the classloader impl should be caching all these lookups for you. third, why are you continuously calling `loadClass()` on the same class names?
      singulars
    2. COSorry for not being complete - I was trying to summarize for the sake of brevity. We have a conditional delegation model here where if certain types of plug-in modules are present, we will delegate to those first BEFORE doing parent delegation. The loadClass() first calls findLoadedClass(), then if the "customization" modules are present, checks them, then does the parent.loadClass(), then any other plug-in modules.
      singulars
    3. COTo answer your third question, WE are not calling loadClass() continually, it's actually a 3rd party library (BIRT) that is doing this. Our microbenchmarking shows that had they called Class.forName() this would have been nearly 1000x faster due to the JVM's caching of classes, but I speculate that perhaps BIRT needed to do some less-efficient class-load operations as a result of running in an OSGI platform.
      singulars
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload