弱引用和`OutOfMemoryError`s

我有一个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.8s),在得到OutOfMemoryError之前,我只能获得大约2100个调用:

#没有足够的内存让Java运行时环境继续运行。
#本地内存分配(malloc)未能为C: BUILD_AREA jdk6_34 hotspot src share vm prims jni.cpp中的jbyte分配3874172个字节
#包含更多信息的错误报告文件保存为:
#[路径]

我尝试在SoundManager.Sound类中实现private static final Vector<WeakReference<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)

  • byteCache是一个HashMap<String, byte[]>
  • clazz是一个Class<?>
  • byteCache是一个HashMap<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秒钟进行堆积。 那么这是说:

    问题疑犯#1:

    由“”加载的2,062个“com.sun.media.sound.DirectAudioDevice $ DirectClip”实例占用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次以上),我会收到一条“耗尽”消息,但仍然会得到相同的错误。 用flush代替drain也没有效果。 使用close可以使线路稍后不能打开(即使在侦听START并呼叫openstart )。


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

    在本地分配中,分配看起来没有在正常的Java分配中失败,我怀疑“GC在抛出OOME之前运行”的正常行为适用于这种情况。

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


    请原谅,如果这是完全关闭的,但我想检查两个基本原理,我无法从您的代码中随意阅读。

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

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

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


    另一种方法:放弃使用Java的剪辑并编写自己的。 我做到了。 我创建了一个将数据存储在内存中的对象,以及另外两个为存储器提供“光标”的对象,以用于两种不同类型的播放。 一个用于循环播放,另一个用于重叠播放。 两者都可以设置为以不同的播放速度运行,因此您可以通过加快或减慢播放速度来获得不同的效果。 两者都将它们的输出指向我写的混音器,其中数据被合并到一个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

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

    上一篇: Weak references and `OutOfMemoryError`s

    下一篇: Autorelease pool page corrupted