Skip to content

Instantly share code, notes, and snippets.

@sehrope
Created December 4, 2014 08:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sehrope/3b4e11124f6e27d9e680 to your computer and use it in GitHub Desktop.
Save sehrope/3b4e11124f6e27d9e680 to your computer and use it in GitHub Desktop.
/*-------------------------------------------------------------------------
*
* Copyright (c) 2004-2014, PostgreSQL Global Development Group
*
*
*-------------------------------------------------------------------------
*/
package org.postgresql.test.leak;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashSet;
import java.util.Set;
import java.util.Properties;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.postgresql.test.TestUtil;
import org.junit.Test;
import org.junit.Assert;
public class ClassLoaderLeakTest {
private static class DriverIsolatingClassLoader extends URLClassLoader {
private static URL artifactJar() {
// this assumes <sysproperty key="driverArtifact" value="${artifact.jar}"/> in the <junit> definition in build.xml
String jar = System.getProperty("driverArtifact");
try {
return new URL("file:./" + jar);
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(e);
}
}
public DriverIsolatingClassLoader() {
super(new URL[] {artifactJar()});
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if( c != null ) {
return c;
}
if (name.startsWith("org.postgresql")) {
Class<?> cls = super.findClass(name);
if (resolve) {
super.resolveClass(cls);
}
return cls;
} else {
return super.loadClass(name, resolve);
}
}
}
/**
* Repeatedly execute System.gc() till it actually runs.
*/
public static void gc() {
Object obj = new Object();
WeakReference ref = new WeakReference<Object>(obj);
obj = null;
int count = 0;
while(ref.get() != null) {
System.out.println("Running gc # " + count++);
System.gc();
}
}
public static void printRegisteredDrivers() {
java.util.Enumeration<Driver> drivers = DriverManager.getDrivers();
System.out.println("Registered drivers:");
while( drivers.hasMoreElements() ) {
Driver driver = drivers.nextElement();
System.out.println(" Driver class: " + driver.getClass().toString() + " --> " + driver);
}
}
//@Test
//public void permgenRemainsStableAfterDriverReloaded() throws Exception {
public static void main(String args[]) throws Exception {
printRegisteredDrivers();
Set<WeakReference<Driver>> driverRefs = new HashSet<WeakReference<Driver>>();
Set<WeakReference<Class<?>>> driverClassRefs = new HashSet<WeakReference<Class<?>>>();
// How many times should we load/unload the driver:
int numDriverLoads = 10;
// How long should we wait before cancelling the statement:
final long cancellationSleepMillis = 250;
String jdbcURL = TestUtil.getURL();
Properties props = new Properties();
props.setProperty("user", TestUtil.getUser());
props.setProperty("password", TestUtil.getPassword());
for (int i = 0; i < numDriverLoads; ++i)
{
System.out.println("Iteration " + i + " of " + numDriverLoads);
Class<Driver> driverClass = (Class<Driver>) Class.forName("org.postgresql.Driver", true, new DriverIsolatingClassLoader());
Driver driver = driverClass.newInstance();
driverRefs.add(new WeakReference<Driver>(driver));
driverClassRefs.add(new WeakReference<Class<?>>(driverClass));
// Actually use the connection a bit:
Connection conn = null;
Statement stmt = null;
try {
conn = driver.connect(jdbcURL, props);
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT 1");
rs.next();
rs.getInt(1);
rs.close();
System.out.println("Successfully tested query");
// Create final copy so that Thread can see it:
final Statement stmt2 = stmt;
new Thread() {
public void run() {
try {
Thread.sleep(cancellationSleepMillis);
System.out.println("Cancelling query");
stmt2.cancel();
} catch( Exception ignore ) {
}
}
}.start();
System.out.println("Executing test query");
stmt.executeQuery("SELECT pg_sleep(10)");
} catch( SQLException e ) {
e.printStackTrace();
// Ignore the statement cancellation exception
} finally {
System.out.println("Closing connection");
TestUtil.closeQuietly(stmt);
TestUtil.closeQuietly(conn);
}
System.out.println("Before deregister:");
printRegisteredDrivers();
// Deregister the driver so it gets unloaded:
DriverManager.deregisterDriver(driver);
System.out.println("After deregister:");
printRegisteredDrivers();
}
printRegisteredDrivers();
for(java.util.Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
Thread thread = entry.getKey();
StackTraceElement stackTraceElements[] = entry.getValue();
System.out.println("Thread: " + thread.getName());
System.out.println(" Stack Trace: ");
for(StackTraceElement line : stackTraceElements) {
System.out.println(" " + line.toString());
}
}
gc();
// check how many driver refs survived...
// note that the leak would occur if the loading of the driver left some "dangling" thread running.
// The threads keep a reference to the protection domain they're running with and a protection domain keeps a
// a reference to the class loader. The org.postgresql.Driver class had a static cancelTimer Timer field, that
// created a thread that was never stopped. This thread was created from the classloader that loaded the driver
// class.
// In a containerized environment (web containers, etc.) this is a problem leading to eventual depletion of
// permgen due to high number of classes being loaded (and never unloaded again).
for (WeakReference<Driver> ref : driverRefs) {
Assert.assertNull("Driver references must not survive GC", ref.get());
}
for (WeakReference<Class<?>> ref : driverClassRefs) {
Assert.assertNull("Driver class references must not survive GC", ref.get());
}
}
}
@metlos
Copy link

metlos commented Dec 4, 2014

I_think_ (but cannot claim I know ;) ) the problem with the test is that in gc() you merely create a single object and wait for it to be reclaimed.

You're waiting for the object to be expunged from the young generation of the heap, while the classes live in the "older" parts of the heap that might not ever be touched unless you really stress GC to get rid of as many unneeded objects as possible.

That's why I'm creating a huge number of objects very rapidly on https://gist.github.com/metlos/59c9cf891482f14d1784#file-classloaderleaktest-java-L82. I'm hoping there that that action will put enough stress on the memory that the garbage collector will have to clear all generations of objects. Only then I check if the classes disappeared.

@metlos
Copy link

metlos commented Dec 4, 2014

If you don't like the rapid object creation approach, you might try for example creating a byte array the size of 90% of the heap. That will either throw OOM, upon which you may try to reduce the size a little bit and retry, or will force GC to clear up the heap (or at least I think it should).

@mjiderhamn
Copy link

There is one aspect missing from this test case, which possibly makes it not properly reflect the scenario which it sets out to test: The contextClassLoader of the thread interacting with the connections/statements in never changed. In a web application the contextClassLoader is set to the classloader of the web app that is being serviced, while borrowed from the thread pool. Since threads, incl Timer threads, inherit the contextClassLoader from the thread that spawns them, it means there would be a strong reference chain from a GC root (the Timer thread) to the web app classloader as long as that thread is alive - regardless of which classloader loaded the driver.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment