Why do threads leak on Android?

2020-07-16 09:10发布

I've been noticing in our Android app that every time we exit to the home screen we increase the heap size (leak) by the amount of the ByteArrayOutputStream. The best I have been able to manage is by adding

this.mByteArrayOutputStream = null;

at the end of run() to prevent the heap size increasing constantly. If anyone could enlighten me I would be very appreciative. I wrote out the following example that illustrates the problem.

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条回答
Ridiculous、
2楼-- · 2020-07-16 09:26

Based on the code that you have shown use, the ByteArrayOutputStream instance can only leak if the ReaderThread instance leaks. And that can only occur if either the thread is still running, or there is still a reachable reference to it somewhere.

Focus on figuring out how the ReaderThread instance leaks.

查看更多
爷、活的狠高调
3楼-- · 2020-07-16 09:34

I´ve had a similar problem and I solved it by nulling the thread.

I've tried your code in the emulator (SDK 16) changing quit()method as follow:

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

I've checked in DDMS that the leak as effectively stopped. Removing the line, the leak returns.

--EDITED--

I've tried this also in a real device using SDK 10, and I'm putting the results bellow.

Did you try the thread nulling in your test code or in you full code? It seams that the test code, don't leak after nulling the thread, neither in a real device or in emulator.

Screen shot after initial start (allocated 7.2MB / used: 4.6MB):

Step1

Screen shot after a couple of restarts (allocated 7.2MB / used: 4.6MB):

Step2

Screen shot after several and very fast restarts, and rotating device servral times (allocated 13.2MB / used: 4.6MB):

Step3

Although memory allocated in the last screen is significantly higher then initial memory, the allocated memory remains 4.6MB, so it´s not leaking.

查看更多
家丑人穷心不美
4楼-- · 2020-07-16 09:40

This problem is pretty interesting. I took a look at it and managed to get a working solution with no leaks. I replaced the volatile variables with their atomic counterparts. I also used an AsyncTask instead of a Thread. That seems to have done the trick. No more leaks.

Code

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

Here is a comparison of both code samples after they went to and from the home screen a couple of times. You can see a byte[] leak on the first one.

Hprof comparison of the solutions

As for why this happens, google recommends that you use a AsyncTask or Service for async operations. I remember one of their videos explicitly advising against Threads. The person making the presentation warned of side effects. I guess this is one of them ? I can clearly see that there is a leak when the activity is brought back to life, but there is no explanation of why said leak would occur (at least on a JVM. I do not know about the internals of the dalvik VM and when it considers threads to have stopped completely). I tried weak-references / nulling the controller / etc and none of those approaches worked.

See this answer for why this leak is reported - https://stackoverflow.com/a/12971464/830964 . It is a false positive.

I was able to get rid of the memory occupied by BAOS without explicitly nulling it out with async tasks. It should work for other variables also. Let me know if that solves it for you.

查看更多
女痞
5楼-- · 2020-07-16 09:44

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.

查看更多
干净又极端
6楼-- · 2020-07-16 09:48

From the Android Debugging page:

The debugger and garbage collector are currently loosely integrated. 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.

That diminishes the value of DDMS heap monitoring don't you think?

查看更多
登录 后发表回答