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 }