groovy-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Alain Stalder <astal...@span.ch>
Subject Re: Improve Groovy class loading performance and memory management
Date Tue, 17 May 2016 19:23:03 GMT
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

Mime
View raw message