6

我一直在我们的 Android 应用程序中注意到,每次我们退出到主屏幕时,我们都会将堆大小(泄漏)增加 ByteArrayOutputStream 的数量。我能够管理的最好的方法是添加

this.mByteArrayOutputStream = null;

run()结束时防止堆大小不断增加。如果有人能启发我,我将不胜感激。我写了下面的例子来说明这个问题。

MainActivity.java

public class MainActivity extends Activity {
    private Controller mController;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);        
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_main, menu);
        return true;
    }

    @Override
    protected void onStart() {
        super.onStart();

        this.mController = new Controller();
        mController.connect();
    }

    @Override
    protected void onStop() {
        super.onStop();

        mController.quit();
    }
}

控制器.java

public class Controller {
    public volatile ReaderThread mThread;

    public Controller() {
        super();
    }

    public void connect() {
        mThread = new ReaderThread("ReaderThread");
        mThread.start();
    }

    public void quit() {
        mThread.quit();
    }

    public static class ReaderThread extends Thread {
        private volatile boolean isProcessing;
        private ByteArrayOutputStream mByteArrayOutputStream;

        public ReaderThread(String threadName) {
            super(threadName);  
        }

        @Override
        public void run() {
            this.isProcessing = true;

            Log.d(getClass().getCanonicalName(), "START");
            this.mByteArrayOutputStream = new ByteArrayOutputStream(2048000);

            int i = 0;
            while (isProcessing) {
                Log.d(getClass().getCanonicalName(), "Iteration: " + i++);
                mByteArrayOutputStream.write(1);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }

            try {
                mByteArrayOutputStream.reset();
                mByteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            Log.d(getClass().getCanonicalName(), "STOP");
        }

        public void quit() {
            this.isProcessing = false;
        }
    }
}
4

5 回答 5

9

Threads are immune to GC because they're garbage collection roots. So, it's likely that the JVM is keeping your ReaderThread in memory, along with its allocations for member variables, thus creating the leak.

Nulling out the ByteArrayOutputStream, as you've noted, would make its buffered data (but not the ReaderThread itself) available for GC.

EDIT:

After some sleuthing, we learned that the Android debugger was causing the perceived leak:

The VM guarantees that any object the debugger is aware of is not garbage collected until after the debugger disconnects. This can result in a buildup of objects over time while the debugger is connected. For example, if the debugger sees a running thread, the associated Thread object is not garbage collected even after the thread terminates.

于 2012-10-18T14:58:16.633 回答
3

Android 调试页面:

调试器和垃圾收集器目前松散地集成在一起。VM 保证调试器知道的任何对象在调试器断开连接之前不会被垃圾收集。当调试器连接时,这可能会导致对象随着时间的推移而堆积。例如,如果调试器看到一个正在运行的线程,则即使在线程终止后,关联的 Thread 对象也不会被垃圾回收。

这会降低 DDMS 堆监控的价值,您不觉得吗?

于 2012-10-19T09:39:56.940 回答
2

这个问题很有趣。我看了看它并设法获得了一个没有泄漏的工作解决方案。我用它们的原子对应物替换了 volatile 变量。我还使用了 AsyncTask 而不是 Thread。这似乎成功了。没有更多的泄漏。

代码

class Controller {
    public ReaderThread mThread;
    private AtomicBoolean isProcessing = new AtomicBoolean(false);
    public Controller() {
        super();
    }

    public void connect() {
        ReaderThread readerThread = new ReaderThread("ReaderThread",isProcessing);
        readerThread.execute(new Integer[0]);
    }

    public void quit() {
        isProcessing.set(false);
    }

    public static class ReaderThread extends AsyncTask<Integer, Integer, Integer> {
        private AtomicBoolean isProcessing;
        private ByteArrayOutputStream mByteArrayOutputStream;

        public ReaderThread(String threadName, AtomicBoolean state) {
            this.isProcessing = state;
        }

        public void run() {
            this.isProcessing.set(true);

            Log.d(getClass().getCanonicalName(), "START");
            this.mByteArrayOutputStream = new ByteArrayOutputStream(2048000);

            int i = 0;
            while (isProcessing.get()) {
                Log.d(getClass().getCanonicalName(), "Iteration: " + i++);
                mByteArrayOutputStream.write(1);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                }
            }

            try {
                mByteArrayOutputStream.reset();
                mByteArrayOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            Log.d(getClass().getCanonicalName(), "STOP");
        }

        public void quit() {
            this.isProcessing.set(false);
        }

        @Override
        protected Integer doInBackground(Integer... params)
        {
            run();
            return null;
        }
    }
}

这是两个代码示例在几次往返主屏幕后的比较。您可以在第一个上看到 byte[] 泄漏。

Hprof 解决方案比较

至于为什么会发生这种情况,google 建议您使用 AsyncTask 或 Service 进行异步操作。我记得他们的一个视频明确建议反对 Threads。进行演示的人警告过副作用。我想这是其中之一?我可以清楚地看到,当活动恢复活力时存在泄漏,但没有解释为什么会发生所述泄漏(至少在 JVM 上。我不知道 dalvik VM 的内部结构以及何时发生认为线程已完全停止)。我尝试了弱引用/将控制器归零/等,但这些方法都没有奏效。

请参阅此答案以了解为什么报告此泄漏 - https://stackoverflow.com/a/12971464/830964。这是一个误报。

我能够摆脱 BAOS 占用的内存,而无需通过异步任务明确地将其清空。它也应该适用于其他变量。让我知道这是否可以为您解决。

于 2012-10-18T16:42:06.180 回答
1

根据您显示使用的代码,该ByteArrayOutputStream实例仅在ReaderThread实例泄漏时才会泄漏。只有当线程仍在运行,或者在某处仍有可访问的引用时,才会发生这种情况。

专注于弄清楚实例是如何ReaderThread泄漏的。

于 2012-10-18T14:16:44.093 回答
1

我有一个类似的问题,我通过将线程归零来解决它。

我已经在模拟器(SDK 16)中尝试了您的代码更改quit()方法如下:

public void quit() { 
    mThread.quit(); 
    mThread = null;  // add this line
} 

我已经在 DDMS 中检查了泄漏有效地停止了。移除线,泄漏返回。

--已编辑--

我也在使用 SDK 10 的真实设备中尝试过这个,我将结果放在下面。

您是否在测试代码或完整代码中尝试过线程归零?它接缝测试代码,在将线程归零后不会泄漏,无论是在真实设备中还是在模拟器中。

初始启动后的屏幕截图(分配 7.2MB / 已使用:4.6MB):

步骤1

几次重启后的屏幕截图(分配 7.2MB / 使用:4.6MB):

第2步

几次非常快速的重启后的屏幕截图,以及旋转设备服务时间(分配 13.2MB / 使用:4.6MB):

第三步

尽管在最后一个屏幕中分配的内存明显高于初始内存,但分配的内存仍为 4.6MB,因此没有泄漏。

于 2012-10-18T19:14:35.040 回答