Skip to main content

Class Metadata Caching

Posted by jhook on December 10, 2006 at 4:38 PM PST

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?