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