【问题标题】:OutOfMemory Exception when multithreading ... heap space?多线程时出现OutOfMemory Exception ...堆空间?
【发布时间】:2015-01-27 11:16:59
【问题描述】:

我正在开发一个需要与 Android 2.3 (Gingerbread) 兼容的应用程序,而我用于开发测试的设备是运行 Android 2.3.6 的 Motorola Atrix MB860。

在这个设备中,我获得了大约 40MB 的最大堆空间,据我所知,我的应用使用了大约 33MB,但我还是得到了 OutOfMemoryError 异常。

基本上,我的代码中对这个问题很重要的部分会创建一个大的String(8MB - 我知道它相当大,但如果它太小它不会满足其中一个要求),然后继续创建 2 个线程,使用该字符串同时写入某个内存空间。

代码如下:

    // Create random string
    StringBuilder sb = new StringBuilder();
    sb.ensureCapacity(8388608); // ensuring 8 MB is allocated on the heap for the StringBuilder object
    for (long i = 0; i < DATA_SIZE; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            sb.append(c);
    }
    String randomByteString = sb.toString();

    ExecutorService executor = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 2; i++) {
        Runnable worker = new SlidingBubbles(param1, param2, randomByteString)
        executor.execute(worker);
    }
    // This will make the executor accept no new threads 
    // and finish all existing threads in the queue 
    executor.shutdown();

    // Wait until all threads are finish 
    while(!executor.isTerminated()) {
        // wait for bubble threads to finish working...
    }

和线程的例程:

private class SlidingBubbles implements Runnable {
    private int param1, param2;
    private String randomByteString;
    private final Object mSignal = new Object();
    private volatile long tempBytesWritten = 0;
    private volatile long totalBytesWritten = 0;

    public SlidingBubbles(int param1, int param2, String randomByteString) {
        this.param1= param1;
        this.param2= param2;
        this.randomByteString = randomByteString;
    }

