I managed to make a Groovy version where garbage collection of ClassInfo happens before the limit in Metaspace or Heap is reached (!) - so far it is just a hack, but maybe it can contribute to a solution...

First some "basic research" on when the Java VM can garbage collect a class, performed with a slightly modified ClassGCTester and the simple "JavaFilling" class from previous tests. Again Oracle JDK 8 (Mac).

--

1) Original test setup (class reference as key, constant String "" as value):

private static WeakHashMap<Class<?>, String> weakFillingClassesMap = new WeakHashMap<Class<?>, String>();
...
weakFillingClassesMap.put(clazz, "");

=> Can immediately be garbage collected (i.e. before limit on Metaspace or Heap is reached), as expected, of course

--

2) Value in the WeakHashMap is a Wrapper with a hard reference to the class:

private static WeakHashMap<Class<?>, Wrapper> weakFillingClassesMap = new WeakHashMap<Class<?>, Wrapper>();
private static class Wrapper { public Class<?> clazz; }
...
Wrapper wrapper = new Wrapper();
wrapper.clazz = clazz;
weakFillingClassesMap.put(clazz, wrapper);

=> Cannot be garbage collected, OutOfMemoryError once limit on Metaspace or Heap is reached

--

3) Value in the WeakHashMap is a Wrapper with a WeakReference to the class:

private static WeakHashMap<Class<?>, Wrapper> weakFillingClassesMap = new WeakHashMap<Class<?>, Wrapper>();
private static class Wrapper { public WeakReference<Class<?>> clazz; }
...
Wrapper wrapper = new Wrapper();
wrapper.clazz = new WeakReference<Class<?>>(clazz);
weakFillingClassesMap.put(clazz, wrapper);

=> Can immediately be garbage collected (i.e. before limit on Metaspace or Heap is reached)

--

4) Value in the WeakHashMap is a WeakReference<Wrapper> with a hard reference to the class in the Wrapper:

private static WeakHashMap<Class<?>, WeakReference<Wrapper>> weakFillingClassesMap = new WeakHashMap<Class<?>, WeakReference<Wrapper>>();
private static class Wrapper { public Class<?> clazz; }
...
Wrapper wrapper = new Wrapper();
wrapper.clazz = clazz;
weakFillingClassesMap.put(clazz, new WeakReference<Wrapper>(wrapper));

=> Can immediately be garbage collected (i.e. before limit on Metaspace or Heap is reached)

--

So, the basic idea would to refactor ClassInfo caches to use 3) or 4) and maybe to override Introspector...

Here's the hack I made for ClassInfo, based on the master branch (note that in that branch there is even still a hard reference to the Class in ClassInfo):

Not using ClassValue stuff at all:

/*private static final GroovyClassValue<ClassInfo> globalClassValue = GroovyClassValueFactory.createGroovyClassValue(new ComputeValue<ClassInfo>(){
        @Override
        public ClassInfo computeValue(Class<?> type) {
            ClassInfo ret = new ClassInfo(type);
            globalClassSet.add(ret);
            return ret;
        }
    });*/

Instead getting ClassInfo from class from a refactored GlobalClassSet:

    public static ClassInfo getClassInfo (Class cls) {
        return globalClassSet.get(cls);
        //return globalClassValue.get(cls);
    }

and here is the refactored GlobalClassSet, now based on a WeakHashMap:

    private static class GlobalClassSet {

//private final ManagedLinkedList<ClassInfo> items = new ManagedLinkedList<ClassInfo>(weakBundle); private final WeakHashMap<Class,WeakReference<ClassInfo>> items = new WeakHashMap<Class,WeakReference<ClassInfo>>();

        public int size(){
            return values().size();
        }

        public int fullSize(){
            return values().size();
        }

        public Collection<ClassInfo> values(){
            synchronized(items){
Collection<WeakReference<ClassInfo>> values = items.values();
                List<ClassInfo> list = new ArrayList<ClassInfo>();
                for (WeakReference<ClassInfo> value : values) {
                    ClassInfo info = value.get();
                    if (info != null) {
                        //System.out.println("ClassInfo is null");
                        list.add(info);
                    }
                }
                return list;
                //return Arrays.asList(items.toArray(new ClassInfo[0]));
            }
        }

        public void add(ClassInfo value){
            synchronized(items){
                //items.add(value);
items.put(value.klazz, new WeakReference<ClassInfo>(value));
            }
        }

        public ClassInfo get(Class cls) {
            WeakReference<ClassInfo> ref;
            synchronized(items) {
                ref = items.get(cls);
            }
            ClassInfo info;
            if (ref == null) {
//System.out.println("ClassInfo Ref is null: " + cls.getName());
                info = new ClassInfo(cls);
                synchronized (items) {
                    items.put(cls, new WeakReference<ClassInfo>(info));
                }
                return info;
            }
            info = ref.get();
            if (info == null) {
//System.out.println("ClassInfo is null: " + cls.getName());
                info = new ClassInfo(cls);
                items.put(cls, new WeakReference<ClassInfo>(info));
                return info;
            }
            return info;
        }

    }

