弱引用和`OutOfMemoryError`s

wch*_*gin 7 java garbage-collection weak-references out-of-memory javasound

我有一个SoundManager简单的声音管理课程.实质上:

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
}
Run Code Online (Sandbox Code Playgroud)

其中大部分用途如下:

sounds.get("zap.wav").start();
Run Code Online (Sandbox Code Playgroud)

据我所知,这应该不是保持在内存中新创建的声音的引用,它应该被垃圾收集相当迅速.但是,使用一个简短的声音文件(108 KB,高达00:00:00秒,实际上大约0.8秒),我得到一个之前我只能进行大约2100次调用OutOfMemoryError:

#Java Runtime Environment没有足够的内存来继续.
#本机内存分配(malloc)无法在C:\ BUILD_AREA\jdk6_34\hotspot\src\share\_vm\prims\_jni.cpp中为jbyte分配3874172个字节
#包含更多信息的错误报告文件保存为:
#[path ]

我尝试private static final Vector<WeakReference<Sound>>SoundManager.Sound类中实现a ,将以下内容添加到构造函数中:

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

这也允许我在程序结束时迭代并停止所有声音(在applet中,这并不总是自动完成).

但是,在相同的OutOfMemoryError情况发生之前,我仍然只能再调用10次以上.

如果重要的是,对于每个文件名,我将文件内容缓存为a byte[],但每个文件只执行一次,因此不应累积.

那么为什么要保留这些引用,如何在不增加堆大小的情况下停止它?


编辑:第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
Run Code Online (Sandbox Code Playgroud)

这是否意味着这个问题完全不受我的控制?javasound需要时间"冷静下来"吗?出于调试目的,我以300 /秒的速度喷出这些声音.


编辑有关我使用JavaSound的更多信息.

我第一次打电话sounds.get("zap.wav"),它看到之前没有加载"zap.wav".它将文件写入a byte[]并存储它.然后它就像之前被缓存一样进行.

第一次和所有后续时间(在缓存之后),该方法将byte[]存储在内存中,创建一个新的ByteArrayInputStream,并AudioSystem.getAudioInputStream(bais)在所述流上使用.难道这些流可以记忆吗?我认为当收集Sound(以及因此Clip)时,流也将被关闭.


使用get每个请求的方法编辑.这是public Sound get(String name).

  • byteCache 是一个 HashMap<String, byte[]>
  • clazz 是一个 Class<?>

byteCache是一个HashMap<String, byte[]>,clazz是一个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;
}
Run Code Online (Sandbox Code Playgroud)

堆分析后编辑.

在撞车之前大约5秒钟.这是在说:

堆转储图表

问题可疑#1:

"com.sun.media.sound.DirectAudioDevice $ DirectClip"的2,062个实例,由""加载占用230,207,264(93.19%)个字节.

关键词com.sun.media.sound.DirectAudioDevice $ DirectClip

这些Clip对象被对象强烈引用,SoundSound对象仅在a中被弱引用Vector<WeakReference<Sound>>.

我还可以看到每个Clip对象都包含一个副本byte[].


编辑每菲尔的评论:

我改变了这个:

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

// Open the clip.
clip.open(ais);
Run Code Online (Sandbox Code Playgroud)

对此:

// 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();
Run Code Online (Sandbox Code Playgroud)

这可以修复错误但从不播放任何声音.

如果我省略clip.close()错误仍然发生.如果我仍然发生错误ais.close()后移动到clip.open.

我也尝试LineListener在剪辑创建时添加一个:

@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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

每次剪辑完成或停止时(即,开始发生后30次/秒),我会收到"消耗"消息,但仍会得到相同的错误.更换drainflush没有影响无论是.使用close会使线路在以后不可打开(即使在收听START和呼叫时open也是如此start).

Ste*_*n C 2

我怀疑问题是您没有明确关闭音频流。您不应该依赖垃圾收集器来关闭它们。

分配似乎在本机分配中失败,而不是在正常的 Java 分配中失败,并且我怀疑“GC 在抛出 OOME 之前运行”的正常行为适用于这种情况。

无论哪种方式,最佳实践是显式关闭流(使用或带有资源的finallyJava 7 )。try这适用于涉及外部资源或堆外内存缓冲区的任何类型的流。