The Source for Java Technology Collaboration
User: Password:



Jacob Hookom

Jacob Hookom's Blog

Class Metadata Caching

Posted by jhook on December 10, 2006 at 04:38 PM | Comments (11)

A lot of frameworks are building off of simple Annotations or dynamic invocation, basically extensions per Class instance. To avoid re-mining this data, we attempt to cache it. This works fine, except when we deal with hot deploys or restarts with statically scoped caches. Keeping reference to some classes/ClassLoaders after repeated restarts could quickly gobble resources.

So I've been seriously looking for strategies that would prevent memory leaks, detecting dynamic changes in ClassLoaders or Classes. Some solutions go as far as to check the file system for class file changes, seems like a quite of overhead.

From talking with some of the guys from Sun, looking at ClassLoaders/Class implementations within the JVM, I thought looking at using WeakHashMaps and ConcurrentHashMaps together might provide a solution.

First, we would have a top-level WeakHashMap of ClassLoaders as the key. Secondly, we'll make the value of this ClassLoader WeakHashMap be a child Map of Class names to metadata (finally).

WeakHashMap<ClassLoader, ConcurrentHashMap<String, ClassMetaData>>

The reason for this setup is:

  1. WeakHashMaps keep weak references to the key, not the value-- so we can prevent retaining reference to old ClassLoaders.
  2. Using the ClassLoader at the top level key is less volatile than per Class, can help avoid synchronization locks with WeakHashMaps.
  3. We use a ConcurrentHashMap for efficient synchronization, for the per ClassLoader information.
  4. We key off of the Class name instead of the Class instance based on the fact that per ClassLoader can only have one instance loaded, but we know that the class could possibly change.
  5. The final value stored is wrapped by a touple that retains reference to the target class so we can check that the one instance cached matches the class passed, even though they have the same name.

Based on these points, I started throwing together a generic cache implementation that can be used to store anything per class, while attemping to avoid long term memory leaks.

public class ClassCache<V> {
    
    private class ClassValue<V> {
        private final Class type;
        private V value;
        
        public ClassValue(Class type, V value) {
            this.type = type;
            this.value = value;
        }
        
        public boolean matches(Class type) {
            return this.type.equals(type);
        }
        
        public V get() {
            return this.value;
        }
    }
    
    private class ClassMap<V> extends ConcurrentHashMap<String, ClassValue<V>> {}
    
    private class ClassLoaderMap extends WeakHashMap<ClassLoader, ClassMap> {}
    
    private ClassLoaderMap classLoaders = new ClassLoaderMap();
    
    public ClassCache() {}
    
    public void set(Class type, V value) {
        if (type == null) return;
        ClassLoader loader = type.getClassLoader();
        if (loader == null) loader = Thread.currentThread().getContextClassLoader();
        
        ClassMap map = this.classLoaders.get(loader);
        if (map == null) {
            map = new ClassMap();
            ClassLoaderMap replace = new ClassLoaderMap();
            
            // copy on write
            replace.putAll(this.classLoaders);
            replace.put(loader, map);
            this.classLoaders = replace;
        }
        
        map.put(type.getName(), new ClassValue(type, value));
    }
    
    public V get(Class type) {
        if (type == null) return null;
        ClassLoader loader = type.getClassLoader();
        if (loader == null) loader = Thread.currentThread().getContextClassLoader();
        
        ClassMap<V> map = this.classLoaders.get(loader);
        if (map != null) {
            ClassValue<V> value = map.get(type.getName());
            if (value != null && value.matches(type)) {
                return value.get();
            } else {
                map.remove(type.getName());
            }
        }
        
        return null;
    }
}

This is just some ideas I've put together, does anyone else have specific suggestions on how to do this better/properly?


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • I had a blog entry last year about dangers of caching class information, illustrating it with custom class loaders and XStream XML-Java framework. It's not just retained resources, since it can lead to incorrect results and unexpected exceptions as well.

    Posted by: kirillcool on December 10, 2006 at 07:10 PM

  • I've been down this road a few times: http://crazybob.org/2006/12/caching-class-related-information.html

    Posted by: crazybob on December 11, 2006 at 12:39 AM

  • Here's a link for your convenience: http://crazybob.org/2006/12/caching-class-related-information.html

    Posted by: crazybob on December 11, 2006 at 12:39 AM

  • I guess I was assuming that Class didn't retain a direct reference to it's ClassLoader, allowing the whole ClassLoader of meta information to be cleaned up at an appropriate time. I was hoping to refrain from implementing/combining a reference map myself, but maybe this is the only route to do it properly?

    Posted by: jhook on December 11, 2006 at 06:04 AM

  • At the very least, you need to make references to the class artifacts soft. For reflection objects, these soft reference clearings can cause unnecessary GC in the permanent generation - not good. Also the ClassLoader itself become softly reachable, although that isn't a problem. Recent Sun implementations of java.io.ObjectStreamClass use a weak-soft map.

    If you can inject a class into the class loader, you can create references that prevent the soft references from clearing.

    This ClassLoader reachability is the common cause of the infamous ThreadLocal leak, and also causes leaks in java.sql.DriverManager and two in java.beans.

    So vote Bug ID 6493635: Provide ClassLoader-local variables (or the more general but more difficult and possibly slower Bug ID 4630118: (ref) Add joined weak references / weak properties).

    Posted by: tackline on December 11, 2006 at 06:59 AM

  • Can you elaborate on why it would be required to make class references soft instead of strong? I've read the javadocs on the subject, it's just not fully clear to me :-) Is it to allow GC of the artifacts or to prevent strong associations back the weakly referenced key?

    Posted by: jhook on December 11, 2006 at 09:18 AM

  • I guess what I'm gunning for is a way to prevent GC'ing of metadata unless the classloader is cleaned up

    Posted by: jhook on December 11, 2006 at 09:25 AM

  • Jacob, you can keep a WeakReference to the Class in ClassValue and then do the same for additional Class references in your actual value.

    Posted by: crazybob on December 11, 2006 at 09:39 AM

  • The soft reference is necessary to prevent strongly referencing the keys.

    Your class metadata strongly references, say, a Method. Method will strongly reference the Class (Method.getDeclaringClass). The Class will strongly reference the ClassLoader that loaded it (Class.getClassLoader). The ClassLoader will strongly reference all the Classes it has loaded (the class loader must give the same Class every time it is requested by name). Therefore, you are caught with the metadata indirectly strongly referencing the key.

    If you want your map to hold a weak/soft reference to your metadata, but not have that reference cleared while the class is still loaded, then you need to inject data into that ClassLoader. If you can extend the ClassLoader, then it's a piece of cake - just add the map to the class loader. If you cannot extend the ClassLoader, then, if you can, inject a class to hold a static variable set of all the strong references you need to keep alive for your weak references. I used a similar technique to call java.sql.DriverManager.getDrivers with a different caller class loader (jroller is a little slow at the moment).

    Posted by: tackline on December 11, 2006 at 10:00 AM

  • Thanks everyone for your help!!

    Posted by: jhook on December 11, 2006 at 11:08 AM

  • If you want your map to hold a weak/soft reference to your metadata, but not have that reference cleared while the class is still loaded, then you need to inject data into that ClassLoader. If you can extend the ClassLoader, then it's a piece of cake - just add the map to the class loader. If you cannot extend the ClassLoader, then, if you can, inject a class to hold a static variable set of all the strong references you need to keep alive for your weak references. I used a similar technique to call java.sql.DriverManager.getDrivers with a different caller class loader (jroller is a little slow at the moment).

    Posted by: castilmei on March 02, 2007 at 03:29 AM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds