-
-
Save sehrope/3b4e11124f6e27d9e680 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*------------------------------------------------------------------------- | |
* | |
* 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()); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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 thecontextClassLoader
is set to the classloader of the web app that is being serviced, while borrowed from the thread pool. Since threads, inclTimer
threads, inherit thecontextClassLoader
from the thread that spawns them, it means there would be a strong reference chain from a GC root (theTimer
thread) to the web app classloader as long as that thread is alive - regardless of which classloader loaded the driver.