【问题标题】:Weak references and `OutOfMemoryError`s弱引用和 `OutOfMemoryError`s
【发布时间】:2013-04-01 03:26:32
【问题描述】:

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

this的大部分用途如下:

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

据我了解,这应该在内存中保留对新创建声音的引用,并且应该相当快地对它进行垃圾收集。但是,对于一个简短的声音文件(108 KB,以惊人的 00:00:00 秒打卡,实际上大约 0.8 秒),我只能进行大约 2100 次调用,然后才能收到OutOfMemoryError

# 内存不足,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());

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

但是,在同样的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)被收集时,流也会被关闭。


EDIT 每个请求使用 get 方法。这是public Sound get(String name)

  • byteCacheHashMap&lt;String, byte[]&gt;
  • clazzClass&lt;?&gt;

byteCacheHashMap&lt;String, byte[]&gt;clazzClass&lt;?&gt;

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 在堆分析之后。

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

问题嫌疑人#1:

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

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

这些Clip 对象被Sound 对象强引用,但Sound 对象仅在Vector&lt;WeakReference&lt;Sound&gt;&gt; 中被弱引用。

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


编辑每个菲尔的 cmets:

我已经改变了这个:

// 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 时)。

【问题讨论】:

  • 在我看来,您正在使用一些 JNI 库,该库正在占用大量资源。
  • 那会是 javasound 问题吗?我所做的一切都是香草。
  • 我对 JavaSound 一无所知。但是您可能会以某种方式“滥用”它,例如不重置某些内容等。
  • OutOfMemoryError 在这种情况下意味着与堆栈相关的东西。你不是在某个地方隐藏了某种循环调用吗?
  • 而对于流,你永远不应该在内存中存储那种东西,你输入流来读取需要的内容。能不能展示一下get方法的内容?

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


【解决方案1】:

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

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

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

【讨论】:

  • (假设“音频流”== AudioInputStream ais)如何在将字节读入剪辑的同时关闭它们? javasound 标签 wiki 不会关闭任何流。
  • 你没有。你等到读完之后。恐怕我无法弄清楚你的代码实际上在做什么......
【解决方案2】:

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

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

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

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

【讨论】:

  • Re: (2) 我确实每次都在创建一个Clip。如果声音文件大约 30 帧长,并且两个炮塔相距 5 帧开火,您应该能够听到两个不同但重叠的开火声音。有没有办法链接两个或多个剪辑的 PCM 数据?那是我的最终目标。另外,您能否提供一个如何/何时正确关闭流的示例?
  • 声音上下文中的“帧”是样本的另一个词,每秒可能有 44100 个样本。也许您使用上面的“帧”一词作为视觉帧数的术语,并且显示的是 30 或 60 fps 之类的东西?不一样的说法!如果您的声音是 1 秒长的立体声 16 位编码,即每个样本 4 字节 * 44100 = 每次播放该声音提示时消耗的 176400 字节!所以你真的应该尽可能多地重复使用 Clips。你知道怎么做吗?如果需要重叠,您可以制作两份副本并使用它们。有些人制作自己的混音器。
  • 关闭名为'ais'的AudioInputStream: ais.close();艾斯=空;完成从流中读取后执行此操作。您可能可以通过将剪辑归零来节省一些空间,但我怀疑这不起作用。您可以尝试使用 SourceDataLine 而不是 Clip。它们消耗的内存要少得多,并且新的 SDL 比新的剪辑启动得更快。不过,重新启动现有的 Clip 是最快的。
  • 复制两份剪辑是什么意思?那是我的目标;我该怎么做?
  • "当你完成从流中读取时" == "当剪辑完成播放时"?
【解决方案3】:

另一种方法:放弃使用 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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-07-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多