JDK-7146977 : finalize and PermGen and PhantomReference prevent GC inocrrectly
  • Type: Bug
  • Component: hotspot
  • Sub-Component: gc
  • Affected Version: 7
  • Priority: P4
  • Status: Resolved
  • Resolution: Fixed
  • OS: windows_7
  • CPU: x86
  • Submitted: 2012-02-19
  • Updated: 2016-01-08
  • Resolved: 2016-01-08
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 9
9Fixed
Related Reports
Relates :  
Relates :  
Description
FULL PRODUCT VERSION :
C:\Program Files\Java\jre7\bin>.\java -version
java version "1.7.0_03"
Java(TM) SE Runtime Environment (build 1.7.0_03-b05)
Java HotSpot(TM) 64-Bit Server VM (build 22.1-b02, mixed mode)


FULL OS VERSION :
MS Windows 6.1.7601
(with Jre 7u3)
also
Linux build.tqinc.info 2.6.38-11-server #48-Ubuntu SMP Fri Jul 29 19:20:32 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux
(with java 1.6.0_26)


EXTRA RELEVANT SYSTEM CONFIGURATION :
The problem does not exhibit on a 32 bit system with Windows and a less pwoerful (but still mutlithreaded - dual) processor

A DESCRIPTION OF THE PROBLEM :
The test case below sets up a class loader with some classes and objects, some with a finalize method, and nulls out all references to the whole structure and then repeatedly calls System.gc while holding a PhantomReference to one of the items to be collected.
If the code is invoked with argument 'ClassLoader' or 'Class" then all is good, and everything is collected after about 5 invocations of System.gc(). If it is 'Object' then gc fails to collect.

This test case was developed from a much more complicated situation with my product.

THE PROBLEM WAS REPRODUCIBLE WITH -Xint FLAG: Yes

THE PROBLEM WAS REPRODUCIBLE WITH -server FLAG: Yes

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
There are two source files below.
One (Main.java) is put in a directory with name memLeakTestDriver
The other (Test.java) is put in a directory with name memLeakTest

Note these names are hardcoded in the setting up of the new classloader:

for (URL u:parent.getURLs()) {
			String uu = u.toString();
			if (uu.contains("memLeakTestDriver")) {
				testCode = new URL(uu.replace("memLeakTestDriver","memLeakTest"));
			}
		}
		URLClassLoader child = URLClassLoader.newInstance(new URL[]{testCode}, parent);
		
====

Compile both classes in place, then run Main.java, e.g.
java -classpath memLeakTestDriver/bin driver.Main Object
so that the classpath substitution above works to access the Test code on the other classloader

====

If the argument to Main is 'ClassLoader' or 'Class' then the behavior is as expected, if it is 'Object' then the gc loop fails to clear the garbage.

EXPECTED VERSUS ACTUAL BEHAVIOR :
Expected:

Hello world!
Hello world 0
Hello world 1
Hello world 2
GC 0 3
GC 1 3
GC 2 3
Bye bye world 2
GC 3 3
GC 4 2
Bye bye world 1
GC 5 2
Bye bye world 0
GC 6 1

Actual:

Hello world!
Hello world 0
Hello world 1
Hello world 2
GC 0 3
Bye bye world 2
GC 1 3
GC 2 2
GC 3 2
GC 4 2
GC 5 2
GC 6 2
GC 7 2
GC 8 2
GC 9 2
GC 10 2
GC 11 2
GC 12 2
GC 13 2
GC 14 2
GC 15 2
GC 16 2
GC 17 2
GC 18 2
GC 19 2
GC 20 2
GC 21 2
GC 22 2
GC 23 2
GC 24 2
GC 25 2
GC 26 2
GC 27 2
GC 28 2
GC 29 2
GC 30 2
GC 31 2
GC 32 2
GC 33 2
GC 34 2
GC 35 2
GC 36 2
GC 37 2
GC 38 2
GC 39 2
GC 40 2
GC 41 2
GC 42 2
GC 43 2
GC 44 2
GC 45 2
GC 46 2
GC 47 2
GC 48 2
GC 49 2
REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------

/memLeakTestDriver/src/driver/Main.java

=====

package driver;

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class Main {

	private enum RefType {
		ClassLoader {
			@Override
			public java.lang.Object get(URLClassLoader child,
					java.lang.Class test, java.lang.Object o) {
				return child;
			}
		}, Class {
			@Override
			public java.lang.Object get(URLClassLoader child,
					java.lang.Class test, java.lang.Object o) {
				return test;
			}
		}, Object {
			@Override
			public java.lang.Object get(URLClassLoader child,
					java.lang.Class test, java.lang.Object o) {
				return o;
			}
		};
		abstract public Object get(URLClassLoader child, Class test, Object o);
	}

	public static void main(String[] args) throws Exception {
		RefType refType = null;
		try {
			refType = RefType.valueOf(args[0]);
			
		}
		catch (Exception e) {
			System.err.println("Expected argument: 'ClassLoader' or 'Class' or 'Object'");
			return;
		}
		

		
		URLClassLoader parent = (URLClassLoader) Main.class.getClassLoader();
		URL testCode = null;
		for (URL u:parent.getURLs()) {
			String uu = u.toString();
			if (uu.contains("memLeakTestDriver")) {
				testCode = new URL(uu.replace("memLeakTestDriver","memLeakTest"));
			}
		}
		URLClassLoader child = URLClassLoader.newInstance(new URL[]{testCode}, parent);
		Class test = child.loadClass("test.Test");
		Object o = test.newInstance();
		Map<ClassLoader,Reference<Class>> weak = new WeakHashMap<ClassLoader,Reference<Class>>();
		weak.put(child, new WeakReference(test));

		o = refType.get(child, test, o);
		
		ReferenceQueue queue = new ReferenceQueue();
		PhantomReference ref = new PhantomReference(
				o, // can be changed to 'test' or 'child' and problem goes away
				queue);
		AtomicInteger problemCount = (AtomicInteger) test.getDeclaredMethod("getProblemCount").invoke(null);
		o = null;
		test = null;
		child = null;
		int cnt = 0;
		while (problemCount.get() != 0 && cnt < 50 ) {
			//	queue.poll()==null && cnt < 30) {
			System.gc();
			System.err.println("GC "+cnt+" "+problemCount.get());
			cnt++;
		}
		ref.toString();
		
	}

}


============================
/memLeakTest/src/test/Test.java
============================
package test;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

public class Test {
	
    static Object problem;
    
    static public AtomicInteger getProblemCount() {
    	return Problem.aliveCount;
    }
	
	public Test()
	{
		System.err.println("Hello world!");
		problem = new Problem();
		new Problem();
	}

}

class Problem {
	static AtomicInteger aliveCount = new AtomicInteger(0);
	
	static Problem test = new Problem();
	final int id = aliveCount.getAndIncrement();
	
	public Problem()  {
		System.err.println("Hello world "+id);
	}

	@Override
	protected void finalize() {
		System.err.println("Bye bye world "+id);
		aliveCount.decrementAndGet();
	}
}


---------- END SOURCE ----------

CUSTOMER SUBMITTED WORKAROUND :
Manually null out some of the suspect references

Comments
JDK-8071507 changed phantom references to be cleared when notified, so the phantom reference that was preventing the expected reclaim will no longer do so.
08-01-2016

If any changes are needed for this issue it should probably be done in conjunction with the "intuitive reference processing" work for JDK-6990442.
06-05-2013