7

我有一个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 秒),我只能进行大约 2100 次调用,然后才能获得OutOfMemoryError

# Java 运行时环境没有足够的内存来继续。
# Native memory allocation (malloc) failed to allocate 3874172 bytes for jbyte in C:\BUILD_AREA\jdk6_34\hotspot\src\share\vm\prims\jni.cpp
# 包含更多信息的错误报告文件保存为:
# [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());

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

但是,在同样的情况发生之前,我仍然只得到了大约 10 次调用OutOfMemoryError

如果重要的话,对于每个文件名,我将文件内容缓存为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”之前没有加载过。它将文件写入 abyte[]并存储它。然后它继续进行,就好像它以前被缓存过一样。

第一次和所有后续时间(在缓存之后),该方法将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;
}

堆分析后编辑。

在崩溃前大约 5 秒进行了堆转储。好吧,这说明了:

堆转储图

问题嫌疑人#1:

由“”加载的“com.sun.media.sound.DirectAudioDevice$DirectClip”的2062个实例占用了230207264(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);

对此:

// 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+ 次/秒),我都会收到一条“耗尽”消息,但仍然会收到相同的错误。替换drainflush也没有效果。Usingclose使线路稍后无法打开(即使在监听START和调用openand时start)。

4

3 回答 3

2

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

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

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

于 2013-04-01T05:11:09.053 回答
1

如果这完全不正确,请原谅,但我想检查两个我无法从随意阅读您的代码中辨别出来的基础知识。

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

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

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

于 2013-04-01T18:22:14.847 回答
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

于 2013-04-03T19:56:07.827 回答