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