If you, for example, try running the SmokeParticles sample from our public demo collection (http://javafx.com/samples/SmokeParticles/index.html) and let it sit
in the browser for a while (you're reading the article), it will bring
your system to its knees (or just keep burning all the spare CPU cycles,
depending on how good your OS scheduler is).
The problem is a memory leak in the JNLP infrastructure, namely in MemoryCache,
together with this innocent and simple pattern used in the demo:
return ImageView {
image : Image { url: "{__DIR__}/../resources/texture.png" }
};
that is, creating (and properly freeing) many ImageViews from the same resource.
The MemoryCache thing is unnecessarily complicated, it seems. It implements
a reference counting scheme, trying to be smarter than the garbage collector
and failing at it. Each call to getLoadedResource with the same (in fact equal,
not the same) String leaks a CachedResourceReference and the mentioned String.
(Please note that each time a new String instance is used, as it is constructed
along the code path):
com.sun.deploy.cache.MemoryCache.getLoadedResource(MemoryCache.java:78)
com.sun.deploy.net.DownloadEngine.getResourceCacheEntry(DownloadEngine.java:1483)
com.sun.deploy.net.DownloadEngine.getResourceCacheEntry(DownloadEngine.java:1436)
com.sun.deploy.net.DownloadEngine.getCachedJarFile(DownloadEngine.java:457)
com.sun.deploy.net.DownloadEngine.getCachedJarFile(DownloadEngine.java:434)
com.sun.jnlp.JNLPClassLoader$3.run(JNLPClassLoader.java:434)
java.security.AccessController.doPrivileged
com.sun.jnlp.JNLPClassLoader.getJarFile(JNLPClassLoader.java:430)
com.sun.jnlp.JNLPCachedJarURLConnection.connect(JNLPCachedJarURLConnection.java:160)
com.sun.jnlp.JNLPCachedJarURLConnection.getInputStream(JNLPCachedJarURLConnection.java:203)
java.net.URL.openStream
com.sun.javafx.scene.image.ImageLoader.readImage
com.sun.javafx.scene.image.ImageLoader.<init>
javafx.scene.image.Image.initialize$impl
javafx.scene.image.Image.initialize
javafx.scene.image.Image.postInit$
javafx.scene.image.Image.initialize$
particles.Particle.create$impl(Particle.fx:33)
particles.Particle.create(Particle.fx:20)
[...]
I have a dump from the point where the GC was scraping for the last free bits
out of 64MB default heap. Bzipped it is 8.5MB.
But it boils down to (for single resource):
MemoryCache.loadedResource map having an entry (LoadedResourceReference)
strong*1 holding the object (CacheEntry) and a big set of WeakReferences
(CachedResourceReferences) to the same CacheEntry.
MemoryCache was probably intended to remove the entry from the map
once all the WeakReferences die, but:
1. that would never happen (because of *1)
2. all would die at once anyway, no need to have 200.000 of them.
Simplified testcase:
import com.sun.deploy.cache.MemoryCache;
public class Test {
public static void main(String[] args) throws Exception {
String u = "http://localhost/";
Object o = new int[100000]; //make it large to detect object leak
Object junk;
System.out.println("Total Memory"+Runtime.getRuntime().totalMemory());
MemoryCache.addLoadedResource(u, o);
for(int i=0; i<1000000; i++) {
junk = MemoryCache.getLoadedResource(u);
}
System.err.println("Done");
}
}