Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Weak references and `OutOfMemoryError`s

I have a SoundManager class for easy sound management. Essentially:

public class SoundManager {
    public static class Sound {
        private Clip clip; // for internal use

        public void stop() {...}
        public void start() {...}
        public void volume(float) {...}
        // etc.
    }

    public Sound get(String filename) {
        // Gets a Sound for the given clip
    }

    // moar stuff
}

Most of the uses of this are as follows:

sounds.get("zap.wav").start();

As I understand it, this should not keep a reference to the newly created sound in memory, and it should be garbage collected fairly rapidly. However, with a short sound file (108 KB, clocking in at a whopping 00:00:00 seconds, and about 0.8s in reality), I can only get to about 2100 invocations before I get an OutOfMemoryError:

# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (malloc) failed to allocate 3874172 bytes for jbyte in C:\BUILD_AREA\jdk6_34\hotspot\src\share\vm\prims\jni.cpp
# An error report file with more information is saved as:
# [path]

I tried implementing a private static final Vector<WeakReference<Sound>> in the SoundManager.Sound class, adding the following to the constructor:

// Add to the sound list.
allSounds.add(new WeakReference<SoundManager.Sound>(this));
System.out.println(allSounds.size());

This also allows me to iterate through at the end of the program and stop all sounds (in an applet, this is not always done automatically).

However, I still only get about 10 more invocations before the same OutOfMemoryError occurs.

If it matters, for each file name, I'm caching the file contents as a byte[], but this is only done once per file so it shouldn't accumulate.

So why are these references being retained, and how can I stop it without just increasing the heap size?


EDIT: The "error report with more information" contains, at line 32:

Java frames: (J=compiled Java code, j=interpreted, Vv=VM code)
J  com.sun.media.sound.DirectAudioDevice.nWrite(J[BIIIFF)I
J  com.sun.media.sound.DirectAudioDevice$DirectDL.write([BII)I
j  com.sun.media.sound.DirectAudioDevice$DirectClip.run()V+163
j  java.lang.Thread.run()V+11
v  ~StubRoutines::call_stub

Does this mean that this issue is completely out of my control? Does javasound need time to "cool down"? I'm spewing these sounds out at 300/second, for debugging purposes.


EDIT with more info on my use of JavaSound.

The first time I call sounds.get("zap.wav"), it sees that "zap.wav" has not been loaded before. It writes the file to a byte[] and stores it. It then proceeds as if it had been cached before.

The first and all subsequent times (after caching), the method takes the byte[] stored in memory, makes a new ByteArrayInputStream, and uses AudioSystem.getAudioInputStream(bais) on said stream. Could it be these streams holding memory? I would think that when the Sound (and thus the Clip) is collected, the stream would be closed also.


EDIT with the get method per request. This is public Sound get(String name).

  • byteCache is a HashMap<String, byte[]>
  • clazz is a Class<?>

byteCache is a HashMap<String, byte[]> and clazz is a Class<?>

try {
    // Create a clip.
    Clip clip = AudioSystem.getClip();

    // Find the full name.
    final String fullPath = prefix + name;

    // See what we have already.
    byte[] theseBytes = byteCache.get(fullPath);

    // Have we found the bytes yet?
    if (theseBytes == null) {
        // Nope. Read it in.
        InputStream is = clazz.getResourceAsStream(fullPath);

        // Credit for this goes to Evgeniy Dorofeev:
        // http://stackoverflow.com/a/15725969/732016

        // Output to a temporary stream.
        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        // Loop.
        for (int b; (b = is.read()) != -1;) {
            // Write it.
            baos.write(b);
        }

        // Close the input stream now.
        is.close();

        // Create a byte array.
        theseBytes = baos.toByteArray();

        // Put in map for later reference.
        byteCache.put(fullPath, theseBytes);
    }

    // Get a BAIS.
    ByteArrayInputStream bais = new ByteArrayInputStream(theseBytes);

    // Convert to an audio stream.
    AudioInputStream ais = AudioSystem.getAudioInputStream(bais);

    // Open the clip.
    clip.open(ais);

    // Create a new Sound and return it.
    return new Sound(clip);
} catch (Exception e) {
    // If they're watching, let them know.
    e.printStackTrace();

    // Nothing to do here.
    return null;
}

EDIT after heap profiling.

Heapdumped about 5 seconds before crash. Well this is telling:

heap dump chart

Problem Suspect #1:

2,062 instances of "com.sun.media.sound.DirectAudioDevice$DirectClip", loaded by "" occupy 230,207,264 (93.19%) bytes.

Keywords com.sun.media.sound.DirectAudioDevice$DirectClip

These Clip objects are strongly referenced by Sound objects but the Sound objects are only weakly referenced in a Vector<WeakReference<Sound>>.

I can also see that each Clip object contains a copy of the byte[].


EDIT per Phil's comments:

I've changed this:

// Convert to an audio stream.
AudioInputStream ais = AudioSystem.getAudioInputStream(bais);

// Open the clip.
clip.open(ais);

to this:

// Convert to an audio stream.
AudioInputStream ais = AudioSystem.getAudioInputStream(bais);

// Close the stream to prevent a memory leak.
ais.close();

// Open the clip.
clip.open(ais);
clip.close();

This fixes the error but never plays any sound.

If I omit clip.close() the error still occurs. If I move ais.close() to after clip.open the error still occurs.

I've also tried adding a LineListener to the clip as it's created:

@Override
public void update(LineEvent le) {
    if (le.getType() == LineEvent.Type.STOP) {
        if (le.getLine() instanceof Clip) {
            System.out.println("draining");
            ((Clip)le.getLine()).drain();
        }
    }
}

I get a "draining" message each time the clip finishes or is stopped (i.e., 30+ times/second after it starts to happen), but still get the same error. Replacing drain with flush has no effect either. Using close makes the line unopenable later on (even when listening for START and calling open and start).

like image 467
wchargin Avatar asked Apr 01 '13 03:04

wchargin


1 Answers

I suspect that the problem is that you are not explicitly closing the audio streams. You should not rely on the garbage collector to close them.

The allocations seem to be failing in native allocations not in normal Java allocations, and I suspect that the normal behaviour of "the GC runs before throwing an OOME" applies in this case.

Either way, it is best practice to close your streams explicitly (using finally or the Java 7 try with resources). This applies to any kind of stream that involves external resources or off-heap memory buffers.

like image 200
Stephen C Avatar answered Oct 23 '22 21:10

Stephen C