    private void doIt() {
        File file = null;
        RandomAccessFile randomAccessFile = null;
        FileChannel fc = null;

        try {
            while(param1> 0) {
                // Instantiate the 1st bubble file
                file = new File(TARGET_DIR, String.valueOf(Calendar.getInstance().getTimeInMillis()));

                while(param2 > 0) {                        
                    randomAccessFile = new RandomAccessFile(file, "rwd");
                    fc = randomAccessFile.getChannel();

                    fc.position(fc.size());
                    synchronized (mSignal) {
                        tempBytesWritten = fc.write(ByteBuffer.wrap(randomByteString.getBytes()));

                        totalBytesWritten += tempBytesWritten;
                    }

       // some other things that don't matter
    }

    @Override
    public void run() {
        wipe();            
    }
}

尴尬(对我而言),在线程例程 (tempBytesWritten = fc.write(ByteBuffer.wrap(randomByteString.getBytes()));) 的第 30 行,第二个线程 ("pool-1-thread-2") 启动异常,退出,第一个线程 ("pool -1-thread-1") 继续(实际上是开始)正常执行。 当 JVM 完成为那个大的 String 分配空间时,应用程序正在使用 33MB 的堆。从代码中可以看出,thatString 只创建了一次,然后在两个线程中都使用了多次。

线程不应该只使用对String 的引用而不是复制它吗? (在这种情况下,这将超过 40MB 的限额)。

我还必须指出,在 Gingerbread (previous research) 上增加这个堆空间是不可能的(或者至少似乎是,就我的理解而言)。

我有什么遗漏吗? 非常感谢任何帮助。

【问题讨论】:

    标签: java android multithreading out-of-memory heap-memory


    【解决方案1】:

    您可以静态地拥有 8MB 数据,并且永远不要创建它的副本。在 Android 上,StringBuilderString 共享内部 char[],但 String#getBytes() 每次都会创建数据的副本。

    我假设你的字符是纯 ASCII,当它们更特殊时这不能正常工作。

    Random random = new Random(); // once!
    byte[] data = new byte[8388608];
    for (int i = 0; i < data.length; i++) {
        data[i] = (byte) chars[random.nextInt(chars.length)];
    }
    

    上面会创建一次没有副本的数据。还要注意new Random() 8388608?循环中的时间也会导致大量内存使用,但它们应该很快被垃圾收集。

    当你这样做时

        public SlidingBubbles(int param1, int param2, byte[] data) {
            ...
            synchronized (mSignal) {
                tempBytesWritten = fc.write(ByteBuffer.wrap(data));
    

    您不再创建该数据的副本,ByteBuffer.wrap 不会创建数据的副本。无论你做什么,将完成的byte[] 传递给SlidingBubbles

    P.s:while(!executor.isTerminated()) {是错误的方式,有一个方法可以解决这个问题:How to wait for all threads to finish, using ExecutorService?

    【讨论】:

    • 指出了一些有趣的事情。我稍后会尝试。但是回复“p.s.”,我用while(!executor.isTerminated()),因为不知道线程的处理需要多长时间,所以我不知道如何设置awaitTermination(long timeout, TimeUnit unit)的时间参数。有没有办法让它变得灵活?
    • @NickFanelli 我只是将其设置为“永远”,或者您是否有理由想要在它们完成之前停止?永远意味着“Long.MAX_VALUE, TimeUnit.NANOSECONDS”,因为这是它实际可以花费的最大值,相当于大约 300 年
    • @NickFanelli 如果你真的只做while(!executor.isTerminated()) {} 而没有任何Thread.sleep() 或类似wait() 这样的东西,你会让处理器忙于100% 负载,一遍又一遍地检查这种情况。
    • 知道了!这正是我所需要的。在我的无知中,我认为“Long.MAX_VALUE 和 TimeUnit.NANOSECONDS”是一种说“在纳秒内输入 longTimeUnit”的方式,我永远不会知道如何预测。谢谢你的解释,@zapl。
    【解决方案2】:

    ByteBuffer.wrap(randomByteString.getBytes()) 在 while 循环中。这是你的罪魁祸首。缓冲区驻留在内存中。您应该在使用缓冲区后摆脱它。由于您正在重用此缓冲区,请将其创建移出循环。 编辑 : 试试这个,保持数组部分不变

    private static final byte [] arr = randomByteString.getBytes();
    for (int i = 0; i < 2; i++) {
            Runnable worker = new SlidingBubbles(param1, param2, arr)
            executor.execute(worker);
        }
    

    在你的可运行文件中试试这个

    try {
    
    while(param1> 0) {
    // Instantiate the 1st bubble file
    file = new File(TARGET_DIR, String.valueOf(Calendar.getInstance().getTimeInMillis()));
    
    while(param2 > 0) {                        
    randomAccessFile = new RandomAccessFile(file, "rwd");
    fc = randomAccessFile.getChannel();
    fc.position(fc.size());
    synchronized (mSignal) {
    tempBytesWritten = fc.write(ByteBuffer.wrap(arr));
    totalBytesWritten += tempBytesWritten;
    }
    // some other things that don't matter
    }
    

    【讨论】:

    • 谢谢@Dexter。将该指令移出循环并将ByteBuffer 本身作为参数发送给线程的构造函数会有所帮助。 OutOfMemmoryError 不再发生。但是,不仅 UI 仍然崩溃,而且由于某种原因,tempBytesWritten 在每次迭代后都显示为 0。
    • 实际上,调试时我发现第一次迭代它确实从fc.write() 调用中获得了 8MB,但随后的返回 0 ... ?
    • @NickFanelli 那是因为Buffer 不能那么容易地重用,它会跟踪它已写入的字节并希望您put 更多,请参阅tutorials.jenkov.com/java-nio/buffers.html 以获得一些解释。摆脱 directAllocated 并暗淡地做fc.write(ByteBuffer.wrap(arr)); 这不是创建数据的副本,它只是围绕它创建一个新的包装器。
    • @zapl 谢谢你指出这一点,我有点不确定:)
    • 应该有一种方法可以将缓冲区返回到原始状态,而不是重新包装数据,清除(不会用 0 覆盖数据)和翻转或其他这些命令的某种组合但我实际上不知道如何。缓冲区很复杂:)
    【解决方案3】:

    我想你也把 StringBuffer 挂了(因为主线程没有退出,sb 仍然会用完内存)?

    因此,您可以在使用完毕后将其清零,从而节省一些内存。即

    String randomByteString = sb.toString();
    sb = null;
    

    【讨论】:

    • 谢谢。您的建议可能有所帮助,但它本身并不能完全解决问题。我将它与@Dexter's 一起使用。
    • 我只是想指出,经过进一步研究,我意识到这个改变没有任何效果。原因是当你将一个对象的实例设置为null时,JVM所做的只是用null覆盖堆中该对象在堆栈中的指针地址,但之前为该对象分配的空间在Heap 不会立即释放。释放该区域是Garbage Collector 的责任,它不会按照程序员的意愿运行。简而言之,将对象的实例设置为null 不会立即节省(或释放)Heap 中的任何内存空间。
    【解决方案4】:

    如果您将字符串分解为 8 X 1MB 块的数组,可能会缓解您的记忆问题。这样:

    1) 您的 StringBuilder 只需 1MB,因此占用的内存更少 - 您可以通过使用 setLength() 方法重置它来重用它,我认为这意味着最坏的情况下它只会占用 1MB

    2) 您的 ByteBuffer 只需 1MB 而不是 8MB

    当你将字符串写入文件时,你可以循环你的数组。

    所以你仍然需要 8MB 的字符串,但除此之外你应该只需要额外的 2MB。

    【讨论】:

      【解决方案5】:

      我接受了@Dexter 的回答,因为它足以解决我的OutOfMemoryError 异常问题。但正如我在 cmets 中提到的,fc.write() 操作在解决之后仍然返回 0(写入的字节数)。

      这是我最终得到的结果(没有问题,除了表演问题,我正在努力寻找最佳曲调)。

          // Create random string
          StringBuilder sb = new StringBuilder();
          sb.ensureCapacity(8388608); // ensuring 8 MB is allocated on the heap for the StringBuilder object
          for (long i = 0; i < DATA_SIZE; i++) {
                  char c = chars[new Random().nextInt(chars.length)];
                  sb.append(c);
          }
          String output = sb.toString();
          sb = null;
      
          ByteBuffer randomByteStringBuffer = ByteBuffer.wrap(output.getBytes());
      
          ExecutorService executor = Executors.newFixedThreadPool(2);
          for (int i = 0; i < 2; i++) {
              Runnable worker = new SlidingBubbles(context, countRuns, originalTotalAvailable, totalBytesWritten, originalBytesAvailable, bubbleSize, randomByteStringBuffer);
              executor.execute(worker);
          }
          // This will make the executor accept no new threads and finish all existing threads in the queue 
          executor.shutdown();
      
          // Wait until all threads are finish 
          while(!executor.isTerminated()) {
              // wait for bubble threads to finish working...
          }
      

      还有线程的例程...

      private class SlidingBubbles implements Runnable {
          private int param1, param2;
          private ByteBuffer randomByteStringBuffer;
          private final Object mSignal = new Object();
          private volatile long tempBytesWritten = 0;
          private volatile long totalBytesWritten = 0;
      
      
          public SlidingBubbles(int param1, int param2, ByteBuffer randomByteStringBuffer) {
              this.param1= param1;
              this.param2= param2;
              this.randomByteStringBuffer = randomByteStringBuffer;
          }
      
          private void doIt() {
              File file = null;
              RandomAccessFile randomAccessFile = null;
              FileChannel fc = null;
      
              try {
      
                  while(countRuns > 0) {
                      // Instantiate the 1st bubble file
                      file = new File(TARGET_DIR, String.valueOf(Calendar.getInstance().getTimeInMillis()));                    
                      while(originalBytesAvailable > 0) {
      
                          randomAccessFile = new RandomAccessFile(file, "rw");
                          fc = randomAccessFile.getChannel();
      
                          fc.position(fc.size());
                          synchronized (mSignal) {
                              tempBytesWritten = fc.write(randomByteStringBuffer);
      
                              /* This rewind() is what did the trick. fc.write() started working normally again and writing 8MB.*/
                              randomByteStringBuffer.rewind();
      
                              totalBytesWritten += tempBytesWritten;
                          }
                       // some other things that don't matter
                      }
                   // some other things that don't matter
                  }
              } catch (IOEception ioe) {
                     Log.d(LOG_TAG, ioe.getMessage());
                }
          }
      
          @Override
          public void run() {
              wipe();            
          }
      }
      

      希望这在未来对其他人也有帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-05-29
        • 1970-01-01
        • 1970-01-01
        • 2013-11-04
        • 1970-01-01
        • 2013-03-30
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多