SoftReferences vs Weakreferences / OutOfMemoryError

I have a problem with soft and weak links. The code has a flag that switches all the logic between soft and weak links. And although with weak links everything seems to work fine, with soft links I constantly get OutOfMemoryError. This happens with JDK7 and JDK6 on MacOSX, as well as IcedTea6 on Debian. However, JDK7 with the G1 collector is a setting that I found working with soft links, everything I tried (serial / parallel GC, client / server, etc.) is an exception.

The code is a bit big, but I tried to narrow it down as much as possible while keeping the problem.
I left a massive comment at the top, describing in more detail how to reproduce the problem.

/* * * Leakling.java * * * Issue: * * * This code throws OutOfMemoryError when using soft references, whereas weak references * work ok. Moreover, with JDK7 G1 garbage collector soft references work as well. Other * collectors seem to fail. Code was tested with MacOSX 10.8.2 JDKs 1.7.0_10-b18 and * 1.6.0_37-b06-434-11M3909, with Debian 6.0 IcedTea6 1.8.13. * Various command line options including -Xmx, -client/-server, -XX:+UseParallelOldGC, * -XX:+UseSerialGC were tested. * * * Examples: * * * 1. Default options, weak references, this works, counters go up and down, * but everything keeps going just as expected: * * java -Xmx50m Leakling \ * --loop-delay=10 --min-chunk-size=1000 --max-chunk-size=100000 --use-soft-references=false * * * 2. Default options, soft references, this eventually throws the exception: * * java -Xmx50m Leakling \ * --loop-delay=10 --min-chunk-size=1000 --max-chunk-size=100000 --use-soft-references=true * * * 3. G1 collector (IcedTea6 doesn't support it), weak references, this works, but it did anyway: * * java -XX:+UseG1GC -Xmx50m Leakling \ * --loop-delay=10 --min-chunk-size=1000 --max-chunk-size=100000 --use-soft-references=false * * * 4. G1 collector, soft references, this works with JDK7. * JDK6 seems to just stop after having hit memory limit (with no message whatsoever). * * java -XX:+UseG1GC -Xmx50m Leakling \ * --loop-delay=10 --min-chunk-size=1000 --max-chunk-size=100000 --use-soft-references=true * * * jarek, 02.01.2013 * * */ import java.lang.ref.*; import java.util.*; public class Leakling { private static final String TAG = "memory-chunk-"; class Chunk { final String name; final int size; final private byte[] mem; Chunk(String name, int minSize, int maxSize, Random randomizer) { int currSize = minSize; if (maxSize > minSize) { currSize += randomizer.nextInt(maxSize - minSize + 1); } this.size = currSize; this.mem = new byte[currSize]; this.name = name; log(this + " has been created (" + currSize + " bytes)"); } @Override public void finalize() throws Throwable { log(this + " is finalizing"); } @Override public String toString() { return name + " of " + getReadableMemorySize(size); } } class WeakChunk extends WeakReference<Chunk> { final String name; public WeakChunk(Chunk chunk, ReferenceQueue<Chunk> queue) { super(chunk, queue); this.name = chunk.name; } @Override public String toString() { return "weak reference of " + name + " is " + ((get() == null) ? "null" : "alive"); } } class SoftChunk extends SoftReference<Chunk> { final String name; public SoftChunk(Chunk chunk, ReferenceQueue<Chunk> queue) { super(chunk, queue); this.name = chunk.name; } @Override public String toString() { return "soft reference of " + name + " is " + ((get() == null) ? "null" : "alive"); } } // Logging as implemented here gives extra timing info (secs.milis starting from the initialization). private final long start = System.currentTimeMillis(); private final Formatter formatter = new Formatter(System.err); private final String formatString = "%1$d.%2$03d %3$s\n"; // I found this be better synchronized... synchronized void log(Object o) { long curr = System.currentTimeMillis(); long diff = curr - start; formatter.format(formatString, (int) (diff / 1000), (int) (diff % 1000), o.toString()); } private final ArrayList<Chunk> allChunks = new ArrayList<Chunk>(); private final ReferenceQueue<Chunk> softReferences = new ReferenceQueue<Chunk>(); private final ReferenceQueue<Chunk> weakReferences = new ReferenceQueue<Chunk>(); private final HashSet<Reference<Chunk>> allReferences = new HashSet<Reference<Chunk>>(); private final Random randomizer = new Random(); private int loopDelay = 200; private int minChunkSize = 100; private int maxChunkSize = 1000; private int chunkCounter = 0; private boolean useSoftReferences = false; private long minMemory = 10 * 1024 * 1024; // Default range is 10-30MB private long maxMemory = 3 * minMemory; private long usedMemory = 0; private String getReadableMemorySize(long size) { if (size >= 1024 * 1024) { return (float) (Math.round((((float) size) / 1024f / 1024f) * 10f)) / 10f + "MB"; } if (size >= 1024) { return (float) (Math.round((((float) size) / 1024f) * 10f)) / 10f + "kB"; } else if (size > 0) { return size + "B"; } else { return "0"; } } private void allocMem() { Chunk chunk = new Chunk(TAG + chunkCounter++, minChunkSize, maxChunkSize, randomizer); allChunks.add(chunk); Reference ref = useSoftReferences ? (new SoftChunk(chunk, softReferences)) : (new WeakChunk(chunk, weakReferences)); allReferences.add(ref); log(ref); usedMemory += chunk.size; } private void freeMem() { if (allChunks.size() < 1) { return; } int i = randomizer.nextInt(allChunks.size()); Chunk chunk = allChunks.get(i); log("freeing " + chunk); usedMemory -= chunk.size; allChunks.remove(i); } private int statMem() throws Exception { for (Reference ref; (ref = softReferences.poll()) != null;) { log(ref); allReferences.remove(ref); } for (Reference ref; (ref = weakReferences.poll()) != null;) { log(ref); allReferences.remove(ref); } int weakRefs = 0; int softRefs = 0; for (Iterator<Reference<Chunk>> i = allReferences.iterator(); i.hasNext();) { Reference<Chunk> ref = i.next(); if (ref.get() == null) { continue; } if (ref instanceof WeakChunk) { weakRefs++; } if (ref instanceof SoftChunk) { softRefs++; } } log(allChunks.size() + " chunks, " + softRefs + " soft refs, " + weakRefs + " weak refs, " + getReadableMemorySize(usedMemory) + " used, " + getReadableMemorySize(Runtime.getRuntime().freeMemory()) + " free, " + getReadableMemorySize(Runtime.getRuntime().totalMemory()) + " total, " + getReadableMemorySize(Runtime.getRuntime().maxMemory()) + " max"); if (loopDelay > 1) { Thread.sleep(loopDelay); } return (int)((100 * usedMemory) / maxMemory); // Return % of maxMemory being used. } public Leakling(String[] args) throws Exception { for (String arg : args) { if (arg.startsWith("--min-memory=")) { minMemory = Long.parseLong(arg.substring("--min-memory=".length())); } else if (arg.startsWith("--max-memory=")) { maxMemory = Long.parseLong(arg.substring("--max-memory=".length())); } else if (arg.startsWith("--min-chunk-size=")) { minChunkSize = Integer.parseInt(arg.substring("--min-chunk-size=".length())); } else if (arg.startsWith("--max-chunk-size=")) { maxChunkSize = Integer.parseInt(arg.substring("--max-chunk-size=".length())); } else if (arg.startsWith("--loop-delay=")) { loopDelay = Integer.parseInt(arg.substring("--loop-delay=".length())); } else if (arg.startsWith("--use-soft-references=")) { useSoftReferences = Boolean.parseBoolean(arg.substring("--use-soft-references=".length())); } else { throw new Exception("Unknown command line option..."); } } } public void run() throws Exception { log("Mem test started..."); while(true) { log("going up..."); do {// First loop allocates memory up to the given limit in a pseudo-random fashion. // Randomized rate of allocations/frees is about 4:1 as per the 10>=8 condition. if (randomizer.nextInt(10) >= 8) { freeMem(); } else { allocMem(); } } while (statMem() < 90); // Repeat until 90% of the given mem limit is hit... log("going down..."); do {// Now do the reverse. Frees are four times more likely than allocations are. if (randomizer.nextInt(10) < 8) { freeMem(); } else { allocMem(); } } while (usedMemory > minMemory); } } public static void main(String[] args) throws Exception { (new Leakling(args)).run(); } } 
+6
source share
2 answers

First of all, do not mix finalizers with links. both affect how quickly an object can be deleted from memory, and everything you can do with the finalizer can be improved with the appropriate link type.

Secondly, as I already mentioned, there may be a gc delay associated with the use of links. At least for “general” gc algorithms, low / soft-bound objects can go through an extra gc loop before they are fully recovered. The significant differences between weak and soft links are that weak links are aggressive and soft links are usually held "as long as possible." this is probably what bothers you.

when you run objects with weak links, the material is cleared when you go, avoiding OOME.

when you start soft-snapped objects, all soft-snapped objects are retained until you get close to the limit. then, when the memory becomes hard, gc tries to start releasing objects with soft binding, but takes too much time (since it may take several gc passes to completely restore the memory), and you get OOME.

I have only a superficial knowledge of G1 gc, so I don’t know why it "works" in this scenaior.

In general, soft links look good, but they do not always work as good as you would like due to a delay in land reclamation. Also, this is a great article with some additional helpful details.

+6
source

Get rid of the finalizer.

The objects are finalized in a separate thread, and the memory cannot really be fixed until completion is complete. In the finalizer, you make a system call (exit) that introduces the wait into this thread. It is very simple, when you are within memory, for any finalizer to wait to call OOM.

As for the soft and weak differences: we refer to minor collections, while soft links will not (I did not check this, perhaps the flag that controls the lifetime of the soft link will just allow it to live across several small collections). It is very likely that your finalizer thread may keep up with dropped objects with a low reference.

+4
source

All Articles