View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.beanutils.memoryleaktests;
18  
19  import java.lang.ref.SoftReference;
20  import java.lang.ref.WeakReference;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  import java.net.URLClassLoader;
24  import java.util.Locale;
25  
26  import org.apache.commons.beanutils.BeanUtilsBean;
27  import org.apache.commons.beanutils.ConvertUtils;
28  import org.apache.commons.beanutils.MappedPropertyDescriptor;
29  import org.apache.commons.beanutils.MethodUtils;
30  import org.apache.commons.beanutils.PropertyUtils;
31  import org.apache.commons.beanutils.WrapDynaBean;
32  import org.apache.commons.beanutils.WrapDynaClass;
33  import org.apache.commons.beanutils.converters.IntegerConverter;
34  import org.apache.commons.beanutils.locale.LocaleBeanUtilsBean;
35  import org.apache.commons.beanutils.locale.LocaleConvertUtils;
36  import org.apache.commons.beanutils.locale.converters.IntegerLocaleConverter;
37  import org.junit.Assume;
38  import org.junit.Test;
39  import static org.junit.Assert.*;
40  
41  /**
42   * Tests BeanUtils memory leaks.
43   *
44   * See https://issues.apache.org/jira/browse/BEANUTILS-291
45   *
46   * @version $Id$
47   */
48  public class MemoryLeakTestCase {
49  
50      /**
51       * Tests that PropertyUtilsBean's descriptorsCache doesn't cause a memory leak.
52       */
53      @Test
54      public void testPropertyUtilsBean_descriptorsCache_memoryLeak() throws Exception {
55  
56          // Clear All BeanUtils caches before the test
57          clearAllBeanUtilsCaches();
58  
59          final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.SomePojo";
60  
61          // The classLoader will go away only when these following variables are released
62          ClassLoader loader = newClassLoader();
63          Class<?> beanClass    = loader.loadClass(className);
64          Object bean        = beanClass.newInstance();
65          // -----------------------------------------------------------------------------
66  
67          final WeakReference<ClassLoader> someRef = new WeakReference<ClassLoader>(loader);
68  
69          // Sanity checks only
70          assertNotNull("ClassLoader is null", loader);
71          assertNotNull("BeanClass is null", beanClass);
72          assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
73          assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
74  
75          // if you comment the following line, the testcase will work, and the ClassLoader will be released.
76          // That proves that nothing is wrong with the test, and PropertyUtils is holding a reference
77          assertEquals("initialValue", PropertyUtils.getProperty(bean, "name"));
78  
79          // this should make the reference go away.
80          loader    = null;
81          beanClass = null;
82          bean      = null;
83  
84          forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
85  
86          if (someRef.get() != null) {
87              profilerLeakReport("PropertyUtilsBean descriptorsCache", className);
88          }
89  
90          // if everything is fine, this will be null
91          assertNull("PropertyUtilsBean is holding a reference to the classLoader", someRef.get());
92  
93          // Clear All BeanUtils caches after the test
94          clearAllBeanUtilsCaches();
95      }
96  
97      /**
98       * Tests that PropertyUtilsBean's mappedDescriptorsCache doesn't cause a memory leak.
99       */
100     @Test
101     public void testPropertyUtilsBean_mappedDescriptorsCache_memoryLeak() throws Exception {
102 
103         // Clear All BeanUtils caches before the test
104         clearAllBeanUtilsCaches();
105 
106         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.SomeMappedPojo";
107 
108         // The classLoader will go away only when these following variables are released
109         ClassLoader loader = newClassLoader();
110         Class<?> beanClass    = loader.loadClass(className);
111         Object bean        = beanClass.newInstance();
112         // -----------------------------------------------------------------------------
113 
114         final WeakReference<ClassLoader> someRef = new WeakReference<ClassLoader>(loader);
115 
116         // Sanity checks only
117         assertNotNull("ClassLoader is null", loader);
118         assertNotNull("BeanClass is null", beanClass);
119         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
120         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
121 
122         // if you comment the following three lines, the testcase will work, and the ClassLoader will be released.
123         // That proves that nothing is wrong with the test, and PropertyUtils is holding a reference
124         assertEquals("Second Value", PropertyUtils.getProperty(bean, "mappedProperty(Second Key)"));
125         PropertyUtils.setProperty(bean, "mappedProperty(Second Key)", "New Second Value");
126         assertEquals("New Second Value", PropertyUtils.getProperty(bean, "mappedProperty(Second Key)"));
127 
128         // this should make the reference go away.
129         loader = null;
130         beanClass = null;
131         bean = null;
132 
133         // PropertyUtilsBean uses the MethodUtils's method cache for mapped properties.
134         // Uncomment the following line to check this is not just a repeat of that memory leak.
135         // MethodUtils.clearCache();
136 
137         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
138 
139         if (someRef.get() != null) {
140             profilerLeakReport("PropertyUtilsBean mappedDescriptorsCache", className);
141         }
142 
143         // if everything is fine, this will be null
144         assertNull("PropertyUtilsBean is holding a reference to the classLoader", someRef.get());
145 
146         // Clear All BeanUtils caches after the test
147         clearAllBeanUtilsCaches();
148     }
149 
150     /**
151      * Tests that MappedPropertyDescriptor can re-create the Method reference after it
152      * has been garbage collected.
153      */
154     @Test
155     public void testMappedPropertyDescriptor_MappedMethodReference1() throws Exception {
156 
157         // Clear All BeanUtils caches before the test
158         clearAllBeanUtilsCaches();
159 
160         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.SomeMappedPojo";
161         final ClassLoader loader = newClassLoader();
162         final Class<?> beanClass    = loader.loadClass(className);
163         final Object bean        = beanClass.newInstance();
164         // -----------------------------------------------------------------------------
165 
166         // Sanity checks only
167         assertNotNull("ClassLoader is null", loader);
168         assertNotNull("BeanClass is null", beanClass);
169         assertNotNull("Bean is null", bean);
170         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
171         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
172 
173         final MappedPropertyDescriptor descriptor = new MappedPropertyDescriptor("mappedProperty", beanClass);
174         assertNotNull("1-Read Method null", descriptor.getMappedReadMethod());
175         assertNotNull("1-Write Method null", descriptor.getMappedWriteMethod());
176         assertEquals("1-Read Method name", "getMappedProperty", descriptor.getMappedReadMethod().getName());
177         assertEquals("1-Read Write name", "setMappedProperty", descriptor.getMappedWriteMethod().getName());
178 
179         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
180 
181         // The aim of this test is to check the functinality in MappedPropertyDescriptor which
182         // re-creates the Method references after they have been garbage collected. However theres no
183         // way of knowing the method references were garbage collected and that code was run, except by
184         // un-commeting the System.out statement in MappedPropertyDescriptor's MappedMethodReference's
185         // get() method.
186 
187         assertNotNull("1-Read Method null", descriptor.getMappedReadMethod());
188         assertNotNull("1-Write Method null", descriptor.getMappedWriteMethod());
189         assertEquals("1-Read Method name", "getMappedProperty", descriptor.getMappedReadMethod().getName());
190         assertEquals("1-Read Write name", "setMappedProperty", descriptor.getMappedWriteMethod().getName());
191 
192         // Clear All BeanUtils caches after the test
193         clearAllBeanUtilsCaches();
194     }
195 
196     /**
197      * Tests that MappedPropertyDescriptor can re-create the Method reference after it
198      * has been garbage collected.
199      */
200     @Test
201     public void testMappedPropertyDescriptor_MappedMethodReference2() throws Exception {
202 
203         // Clear All BeanUtils caches before the test
204         clearAllBeanUtilsCaches();
205 
206         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.SomeMappedPojo";
207         ClassLoader loader = newClassLoader();
208         Class<?> beanClass    = loader.loadClass(className);
209         Object bean        = beanClass.newInstance();
210         // -----------------------------------------------------------------------------
211 
212         // Sanity checks only
213         assertNotNull("ClassLoader is null", loader);
214         assertNotNull("BeanClass is null", beanClass);
215         assertNotNull("Bean is null", bean);
216         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
217         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
218 
219         final MappedPropertyDescriptor descriptor = new MappedPropertyDescriptor("mappedProperty", beanClass);
220         assertNotNull("1-Read Method null", descriptor.getMappedReadMethod());
221         assertNotNull("1-Write Method null", descriptor.getMappedWriteMethod());
222         assertEquals("1-Read Method name", "getMappedProperty", descriptor.getMappedReadMethod().getName());
223         assertEquals("1-Read Write name", "setMappedProperty", descriptor.getMappedWriteMethod().getName());
224 
225         // this should make the reference go away.
226         loader = null;
227         beanClass = null;
228         bean = null;
229 
230         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
231 
232         // The aim of this test is to check the functinality in MappedPropertyDescriptor which
233         // re-creates the Method references after they have been garbage collected. However theres no
234         // way of knowing the method references were garbage collected and that code was run, except by
235         // un-commeting the System.out statement in MappedPropertyDescriptor's MappedMethodReference's
236         // get() method.
237 
238         assertNotNull("1-Read Method null", descriptor.getMappedReadMethod());
239         assertNotNull("1-Write Method null", descriptor.getMappedWriteMethod());
240         assertEquals("1-Read Method name", "getMappedProperty", descriptor.getMappedReadMethod().getName());
241         assertEquals("1-Read Write name", "setMappedProperty", descriptor.getMappedWriteMethod().getName());
242 
243         // Clear All BeanUtils caches after the test
244         clearAllBeanUtilsCaches();
245     }
246 
247     /**
248      * Tests that MethodUtils's cache doesn't cause a memory leak.
249      */
250     @Test
251     public void testMethodUtils_cache_memoryLeak() throws Exception {
252 
253         // Clear All BeanUtils caches before the test
254         clearAllBeanUtilsCaches();
255 
256         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.SomePojo";
257 
258         // The classLoader will go away only when these following variables are released
259         ClassLoader loader = newClassLoader();
260         Class<?> beanClass    = loader.loadClass(className);
261         Object bean        = beanClass.newInstance();
262         // -----------------------------------------------------------------------------
263 
264         final WeakReference<ClassLoader> someRef = new WeakReference<ClassLoader>(loader);
265 
266         // Sanity checks only
267         assertNotNull("ClassLoader is null", loader);
268         assertNotNull("BeanClass is null", beanClass);
269         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
270         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
271 
272         // if you comment the following line, the testcase will work, and the ClassLoader will be released.
273         // That proves that nothing is wrong with the test, and MethodUtils is holding a reference
274         assertEquals("initialValue", MethodUtils.invokeExactMethod(bean, "getName", new Object[0]));
275 
276         // this should make the reference go away.
277         loader    = null;
278         beanClass = null;
279         bean      = null;
280 
281         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
282 
283         if (someRef.get() != null) {
284             profilerLeakReport("MethodUtils cache", className);
285         }
286 
287         // if everything is fine, this will be null
288         assertNull("MethodUtils is holding a reference to the classLoader", someRef.get());
289 
290         // Clear All BeanUtils caches after the test
291         clearAllBeanUtilsCaches();
292     }
293 
294     /**
295      * Tests that WrapDynaClass's dynaClasses doesn't cause a memory leak.
296      */
297     @Test
298     public void testWrapDynaClass_dynaClasses_memoryLeak() throws Exception {
299 
300         // Clear All BeanUtils caches before the test
301         clearAllBeanUtilsCaches();
302 
303         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.SomePojo";
304 
305         // The classLoader will go away only when these following variables are released
306         ClassLoader loader = newClassLoader();
307         Class<?> beanClass    = loader.loadClass(className);
308         Object bean        = beanClass.newInstance();
309         WrapDynaBean wrapDynaBean = new WrapDynaBean(bean);
310         // -----------------------------------------------------------------------------
311 
312         final WeakReference<ClassLoader> someRef = new WeakReference<ClassLoader>(loader);
313 
314         // Sanity checks only
315         assertNotNull("ClassLoader is null", loader);
316         assertNotNull("BeanClass is null", beanClass);
317         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
318         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
319 
320         // if you comment the following line, the testcase will work, and the ClassLoader will be released.
321         // That proves that nothing is wrong with the test, and WrapDynaClass is holding a reference
322         assertEquals("initialValue", wrapDynaBean.get("name"));
323 
324         // this should make the reference go away.
325         loader       = null;
326         beanClass    = null;
327         bean         = null;
328         wrapDynaBean = null;
329 
330         // Wrap Dyna Class uses the PropertyUtilsBean's decriptor caches.
331         // Uncomment the following line to check this is not just a repeat of that memory leak.
332         // BeanUtilsBean.getInstance().getPropertyUtils().clearDescriptors();
333 
334         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
335 
336         if (someRef.get() != null) {
337             profilerLeakReport("WrapDynaClass dynaClasses", className);
338         }
339 
340         // if everything is fine, this will be null
341         assertNull("WrapDynaClass is holding a reference to the classLoader", someRef.get());
342 
343         // Clear All BeanUtils caches after the test
344         clearAllBeanUtilsCaches();
345     }
346 
347     /**
348      * Tests that ConvertUtilsBean's converters doesn't cause a memory leak.
349      */
350     @Test
351     public void testConvertUtilsBean_converters_memoryLeak() throws Exception {
352 
353         // Clear All BeanUtils caches before the test
354         clearAllBeanUtilsCaches();
355 
356         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.CustomInteger";
357 
358         // The classLoader will go away only when these following variables are released
359         ClassLoader loader = newClassLoader();
360         Class<?> beanClass    = loader.loadClass(className);
361         Object bean        = beanClass.newInstance();
362         // -----------------------------------------------------------------------------
363 
364         final WeakReference<ClassLoader> someRef = new WeakReference<ClassLoader>(loader);
365 
366         // Sanity checks only
367         assertNotNull("ClassLoader is null", loader);
368         assertNotNull("BeanClass is null", beanClass);
369         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
370         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
371 
372         // if you comment the following two lines, the testcase will work, and the ClassLoader will be released.
373         // That proves that nothing is wrong with the test, and ConvertUtilsBean is holding a reference
374         ConvertUtils.register(new IntegerConverter(), beanClass);
375         assertEquals("12345", ConvertUtils.convert(bean, String.class));
376 
377         // this should make the reference go away.
378         loader    = null;
379         beanClass = null;
380         bean      = null;
381 
382         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
383 
384         if (someRef.get() != null) {
385             profilerLeakReport("ConvertUtilsBean converters", className);
386         }
387 
388         // if everything is fine, this will be null
389         assertNull("ConvertUtilsBean is holding a reference to the classLoader", someRef.get());
390 
391         // Clear All BeanUtils caches after the test
392         clearAllBeanUtilsCaches();
393     }
394 
395     /**
396      * Tests that LocaleConvertUtilsBean's converters doesn't cause a memory leak.
397      */
398     @Test
399     public void testLocaleConvertUtilsBean_converters_memoryLeak() throws Exception {
400 
401         // Clear All BeanUtils caches before the test
402         clearAllBeanUtilsCaches();
403 
404         final String className = "org.apache.commons.beanutils.memoryleaktests.pojotests.CustomInteger";
405 
406         // The classLoader will go away only when these following variables are released
407         ClassLoader loader = newClassLoader();
408         Class<?> beanClass    = loader.loadClass(className);
409         Object bean        = beanClass.newInstance();
410         // -----------------------------------------------------------------------------
411 
412         final WeakReference<ClassLoader> someRef = new WeakReference<ClassLoader>(loader);
413 
414         // Sanity checks only
415         assertNotNull("ClassLoader is null", loader);
416         assertNotNull("BeanClass is null", beanClass);
417         assertNotSame("ClassLoaders should be different..", getClass().getClassLoader(), beanClass.getClassLoader());
418         assertSame("BeanClass ClassLoader incorrect", beanClass.getClassLoader(), loader);
419 
420         // if you comment the following two lines, the testcase will work, and the ClassLoader will be released.
421         // That proves that nothing is wrong with the test, and LocaleConvertUtilsBean is holding a reference
422         LocaleConvertUtils.register(new IntegerLocaleConverter(Locale.US, false), beanClass, Locale.US);
423         assertEquals(new Integer(12345), LocaleConvertUtils.convert(bean.toString(), Integer.class, Locale.US, "#,###"));
424 
425         // this should make the reference go away.
426         loader    = null;
427         beanClass = null;
428         bean      = null;
429 
430         forceGarbageCollection(); /* Try to force the garbage collector to run by filling up memory */
431 
432         if (someRef.get() != null) {
433             profilerLeakReport("LocaleConvertUtilsBean converters", className);
434         }
435 
436         // if everything is fine, this will be null
437         assertNull("LocaleConvertUtilsBean is holding a reference to the classLoader", someRef.get());
438 
439         // Clear All BeanUtils caches after the test
440         clearAllBeanUtilsCaches();
441     }
442 
443     /**
444      * Clears all the BeanUtils Caches manually.
445      *
446      * This is probably overkill, but since we're dealing with static caches
447      * it seems sensible to ensure that all test cases start with a clean sheet.
448      */
449     private void clearAllBeanUtilsCaches() {
450 
451         // Clear BeanUtilsBean's PropertyUtilsBean descriptor caches
452         BeanUtilsBean.getInstance().getPropertyUtils().clearDescriptors();
453 
454         // Clear LocaleBeanUtilsBean's PropertyUtilsBean descriptor caches
455         LocaleBeanUtilsBean.getLocaleBeanUtilsInstance().getPropertyUtils().clearDescriptors();
456 
457         // Clear MethodUtils's method cache
458         MethodUtils.clearCache();
459 
460         // Clear WrapDynaClass cache
461         WrapDynaClass.clear();
462 
463         // replace the existing BeanUtilsBean instance for the current class loader with a new, clean instance
464         BeanUtilsBean.setInstance(new BeanUtilsBean());
465 
466         // replace the existing LocaleBeanUtilsBean instance for the current class loader with a new, clean instance
467         LocaleBeanUtilsBean.setInstance(new LocaleBeanUtilsBean());
468     }
469 
470     /**
471      * Tries to force the garbage collector to run by filling up memory and calling System.gc().
472      */
473     private void forceGarbageCollection() throws Exception {
474         // Fill up memory
475         final SoftReference<Object> ref = new SoftReference<Object>(new Object());
476         int count = 0;
477         while(ref.get() != null && count++ < 5) {
478             java.util.ArrayList<String> list = new java.util.ArrayList<String>();
479             try {
480                 long i = 0;
481                 while (true && ref.get() != null) {
482                     list.add("A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String A Big String " + (i++));
483                 }
484                 System.out.println("Count(1) " + count + " : " + getMemoryStats());
485             } catch (final OutOfMemoryError ignored) {
486                 // Cannot do anything here
487             }
488             // Trying to debug Continuum test fail: try calling GC before releasing the memory
489             System.gc();
490             list.clear();
491             list = null;
492             System.gc();
493             System.out.println("After GC2: " + getMemoryStats()+ " Count " + count);
494             Thread.sleep(1000);
495         }
496 
497         final boolean isNotNull = ref.get() != null;
498         System.out.println("Count " + count+ " " + isNotNull); // debug for Continuum failure
499         String message = "Your JVM is not releasing SoftReference, try running the testcase with less memory (-Xmx)";
500         Assume.assumeFalse(message, isNotNull);
501     }
502 
503     /**
504      * Creates a new class loader instance.
505      */
506     private static URLClassLoader newClassLoader() throws MalformedURLException {
507 
508         final String dataFilePath = MemoryLeakTestCase.class.getResource("pojotests").getFile();
509         //System.out.println("dataFilePath: " + dataFilePath);
510         final String location = "file://" + dataFilePath.substring(0,dataFilePath.length()-"org.apache.commons.beanutils.memoryleaktests.pojotests".length());
511         //System.out.println("location: " + location);
512 
513         final StringBuilder newString = new StringBuilder();
514         for (int i=0;i<location.length();i++) {
515             if (location.charAt(i)=='\\') {
516                 newString.append("/");
517             } else {
518                 newString.append(location.charAt(i));
519             }
520         }
521         final String classLocation = newString.toString();
522         //System.out.println("classlocation: " + classLocation);
523 
524         final URLClassLoader theLoader = URLClassLoader.newInstance(new URL[]{new URL(classLocation)},null);
525         return theLoader;
526     }
527 
528     /**
529      * Produces a profiler report about where the leaks are.
530      *
531      * This requires JBoss's profiler be installed, see:
532      *     http://labs.jboss.com/jbossprofiler/
533      *
534      * @param className The name of the class to profile
535      */
536     private void profilerLeakReport(final String test, final String className) {
537        /*
538         * If you want a report about where the leaks are... uncomment this,
539         * add jboss-profiler.jvmti.jar and jboss-commons.jar (for org.jboss.loggin).
540         * You will then have a report for where the references are.
541         System.out.println(" ----------------" + test + " START ----------------");
542         org.jboss.profiler.jvmti.JVMTIInterface jvmti = new org.jboss.profiler.jvmti.JVMTIInterface();
543         System.out.println(jvmti.exploreClassReferences(className, 8, true, true, true, false, false));
544         System.out.println(" ----------------" + test + " END ------------------");
545         */
546     }
547 
548     /**
549      * Gets the total, free, used memory stats.
550      * @return the total, free, used memory stats
551      */
552     private String getMemoryStats() {
553         final java.text.DecimalFormat fmt = new java.text.DecimalFormat("#,##0");
554         final Runtime runtime = Runtime.getRuntime();
555         final long free = runtime.freeMemory() / 1024;
556         final long total = runtime.totalMemory() / 1024;
557         final long used = total - free;
558         return "MEMORY - Total: " + fmt.format(total) + "k " + "Used: "
559                 + fmt.format(used) + "k " + "Free: "
560                 + fmt.format(free) + "k";
561     }
562 }