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