What looks less than ideal is the first synchronize on items in get(), but I don't know to what degree that would matter in practice, I don't know how often that is called. In my tests this version appeared even to be slightly faster than the one that is using Java 7 ClassValue, but there was just a single thread...

For testing with ClassGCTester I made a manual cleanup of the Introspector cache after loading each class in the loop, if only loading the GroovyFilling class from the URLClassLoader like this:

   Introspector.flushFromCaches(clazz);

If loading also all of Groovy from the URLClassLoader I had to do it like this to clean up for all Groovy classes (which of course makes things slower):

   Introspector.flushCaches();

Sample output if only loading the GroovyFilling class from the URLClassLoader:

--
Java Version:    1.8.0_92
Groovy Version:  2.5.0-SNAPSHOT
Java Class Path: .:groovy-2.5.0-SNAPSHOT.jar
Arguments:       -cp filling/ -parent tester -classes GroovyFilling
VM Arguments:    -XX:MaxMetaspaceSize=64m -Xmx512m
PID:             57399

Secs Test classes Metaspace/PermGen Heap Load time Create time #loaded #remaining used committed used committed average average 0 1 1 6.3m 6.5m 13.3m 245.5m 1.150ms 15.411ms 1 498 498 9.1m 10.5m 29.9m 245.5m 0.339ms 1.598ms 2 1361 1361 12.4m 15.5m 26.1m 245.5m 0.267ms 1.164ms 3 2303 10 7.3m 16.8m 4.6m 229.0m 0.252ms 1.022ms 4 3496 1203 11.8m 16.8m 46.3m 244.5m 0.218ms 0.904ms 5 4640 2347 16.2m 21.4m 71.6m 240.0m 0.207ms 0.852ms 6 5637 3344 20.0m 27.1m 74.8m 237.5m 0.203ms 0.843ms 7 6827 1024 11.1m 18.2m 19.1m 269.0m 0.200ms 0.808ms 8 8120 2317 16.1m 23.4m 73.2m 254.0m 0.191ms 0.779ms 9 9382 3579 20.9m 28.6m 122.7m 269.0m 0.184ms 0.761ms 10 10669 1036 11.2m 22.0m 21.3m 274.5m 0.183ms 0.741ms 11 12010 2377 16.3m 24.3m 81.9m 289.5m 0.177ms 0.726ms 12 13275 3642 21.2m 29.3m 129.5m 288.5m 0.174ms 0.718ms 13 14482 4849 25.8m 36.0m 42.9m 289.5m 0.172ms 0.714ms 14 15792 1177 11.7m 22.7m 36.1m 319.5m 0.172ms 0.704ms 15 17112 2497 16.8m 27.0m 86.5m 319.5m 0.169ms 0.697ms
--

Sample output if loading Groovy and the GroovyFilling class from the URLClassLoader:

--
Java Version:    1.8.0_92
Groovy Version:  2.5.0-SNAPSHOT
Java Class Path: .
Arguments: -cp groovy-2.5.0-SNAPSHOT.jar:filling/ -parent null -classes GroovyFilling
VM Arguments:    -XX:MaxMetaspaceSize=64m -Xmx512m
PID:             59454

Secs Test classes Metaspace/PermGen Heap Load time Create time #loaded #remaining used committed used committed average average 0 1 1 7.9m 8.5m 18.0m 245.5m 2.345ms 122.303ms 1 10 3 10.3m 16.8m 24.2m 225.0m 1.798ms 103.932ms 2 20 1 7.0m 20.0m 20.4m 267.5m 1.512ms 100.385ms 3 29 10 22.4m 23.9m 83.2m 267.5m 1.436ms 101.481ms 4 41 7 17.3m 21.3m 65.7m 319.5m 1.332ms 97.067ms 5 52 2 8.8m 21.5m 31.2m 352.0m 1.284ms 95.194ms 6 64 14 29.3m 31.2m 116.6m 352.0m 1.235ms 92.452ms 7 76 9 20.8m 23.4m 85.5m 315.5m 1.193ms 90.991ms 8 88 4 12.3m 20.5m 35.2m 362.5m 1.166ms 90.134ms 9 100 16 32.8m 34.5m 54.7m 366.5m 1.135ms 88.930ms 10 112 11 24.2m 26.8m 84.6m 397.0m 1.109ms 88.191ms
[...]
--

As I said, so far rather a hack, probably better to reimplement the GroovyClassValuePreJava7 class instead? Performance under concurrent use? Are other caches that apparently exist in ClassInfo also no issue under different circumstances? (And at some point: does it work across VMs and OSes etc.?)

Would it make sense to implement a "GroovyIntrospector" which caches things in a WeakHashMap<Class,<WeakReference<Method[]>> instead of in a WeakHashMap<Class,Method[]> as does Introspector, or something like that? Not sure there, because it is all static and not sure how much this has or will change from Java release to Java release, but maybe that is not so important, just need an implementation that works? Or is it sort of a public API for Groovy classes that is widely used?

Alain

Reply via email to