Java – softreferences and weakreferences / outofmemoryerror
There was a problem processing soft and weak references The code has a flag that can be switched
The code is a little big, but I try to keep the problem as small as possible I left a lot of comments on it, 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),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,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,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(); } }
Solution
First, don't mix references with terminators Both affect the speed at which objects are deleted from memory, and using the appropriate reference type can do everything the finalizer can do better
Second, as I mentioned, there may be GC delays associated with using references At least for "common" GC algorithms, weakly / soft referenced objects may require additional GC runs before they are fully recycled The essential difference between weak references and soft references is that weak references are positive GC, while soft references are usually "as long as possible" It's probably what makes you happy
When you run with weakly referenced objects, things will be cleaned up over time, allowing you to avoid using oome
When you run with soft referenced objects, all soft referenced objects will remain until you approach the limit Then, when memory becomes tight, GC will try to start releasing soft referenced objects, but this will take too long (because it may need multiple GC passes to completely reclaim memory) and eventually get an oome
I only have a superficial understanding of G1 GC, so I don't know why it "works" in that scene
All in all, soft references are good, but they are not always as good as you want because of recycling delays In addition, this is a great article, and there are some additional useful details