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  
18  package org.apache.commons.beanutils.converters;
19  
20  import java.lang.ref.WeakReference;
21  import static org.junit.Assert.*;
22  import org.apache.commons.beanutils.ConvertUtils;
23  import org.apache.commons.beanutils.Converter;
24  import org.junit.Test;
25  
26  /**
27   * This class provides a number of unit tests related to classloaders and
28   * garbage collection, particularly in j2ee-like situations.
29   *
30   * @version $Id$
31   */
32  public class MemoryTestCase {
33  
34      @Test
35      public void testWeakReference() throws Exception {
36          final ClassLoader origContextClassLoader = Thread.currentThread().getContextClassLoader();
37          try {
38          ClassReloader componentLoader = new ClassReloader(origContextClassLoader);
39  
40          Thread.currentThread().setContextClassLoader(componentLoader);
41          Thread.currentThread().setContextClassLoader(origContextClassLoader);
42  
43          final WeakReference<ClassLoader> ref = new WeakReference<ClassLoader>(componentLoader);
44          componentLoader = null;
45  
46          forceGarbageCollection(ref);
47          assertNull(ref.get());
48          } finally {
49              // Restore context classloader that was present before this
50              // test started. It is expected to be the same as the system
51              // classloader, but we handle all cases here..
52              Thread.currentThread().setContextClassLoader(origContextClassLoader);
53  
54              // and restore all the standard converters
55              ConvertUtils.deregister();
56          }
57      }
58  
59      /**
60       * Test whether registering a standard Converter instance while
61       * a custom context classloader is set causes a memory leak.
62       *
63       * <p>This test emulates a j2ee container where BeanUtils has been
64       * loaded from a "common" lib location that is shared across all
65       * components running within the container. The "component" registers
66       * a converter object, whose class was loaded from the "common" lib
67       * location. The registered converter:
68       * <ul>
69       * <li>should not be visible to other components; and</li>
70       * <li>should not prevent the component-specific classloader from being
71       *  garbage-collected when the container sets its reference to null.
72       * </ul>
73       *
74       */
75      @Test
76      public void testComponentRegistersStandardConverter() throws Exception {
77  
78          final ClassLoader origContextClassLoader = Thread.currentThread().getContextClassLoader();
79          try {
80              // sanity check; who's paranoid?? :-)
81              assertEquals(origContextClassLoader, ConvertUtils.class.getClassLoader());
82  
83              // create a custom classloader for a "component"
84              // just like a container would.
85              ClassLoader componentLoader1 = new ClassLoader() {};
86              final ClassLoader componentLoader2 = new ClassLoader() {};
87  
88              final Converter origFloatConverter = ConvertUtils.lookup(Float.TYPE);
89              final Converter floatConverter1 = new FloatConverter();
90  
91              // Emulate the container invoking a component #1, and the component
92              // registering a custom converter instance whose class is
93              // available via the "shared" classloader.
94              Thread.currentThread().setContextClassLoader(componentLoader1);
95              {
96                  // here we pretend we're running inside component #1
97  
98                  // When we first do a ConvertUtils operation inside a custom
99                  // classloader, we get a completely fresh copy of the
100                 // ConvertUtilsBean, with all-new Converter objects in it..
101                 assertFalse(ConvertUtils.lookup(Float.TYPE) == origFloatConverter);
102 
103                 // Now we register a custom converter (but of a standard class).
104                 // This should only affect code that runs with exactly the
105                 // same context classloader set.
106                 ConvertUtils.register(floatConverter1, Float.TYPE);
107                 assertTrue(ConvertUtils.lookup(Float.TYPE) == floatConverter1);
108             }
109             Thread.currentThread().setContextClassLoader(origContextClassLoader);
110 
111             // The converter visible outside any custom component should not
112             // have been altered.
113             assertTrue(ConvertUtils.lookup(Float.TYPE) == origFloatConverter);
114 
115             // Emulate the container invoking a component #2.
116             Thread.currentThread().setContextClassLoader(componentLoader2);
117             {
118                 // here we pretend we're running inside component #2
119 
120                 // we should get a completely fresh ConvertUtilsBean, with
121                 // all-new Converter objects again.
122                 assertFalse(ConvertUtils.lookup(Float.TYPE) == origFloatConverter);
123                 assertFalse(ConvertUtils.lookup(Float.TYPE) == floatConverter1);
124             }
125             Thread.currentThread().setContextClassLoader(origContextClassLoader);
126 
127             // Emulate a container "undeploying" component #1. This should
128             // make component loader available for garbage collection (we hope)
129             final WeakReference<ClassLoader> weakRefToComponent1 = new WeakReference<ClassLoader>(componentLoader1);
130             componentLoader1 = null;
131 
132             // force garbage collection and  verify that the componentLoader
133             // has been garbage-collected
134             forceGarbageCollection(weakRefToComponent1);
135             assertNull(
136                 "Component classloader did not release properly; memory leak present",
137                 weakRefToComponent1.get());
138         } finally {
139             // Restore context classloader that was present before this
140             // test started, so that in case of a test failure we don't stuff
141             // up later tests...
142             Thread.currentThread().setContextClassLoader(origContextClassLoader);
143 
144             // and restore all the standard converters
145             ConvertUtils.deregister();
146         }
147     }
148 
149     /**
150      * Test whether registering a custom Converter subclass while
151      * a custom context classloader is set causes a memory leak.
152      *
153      * <p>This test emulates a j2ee container where BeanUtils has been
154      * loaded from a "common" lib location that is shared across all
155      * components running within the container. The "component" registers
156      * a converter object, whose class was loaded via the component-specific
157      * classloader. The registered converter:
158      * <ul>
159      * <li>should not be visible to other components; and</li>
160      * <li>should not prevent the component-specific classloader from being
161      *  garbage-collected when the container sets its reference to null.
162      * </ul>
163      *
164      */
165     @Test
166     public void testComponentRegistersCustomConverter() throws Exception {
167 
168         final ClassLoader origContextClassLoader = Thread.currentThread().getContextClassLoader();
169         try {
170             // sanity check; who's paranoid?? :-)
171             assertEquals(origContextClassLoader, ConvertUtils.class.getClassLoader());
172 
173             // create a custom classloader for a "component"
174             // just like a container would.
175             ClassReloader componentLoader = new ClassReloader(origContextClassLoader);
176 
177             // Load a custom Converter via component loader. This emulates what
178             // would happen if a user wrote their own FloatConverter subclass
179             // and deployed it via the component-specific classpath.
180             Thread.currentThread().setContextClassLoader(componentLoader);
181             {
182               // Here we pretend we're running inside the component, and that
183               // a class FloatConverter has been loaded from the component's
184               // private classpath.
185               final Class<?> newFloatConverterClass = componentLoader.reload(FloatConverter.class);
186               Object newFloatConverter = newFloatConverterClass.newInstance();
187               assertTrue(newFloatConverter.getClass().getClassLoader() == componentLoader);
188 
189               // verify that this new object does implement the Converter type
190               // despite being loaded via a classloader different from the one
191               // that loaded the Converter class.
192               assertTrue(
193                 "Converter loader via child does not implement parent type",
194                 Converter.class.isInstance(newFloatConverter));
195 
196               // this converter registration will only apply to the
197               // componentLoader classloader...
198               ConvertUtils.register((Converter)newFloatConverter, Float.TYPE);
199 
200               // After registering a custom converter, lookup should return
201               // it back to us. We'll try this lookup again with a different
202               // context-classloader set, and shouldn't see it
203               final Converter componentConverter = ConvertUtils.lookup(Float.TYPE);
204               assertTrue(componentConverter.getClass().getClassLoader() == componentLoader);
205 
206               newFloatConverter = null;
207             }
208             Thread.currentThread().setContextClassLoader(origContextClassLoader);
209 
210             // Because the context classloader has been reset, we shouldn't
211             // see the custom registered converter here...
212             final Converter sharedConverter = ConvertUtils.lookup(Float.TYPE);
213             assertFalse(sharedConverter.getClass().getClassLoader() == componentLoader);
214 
215             // and here we should see it again
216             Thread.currentThread().setContextClassLoader(componentLoader);
217             {
218                 final Converter componentConverter = ConvertUtils.lookup(Float.TYPE);
219                 assertTrue(componentConverter.getClass().getClassLoader() == componentLoader);
220             }
221             Thread.currentThread().setContextClassLoader(origContextClassLoader);
222             // Emulate a container "undeploying" the component. This should
223             // make component loader available for garbage collection (we hope)
224             final WeakReference<ClassLoader> weakRefToComponent = new WeakReference<ClassLoader>(componentLoader);
225             componentLoader = null;
226 
227             // force garbage collection and  verify that the componentLoader
228             // has been garbage-collected
229             forceGarbageCollection(weakRefToComponent);
230             assertNull(
231                 "Component classloader did not release properly; memory leak present",
232                 weakRefToComponent.get());
233         } finally {
234             // Restore context classloader that was present before this
235             // test started. It is expected to be the same as the system
236             // classloader, but we handle all cases here..
237             Thread.currentThread().setContextClassLoader(origContextClassLoader);
238 
239             // and restore all the standard converters
240             ConvertUtils.deregister();
241         }
242     }
243 
244     /**
245      * Attempt to force garbage collection of the specified target.
246      *
247      * <p>Unfortunately there is no way to force a JVM to perform
248      * garbage collection; all we can do is <i>hint</i> to it that
249      * garbage-collection would be a good idea, and to consume
250      * memory in order to trigger it.</p>
251      *
252      * <p>On return, target.get() will return null if the target has
253      * been garbage collected.</p>
254      *
255      * <p>If target.get() still returns non-null after this method has returned,
256      * then either there is some reference still being held to the target, or
257      * else we were not able to trigger garbage collection; there is no way
258      * to tell these scenarios apart.</p>
259      */
260     private void forceGarbageCollection(final WeakReference<?> target) {
261         int bytes = 2;
262 
263         while(target.get() != null) {
264             System.gc();
265 
266             // Create increasingly-large amounts of non-referenced memory
267             // in order to persuade the JVM to collect it. We are hoping
268             // here that the JVM is dumb enough to run a full gc pass over
269             // all data (including the target) rather than simply collecting
270             // this easily-reclaimable memory!
271             try {
272                 @SuppressWarnings("unused")
273                 final
274                 byte[] b = new byte[bytes];
275                 bytes = bytes * 2;
276             } catch(final OutOfMemoryError e) {
277                 // well that sure should have forced a garbage collection
278                 // run to occur!
279                 break;
280             }
281         }
282 
283         // and let's do one more just to clean up any garbage we might have
284         // created on the last pass..
285         System.gc();
286     }
287 }