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 }