6

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;
        }
    }
}
Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
  • The Heap size growth has been determined exclusively through the use of DDMS. I can also see in the HPROF dump that the terminated thread sticks around for seemingly forever. – Cameron Lowell Palmer Oct 18 '12 at 14:11
  • is the byteArray flushed at the end? are you sure the ReaderThread stops properly? Are you sure your application reaches it's onDestroy() ? – Shark Oct 18 '12 at 14:22
  • I'm confident that the thread stops since it makes it to the STOP message. While the mController might very well stay alive for as long as the activity is alive, the next call to onStart() would overwrite the thread with a new instance making the old one eligible for GC. Right? – Cameron Lowell Palmer Oct 18 '12 at 14:36
  • I'll add to this that reset() on the byte array will zero it out. close() does nothing according to the docs. – Cameron Lowell Palmer Oct 18 '12 at 14:39
  • Have you tried to null the ref to `Controller` after `mController.quit()` explicitly? – Fildor Oct 18 '12 at 15:14
  • I have tried nulling that and the thread reference. – Cameron Lowell Palmer Oct 18 '12 at 15:42

5 Answers5

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.

Community
  • 1
  • 1
acj
  • 4,821
  • 4
  • 34
  • 49
  • That is my experience. Nulling out the byte array makes the size of the leak significantly smaller, but we are still leaking everything else in the thread. However, this isn't a 'running' thread. Are stopped threads also immune from garbage collection? – Cameron Lowell Palmer Oct 18 '12 at 15:06
  • I don't know of a reliable fix for this, unfortunately. As @StephenC noted, figuring out how the `ReaderThread` is leaking is the key. Since it's a `Thread` subclass, the JVM has the final word. You might be able to make the `Thread` an member variable of your class instead of subclassing it. This would decouple your allocations from the lifetime of the `Thread`, making them eligible for garbage collection. Separately, I would be interested to know if calling `interrupt()` at the end of `run()` has any effect. – acj Oct 18 '12 at 15:14
  • OK. I will try the interrupt() call, and making the thread a member variable. I'll report back. – Cameron Lowell Palmer Oct 18 '12 at 15:26
  • Sounds good. Regarding your earlier comment, typically the JVM will clean up completed threads right away. Are you running this app in Debug mode? I know that ADT/DDMS used to keep old threads around for debugging purposes until the app terminated. Not sure whether it still does. – acj Oct 18 '12 at 15:29
  • In the debugger, with or without a call to interrupt()) you can see the ReaderThread is (running) and on app exit it disappears from the list of application threads. However, in both of those cases allocated heap grows. – Cameron Lowell Palmer Oct 18 '12 at 15:38
  • I think the problem may be related to the debugger's thread and heap tracking mechanisms. Please try running the app without the debugger (i.e., with the app in release mode) to see if the leaks still occur. – acj Oct 18 '12 at 15:45
  • Read the last paragraph on this page http://developer.android.com/tools/debugging/index.html. – Cameron Lowell Palmer Oct 19 '12 at 09:36
  • You should modify your answer to reflect the verification that Debugger is in fact the problem. I will award you the answer – Cameron Lowell Palmer Oct 19 '12 at 11:19
  • Very glad to hear that you found the root cause. I've edited my answer. – acj Oct 19 '12 at 11:51
3

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?

Cameron Lowell Palmer
  • 21,528
  • 7
  • 125
  • 126
  • 1
    Wow. I just verified that this was the case. Here are the steps I took a.) Build up heap usage to 30+ MB b.) Disconnect the debugger c.) Use a third party app (Norton utilities) to check the memory usage again. As noted by the documentation the memory usage drops once the debugger disconnects. The problem is only prevalent on threads and not async tasks or services. While the debugger reported a false positive, I'd still use a AsyncTask or Service to run background tasks. You get to follow google's recommendation and the debugger does not report leaks either :) Good find Cameron. – Deepak Bala Oct 19 '12 at 11:06
2

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.

Community
  • 1
  • 1
Deepak Bala
  • 11,095
  • 2
  • 38
  • 49
  • I will try this and get back to you. I have used AsyncTasks, but in general this is problematic because it returns on the main thread, and our application here is a long running WebSocket connection with streaming data. However, it seems you have worked around that by making the AsyncTask long running. :) – Cameron Lowell Palmer Oct 19 '12 at 08:40
  • I guess you could use a service instead of an async task. It is better suited for the job. I'm willing to bet that using services will not cause leaks either. – Deepak Bala Oct 19 '12 at 10:54
  • From the docs... AsyncTask is designed to be a helper class around Thread and Handler and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs provided by the java.util.concurrent pacakge such as Executor, ThreadPoolExecutor and FutureTask. – Cameron Lowell Palmer Oct 19 '12 at 11:33
  • hmmm... My understanding of the recommendation might have been skewed from watching this video - http://www.youtube.com/watch?feature=player_detailpage&v=xHXn3Kg2IQE#t=512s The video talks about starting threads from an activity and retrieving the results using a thread. The author says that is a bad idea and proceeds to recommend async tasks and services. I guess I misunderstood the message as 'Threads are bad' ? I'll look deeper into this. Thanks for the quote. – Deepak Bala Oct 19 '12 at 11:43
  • Well, threads are evil. :) So much so that Apple went and created Grand Central Dispatch (GCD) to help deal with them. The biggest problem with threads in Android is managing the complexity and AsyncTask does a lot to remedy the common case. Namely, do something in the background and return on the main thread. – Cameron Lowell Palmer Oct 19 '12 at 11:58
1

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.

Stephen C
  • 698,415
  • 94
  • 811
  • 1,216
  • The original code is involved in a WebSocket library. I wrote this trivial android app to isolate the two components that seemed to be involved in the leak and it does leak. – Cameron Lowell Palmer Oct 18 '12 at 14:45
1

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.

Luis
  • 11,978
  • 3
  • 27
  • 35
  • Let me say, good idea. I am already nulling out the mThread reference after quit and it doesn't help in SDK 16 according to DDMS heap monitoring, but even if it does it needs to work back to at least 10. – Cameron Lowell Palmer Oct 19 '12 at 08:29
  • That's strange. I've tried your test code in a real device and emulator (both using SDK10) and it seams to solve the leak. I've attached screen shots to the answer, for you to check if my understanding is correct. Cheers – Luis Oct 19 '12 at 11:54
  • What Eclipse version are you running? – Cameron Lowell Palmer Oct 19 '12 at 11:59
  • Eclipse SDK - Version: 3.7.0 - Build id: I20110613-1736 – Luis Oct 19 '12 at 12:01
  • But I don't believe that Eclipse can play any role on this, as the compiler belongs to the SDK. – Luis Oct 19 '12 at 12:16
  • As I posted in my answer, Threads don't get GCd when you are attached to the debugger according to the docs. So, if you are getting a different result that must be environment. Eclipse, ADT, something is different and I would love to know what it is. – Cameron Lowell Palmer Oct 19 '12 at 12:24