为什么线程在Android上泄漏?

时间:2012-10-18 14:04:48

标签: java android multithreading garbage-collection

我一直在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();
    }
}

Controller.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;
        }
    }
}

5 个答案:

答案 0 :(得分:8)

线程对GC免疫,因为它们是garbage collection roots。因此,JVM可能会将您的ReaderThread保留在内存中,以及它对成员变量的分配,从而造成泄漏。

正如您所指出的那样,取消ByteArrayOutputStream会使GC的缓冲数据(但不是ReaderThread本身)可用。

修改

经过一番调查,我们了解到the Android debugger was causing the perceived leak

  

VM保证调试器知道的任何对象在调试器断开连接之前不会被垃圾回收。 在调试器连接时,这会导致对象随时间累积。例如,如果调试器看到正在运行的线程,则即使在线程终止后,关联的Thread对象也不会被垃圾回收。

答案 1 :(得分:3)

来自Android Debugging页面:

  

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

你认为这会降低DDMS堆监控的价值吗?

答案 2 :(得分: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 comparison of the solutions

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

请参阅此答案,了解报告此泄漏的原因 - https://stackoverflow.com/a/12971464/830964。这是误报。

我能够摆脱BAOS占用的内存而不用异步任务显式地将其清空。它也适用于其他变量。如果能为您解决,请告诉我。

答案 3 :(得分:1)

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

专注于搞清楚 ReaderThread实例泄漏的方式。

答案 4 :(得分:1)

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

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

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

我在DDMS中检查过泄漏是否有效停止了。删除线,泄漏返回。

- 编辑 -

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

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

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

Step1

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

Step2

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

Step3

虽然在最后一个屏幕中分配的内存明显高于初始内存,但分配的内存仍为4.6MB,因此不会泄漏。