blob: 15e8480ad2a428a706aea63ab5ff4e18bdcdb8e6 [file] [log] [blame]
/*
* 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 {
@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();
}
}
/**
* 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 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();
}
}
/**
* 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();
}
}