/* | |
* Licensed to the Apache Software Foundation (ASF) under one or more | |
* contributor license agreements. See the NOTICE file distributed with | |
* this work for additional information regarding copyright ownership. | |
* The ASF licenses this file to You under the Apache License, Version 2.0 | |
* (the "License"); you may not use this file except in compliance with | |
* the License. You may obtain a copy of the License at | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
*/ | |
package org.apache.commons.beanutils2.converters; | |
import static org.junit.Assert.assertEquals; | |
import static org.junit.Assert.assertFalse; | |
import static org.junit.Assert.assertNull; | |
import static org.junit.Assert.assertTrue; | |
import java.lang.ref.WeakReference; | |
import org.apache.commons.beanutils2.ConvertUtils; | |
import org.apache.commons.beanutils2.Converter; | |
import org.junit.Test; | |
/** | |
* This class provides a number of unit tests related to classloaders and | |
* garbage collection, particularly in j2ee-like situations. | |
* | |
*/ | |
public class MemoryTestCase { | |
/** | |
* Attempt to force garbage collection of the specified target. | |
* | |
* <p>Unfortunately there is no way to force a JVM to perform | |
* garbage collection; all we can do is <i>hint</i> to it that | |
* garbage-collection would be a good idea, and to consume | |
* memory in order to trigger it.</p> | |
* | |
* <p>On return, target.get() will return null if the target has | |
* been garbage collected.</p> | |
* | |
* <p>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 were not able to trigger garbage collection; there is no way | |
* to tell these scenarios apart.</p> | |
*/ | |
private void forceGarbageCollection(final WeakReference<?> target) { | |
int bytes = 2; | |
while(target.get() != null) { | |
System.gc(); | |
// Create increasingly-large amounts of non-referenced memory | |
// in order to persuade the JVM to collect it. We are hoping | |
// here that the JVM is dumb enough to run a full gc pass over | |
// all data (including the target) rather than simply collecting | |
// this easily-reclaimable memory! | |
try { | |
@SuppressWarnings("unused") | |
final | |
byte[] b = new byte[bytes]; | |
bytes = bytes * 2; | |
} catch(final OutOfMemoryError e) { | |
// well that sure should have forced a garbage collection | |
// run to occur! | |
break; | |
} | |
} | |
// and let's do one more just to clean up any garbage we might have | |
// created on the last pass.. | |
System.gc(); | |
} | |
/** | |
* Test whether registering a custom Converter subclass while | |
* a custom context classloader is set causes a memory leak. | |
* | |
* <p>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 container. The "component" registers | |
* a converter object, whose class was loaded via the component-specific | |
* classloader. The registered converter: | |
* <ul> | |
* <li>should not be visible to other components; and</li> | |
* <li>should not prevent the component-specific classloader from being | |
* garbage-collected when the container sets its reference to null. | |
* </ul> | |
* | |
*/ | |
@Test | |
public void testComponentRegistersCustomConverter() throws Exception { | |
final ClassLoader origContextClassLoader = Thread.currentThread().getContextClassLoader(); | |
try { | |
// sanity check; who's paranoid?? :-) | |
assertEquals(origContextClassLoader, ConvertUtils.class.getClassLoader()); | |
// create a custom classloader for a "component" | |
// just like a container would. | |
ClassReloader componentLoader = new ClassReloader(origContextClassLoader); | |
// Load a custom Converter via component loader. This emulates what | |
// would happen if a user wrote their own FloatConverter subclass | |
// and deployed it via the component-specific classpath. | |
Thread.currentThread().setContextClassLoader(componentLoader); | |
{ | |
// Here we pretend we're running inside the component, and that | |
// a class FloatConverter has been loaded from the component's | |
// private classpath. | |
final Class<?> newFloatConverterClass = componentLoader.reload(FloatConverter.class); | |
Object newFloatConverter = newFloatConverterClass.newInstance(); | |
assertTrue(newFloatConverter.getClass().getClassLoader() == componentLoader); | |
// verify that this new object does implement the Converter type | |
// despite being loaded via a classloader different from the one | |
// that loaded the Converter class. | |
assertTrue( | |
"Converter loader via child does not implement parent type", | |
Converter.class.isInstance(newFloatConverter)); | |
// this converter registration will only apply to the | |
// componentLoader classloader... | |
ConvertUtils.register((Converter)newFloatConverter, Float.TYPE); | |
// After registering a custom converter, lookup should return | |
// it back to us. We'll try this lookup again with a different | |
// context-classloader set, and shouldn't see it | |
final Converter componentConverter = ConvertUtils.lookup(Float.TYPE); | |
assertTrue(componentConverter.getClass().getClassLoader() == componentLoader); | |
newFloatConverter = null; | |
} | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// Because the context classloader has been reset, we shouldn't | |
// see the custom registered converter here... | |
final Converter sharedConverter = ConvertUtils.lookup(Float.TYPE); | |
assertFalse(sharedConverter.getClass().getClassLoader() == componentLoader); | |
// and here we should see it again | |
Thread.currentThread().setContextClassLoader(componentLoader); | |
{ | |
final Converter componentConverter = ConvertUtils.lookup(Float.TYPE); | |
assertTrue(componentConverter.getClass().getClassLoader() == componentLoader); | |
} | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// Emulate a container "undeploying" the component. This should | |
// make component loader available for garbage collection (we hope) | |
final WeakReference<ClassLoader> weakRefToComponent = new WeakReference<>(componentLoader); | |
componentLoader = null; | |
// force garbage collection and verify that the componentLoader | |
// has been garbage-collected | |
forceGarbageCollection(weakRefToComponent); | |
assertNull( | |
"Component classloader did not release properly; memory leak present", | |
weakRefToComponent.get()); | |
} finally { | |
// Restore context classloader that was present before this | |
// test started. It is expected to be the same as the system | |
// classloader, but we handle all cases here.. | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// and restore all the standard converters | |
ConvertUtils.deregister(); | |
} | |
} | |
/** | |
* Test whether registering a standard Converter instance while | |
* a custom context classloader is set causes a memory leak. | |
* | |
* <p>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 container. The "component" registers | |
* a converter object, whose class was loaded from the "common" lib | |
* location. The registered converter: | |
* <ul> | |
* <li>should not be visible to other components; and</li> | |
* <li>should not prevent the component-specific classloader from being | |
* garbage-collected when the container sets its reference to null. | |
* </ul> | |
* | |
*/ | |
@Test | |
public void testComponentRegistersStandardConverter() throws Exception { | |
final ClassLoader origContextClassLoader = Thread.currentThread().getContextClassLoader(); | |
try { | |
// sanity check; who's paranoid?? :-) | |
assertEquals(origContextClassLoader, ConvertUtils.class.getClassLoader()); | |
// create a custom classloader for a "component" | |
// just like a container would. | |
ClassLoader componentLoader1 = new ClassLoader() {}; | |
final ClassLoader componentLoader2 = new ClassLoader() {}; | |
final Converter origFloatConverter = ConvertUtils.lookup(Float.TYPE); | |
final Converter floatConverter1 = new FloatConverter(); | |
// Emulate the container invoking a component #1, and the component | |
// registering a custom converter instance whose class is | |
// available via the "shared" classloader. | |
Thread.currentThread().setContextClassLoader(componentLoader1); | |
{ | |
// here we pretend we're running inside component #1 | |
// When we first do a ConvertUtils operation inside a custom | |
// classloader, we get a completely fresh copy of the | |
// ConvertUtilsBean, with all-new Converter objects in it.. | |
assertFalse(ConvertUtils.lookup(Float.TYPE) == origFloatConverter); | |
// Now we register a custom converter (but of a standard class). | |
// This should only affect code that runs with exactly the | |
// same context classloader set. | |
ConvertUtils.register(floatConverter1, Float.TYPE); | |
assertTrue(ConvertUtils.lookup(Float.TYPE) == floatConverter1); | |
} | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// The converter visible outside any custom component should not | |
// have been altered. | |
assertTrue(ConvertUtils.lookup(Float.TYPE) == origFloatConverter); | |
// Emulate the container invoking a component #2. | |
Thread.currentThread().setContextClassLoader(componentLoader2); | |
{ | |
// here we pretend we're running inside component #2 | |
// we should get a completely fresh ConvertUtilsBean, with | |
// all-new Converter objects again. | |
assertFalse(ConvertUtils.lookup(Float.TYPE) == origFloatConverter); | |
assertFalse(ConvertUtils.lookup(Float.TYPE) == floatConverter1); | |
} | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// Emulate a container "undeploying" component #1. This should | |
// make component loader available for garbage collection (we hope) | |
final WeakReference<ClassLoader> weakRefToComponent1 = new WeakReference<>(componentLoader1); | |
componentLoader1 = null; | |
// force garbage collection and verify that the componentLoader | |
// has been garbage-collected | |
forceGarbageCollection(weakRefToComponent1); | |
assertNull( | |
"Component classloader did not release properly; memory leak present", | |
weakRefToComponent1.get()); | |
} finally { | |
// Restore context classloader that was present before this | |
// test started, so that in case of a test failure we don't stuff | |
// up later tests... | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// and restore all the standard converters | |
ConvertUtils.deregister(); | |
} | |
} | |
@Test | |
public void testWeakReference() throws Exception { | |
final ClassLoader origContextClassLoader = Thread.currentThread().getContextClassLoader(); | |
try { | |
ClassReloader componentLoader = new ClassReloader(origContextClassLoader); | |
Thread.currentThread().setContextClassLoader(componentLoader); | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
final WeakReference<ClassLoader> ref = new WeakReference<>(componentLoader); | |
componentLoader = null; | |
forceGarbageCollection(ref); | |
assertNull(ref.get()); | |
} finally { | |
// Restore context classloader that was present before this | |
// test started. It is expected to be the same as the system | |
// classloader, but we handle all cases here.. | |
Thread.currentThread().setContextClassLoader(origContextClassLoader); | |
// and restore all the standard converters | |
ConvertUtils.deregister(); | |
} | |
} | |
} |