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_AREAjdk6_34hotspotsrcsharevmprimsjni.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:

    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 (ie, 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 ).


    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.


    Please excuse me if this is totally off, but I want to check on two fundamentals that I can't discern from a casual reading of your code.

    1) How long are your sound files? Given a specific number of files, lengths in milliseconds, sample rate and encoding (eg, 16-bit, stereo) you should be able to calculate the expected amount of memory to be consumed. What is that amount?

    2) A really common error with Clips is to recreate them every time you play them instead of reusing existing Clips. I see in the comments the line: "sounds.get("zap.wav").start()" and that makes me wonder if you are making this fundamental error. You should only make a Clip once, and then reset the frame position to 0 when you wish to play it again. If you are recreating Clips at a furious rate, you WILL fill up memory very quickly, as each playback will create an additional object with its own copy of the PCM data.

    Also, as one commenter states, it IS important to close the various streams. Not doing so will cause memory leaks.


    Another approach: abandon the use of Java's Clip and write your own. I did this. I made an object that stores the data in memory and two other objects that provide "cursors" into the storage for two different type of playback. One is for looping and the other is for overlapping playback. Both can be set to run at different playback speeds, so you can get different effects by speeding up or slowing down the playback. Both direct their output to a mixer I wrote, where the data is consolidated into a single SourceDataLine.

    The code is presented here, included in the jar linked in the first post: http://www.java-gaming.org/topics/simple-audio-mixer-2nd-pass/27943/view.html

    I'm looking forward to getting back to working on it some more, possibly putting it on GitHub.

    Also, TinySound is a very capable sound manager. You can learn about it here. The approach is very similar to what I do, mixing down to a single output. TinySound provides support for Ogg and such. I don't think it provides vari-speed playback.

    http://www.java-gaming.org/topics/need-a-really-simple-library-for-playing-sounds-and-music-try-tinysound/25974/view.html

    链接地址: http://www.djcxy.com/p/12626.html

    上一篇: 在java8中找不到java.util.stream

    下一篇: 弱引用和`OutOfMemoryError`s