弱引用和`OutOfMemoryError`s

时间:2013-04-01 03:26:33

标签: java garbage-collection out-of-memory javasound weak-references

我有一个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
}

其中大多数用途如下:

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

据我了解,这应该在内存中保留对新创建的声音的引用,并且应该相当快速地进行垃圾收集。但是,使用一个简短的声音文件(108 KB,高达00:00:00秒,实际上约为0.8秒),在获得OutOfMemoryError之前,我只能进行大约2100次调用:< / p>

  

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

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

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

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

但是,在同一OutOfMemoryError发生之前,我仍然只会再调用10次以上。

如果重要的话,对于每个文件名,我将文件内容缓存为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

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


编辑,了解有关使用JavaSound的更多信息。

我第一次拨打sounds.get("zap.wav")时,发现之前没有加载“zap.wav”。它将文件写入byte[]并存储它。然后它就像之前被缓存一样继续进行。

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


每次请求使用get方法

编辑。这是public Sound get(String name)

  • byteCacheHashMap<String, byte[]>
  • clazzClass<?>

byteCacheHashMap<String, byte[]>clazzClass<?>

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;
}

堆分析后

编辑

在撞车前大约5秒钟。这说得好:

heap dump chart

问题嫌疑人#1:

  

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

     

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

这些Clip个对象被Sound个对象强烈引用,但Sound个对象仅在Vector<WeakReference<Sound>>中被弱引用。

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


根据菲尔的评论

编辑

我改变了这个:

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

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

到此:

// 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();

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

如果我省略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();
        }
    }
}

每次剪辑完成或停止时(即,开始发生后30次/秒),我都会收到“耗尽”消息,但仍会出现相同的错误。用drain替换flush也没有效果。使用close会使该行在以后无法打开(即使在收听START并呼叫openstart时)。

3 个答案:

答案 0 :(得分:2)

我怀疑问题在于您显式关闭音频流。你不应该依赖垃圾收集器来关闭它们。

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

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

答案 1 :(得分:1)

如果完全关闭,请原谅我,但我想查看两个基本原则,我无法通过随意阅读您的代码来辨别。

1)你的声音文件有多长?给定特定数量的文件,以毫秒为单位的长度,采样率和编码(例如,16位,立体声),您应该能够计算要消耗的预期内存量。那金额是多少?

2)Clips的一个非常常见的错误是每次播放它们时重新创建它们而不是重用现有的剪辑。我在评论中看到了这句话:“sounds.get(”zap.wav“)。start()”这让我想知道你是否犯了这个基本错误。您应该只制作一次剪辑,然后在想要再次播放时将帧位置重置为0。如果您以极快的​​速度重新创建剪辑,则会很快填满内存,因为每次播放都会创建一个带有自己的PCM数据副本的附加对象。

另外,正如一位评论者所说,关闭各种流是很重要的。不这样做会导致内存泄漏。

答案 2 :(得分:0)

另一种方法:放弃使用Java的Clip并编写自己的。我这样做了我创建了一个将数据存储在内存中的对象和另外两个为存储提供“游标”的对象,用于两种不同类型的回放。一个用于循环,另一个用于重叠播放。两者都可以设置为以不同的播放速度运行,因此您可以通过加快或减慢播放速度来获得不同的效果。两者都将其输出定向到我编写的混音器,其中数据合并为单个SourceDataLine。

此处显示的代码包含在第一篇文章中链接的jar中: http://www.java-gaming.org/topics/simple-audio-mixer-2nd-pass/27943/view.html

我很期待再回到它的工作,可能会把它放在GitHub上。

此外,TinySound是一个非常强大的声音管理器。你可以在这里了解它。这种方法与我的方法非常相似,混合到单个输出。 TinySound为Ogg等提供支持。我不认为它提供变速播放。

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