LanDenLabs - Android Memory Leaks (Dec-2017) [Go Back]

leaks

Android / Java - Memory leaks (April 2018)
By: Dennis Lang - Dec 2017

Show Table of Contents

Related Articles

Garbage Collector

The Java memory manager uses a Garbage Collector to periodically remove unreferenced memory. The Java memory system is not based on reference counting as is common in C++ smart pointers, but rather uses a tree graph to determine active memory allocations. Each thread has a root node and there is a separate root node for static memory allocations. When the Garbage Collection (GC) runs it first walks the memory allocation table and marks every cell as inactive. It then walks the tree graphs and marks all reachable nodes as active. Lastly it scans the allocation table and calls finalize on the inactive memory allocations removing them from the tree and table.

The tree graph nodes each represent a memory allocation which optionally holds pointers to other memory allocations. The tree graph may be more of a visual representation of how Java tracks reachable and unreachable memory and not the actually internal representation used.

Memory - all allocations reachable "actice"

Memory Table

AddressSizeActive
1 n bytesactive
2 n bytesactive
3 n bytesactive
4 n bytesactive
5 n bytesactive
6 n bytesactive
7 n bytesactive
8 n bytesactive
9 (root) n bytesactive

Memory Tree

Example of inactive memory cluster which GC will release.

Memory - few inactive allocations GC will release

Memory Table

AddressSizeActive
1 n bytesactive
2 n bytesactive
3 n bytesactive
4 n bytesactive
5 n bytesinactive
6 n bytesactive
7 n bytesactive
8 n bytesinactive
9 (root) n bytesactive

Memory Tree

Example of circular reference which has become unreachable and will be released by GC.

Memory - with inactive circular references cluster

Memory Table

AddressSizeActive
1 n bytesinactive
2 n bytesinactive
3 n bytesinactive
4 n bytesinactive
5 n bytesinactive
6 n bytesactive
7 n bytesactive
8 n bytesactive
9 (root) n bytesactive

Memory Tree

>

How to make a leak

The following is a summary of common Android memory leaks and how to avoid them. We start by explaining what a memory leak is and how to create them. Then we will show which are common leaks and how to avoid them.

Since Java VM will automatically release any unreachable memory it is hard to make a memory leak. In other languages like C/C++ you can allocate memory using malloc() and forget to release the memory by not calling free() on the pointer. In Java there is no free method and the memory cleans up after itself. In Java it is not possible to have a true leak like you can in C/C++. Instead Java leaks are just poor object managment. Meaning you can consume too much memory if you hold on to objects too long. In Android a common leak occurs when creating listener and adding it to a list and failing to remove it from the list.

The following code fragment intentionally creates a leak by saving the anonymous listener of the class it is making. Since the anonymous class holds a pointer to the class (see disucssion below), holding a copy of the listener causes class memory to stay active.


public class Leak1 {

    long[] mBigMemory = new long[512];  // allocate some memory.

    interface  Listener {
        void myListener(int value);
    }

    Listener getListener() {
        return new Listener() {
            @Override
            public void myListener(int value) {
                System.out.println("  Got value=" + value);
            }
        };
    }

    public void doWork() {
        // do something.
    }
}

public class LeakTester {

    List<Leak1.Listener> mListeners = new ArrayList<>();

    public void Tester(int itemCnt) {
        for (int idx = 0; idx < itemCnt; idx++) {
            Leak1 leak1 = new Leak1();
            
            // Create leak by holding Listener (anonymous/inner class)
            // which has pointer to parent class.
            mListeners.add(leak1.getListener());
        }
    }
}
    

By removing the listener from the list, we allow the class memory to be released. Example code fragment which avoids leak.


public class LeakTester {

    List mListeners = new ArrayList<>();

    public void Tester(int itemCnt) {
        for (int idx = 0; idx < itemCnt; idx++) {
            Leak1 leak1 = new Leak1();

            // Create leak by holding Listener (anonymous/inner class)
            // which has pointer to parent class.
            mListeners.add(leak1.getListener());

            //  use Leak1 class
            leak1.doWork();

            // Clear listener to release class memory and avoid leak.
            mListeners.remove(leak1.getListener());
        }
    }
}
    

Reference Counting (not a Java problem)

I originally thought Java memory management used Reference counting which makes circular references a common source of memory leaks. Good news is Java VM does not use reference counting. Java is able to release circular object references which are nolonger reachable from the root memory allocation. See diagram

Background on Reference Counting:

Terminology - Both pointer and reference are used interchangeably to denote a linkage between objects. Java only releases memory associated with an object when there is no reachable path from the root allocation nodes (one root per thread and one for static allocations)


        Examples of circular references:
        
             Self -> Self                       // Reference to self
             Item1 -> Item2 -> Item3 -> Item1   // Circular chain 
        


// Java example of circular reference to self.
class Self {
    Object mTag;
};

Self self = new Self();
self.mTag = self;
        


// Java example of circular reference chain.
class  Base {
    int mIdx;
    Base(int idx) {
        mIdx = idx;
    }
}

class Item extends Base {
    Base mBase;
    Item(Base base, int idx) {
        super(idx);
        mBase = base;
    }
    void set(Base base) {
        mBase = base;
    }
}

void makeCircularChain() {
    Item item;
    Item last = item = new Item(null, 0);
    for (int idx = 1; idx < 3; idx++) {
        last = new Item(last, idx);
    }
    item.set(last);  // 3 instances of item form circular reference chain.
}
The above are examples of common Android circular references which don't produce memory leaks because Java ignores reference counting. They are leaks only if the circular reference cluster is not reachable from the root allocation nodes.

Circular references are still important to understand because they can cause extra classes to stay active and not get released. See circular memory released and leak code fragment .

Anonymous (Inner) class

Anonymous classes are handy quick code fragments added to handle callbacks. Anonymous classes have full access to the parent class. As such, they have an internal pointer to the parent class. This internal pointer creates a memory reference from the Listener to the parent class. If the listener is held outside of the parent class it will prevent the parent class from getting released until the listener is also cleared, see leak code fragment .

Some common ways Android uses anonymous classes are:

  1. Listeners
  2. Runnables

// Example of anonymous class, creating a reference to parent class.

mSpecialButton = (Button)findViewById(R.id.at_button);
mSpecialButton.setOnClickListener(new View.OnClickListener() {
    // The moment this anonymous class is created it has a pointer to the parent.
    @Override public void onClick(View v) {
        // do something special
    }
});
        

// Example of anonymous class using Runnable creating a circular reference to parent.
// NOTE - Once Runnable executes and removed from mHandle queue, 
// circular reference disappears. 

public void updateSeeMore() {
    mHandle.post(new Runnable() {
        @Override
        public void run() {
            // do something.
        }
    });
}
        
Anonymous class can be a problem because they have an automatic pointer to their parent. If you store an anonymous class instance in another class you link the anonymous outer class and the holder class. If you fail to clear the anonymous class in the holder and break the linkage, the anonymous outer may leak. See solution in Avoid Leaks.

Non-static Inner class

Non-static inner classes are identical to anonymous classes in that they have an internal pointer to their parent class and can creating a permenant reference to the parent class until the static instance is cleared.

// Example of non-static inner class memory leak.

class MyActivity extends Activity {

    // Storing non-static inner class in static data member creates
    // a permenant reference. MyActivity will never get garbage collected
    // until the static value is cleared. 
    static ClickHandler clickHandler = new ClickHandler(); 
    
    public void myMethod() {
        Button[] buttons = getButtonArray();
        for (Button button : buttons) {
            button.setOnClickListener(clickHandler);   
        }
    }

    // Non-static inner class has pointer to parent class.
    class ClickHandler implements View.OnClickListener {
        // Inner class has automatic pointer to parent as if it had
        // a data member like this:
        //
        // MyActivity mPointer2Parent = MyActivity.this;
        
        public void onClick(View v) {
            showToast(((Button) v).getText());
        }
    }
}
Note - By refactoring the non-static inner class into a static inner class you can avoid the parent reference. The other option is to clear any callbacks (ex: setOnClickListener(null)) to a null value when your are done with the class. In the code above, you would set clickHandler to null..

Handler

Android Handler is a handy object to manage asynchronous activity and execute logic on a specific thread, typically the UI (main) thread. Every Handler has a Looper which has a pointer to an Activity. Every message you post to the Handler has a pointer to the Handler. If your messages has auxiliary data, you may have additional pointers to objects. Posting a message to a Handler creates a circular reference to the Handler while the message is pending (in the handler queue).

If your class provides public methods for external users to post delayed messages on your Handler you need to explicitly remove ALL messages during destruction to avoid leaks.


// Example sending message to Handler

class GalleryFragment .... {

    final int MSG_SHOW_IMAGE = 100;
    final Handler mHandler = new Handler();
   
    updateGallery(InputStream is) {
        final Bitmap bitmap = BitmapFactory.decodeStream(is);
        mHandler.obtainMessage(MSG_SHOW_IMAGE, bitmap).sendToTarget();
    }
}
        
While the message is pending the Handler will have a circular reference. The pending message is typically a short term problem because it is quickly processed and the circular reference disappears. The problem can be extended by using a delayed message.
  
final Message msg = handler.obtainMessage();
mHandler.sendMessageDelayed(MSG_SHOW_IMAGE, bitmap, TimeUnit.MINUTES.toMillis(1000));
        
The delayed send will obviously increase the lenght the circular reference exists. The same is true for post and postDelayed message APIs on Handlers and View objects. The leak occurs when you have pending messages which are not processed and your class needs to be destroyed.

Avoid ViewHolders

Avoid using View Holders unless you have sufficient reuse of the view objects to justify the overhead and code complexity. The View Holder design pattern is popular in Android and even forced on you by some of the new container objects like RecyclerView. The problem with ViewHolder is its primary purpose is to improve performance by avoid the expense of searching for View objects when calling findViewById(). This only improves performance if you have enough rows in your scrolling view to reuse more views then the number of ViewHolders you create.

The following is a graph from Google I/O presentation on ViewHolder showing only a small improvement in performance from 50 frames/second to 55 frames/second when scrolling 10,000 items. My point is View Holders rarely improve performance enough that a human will notice and they increase code complexity, making it harder to maintain (add/remove UI requires more code changes) and it increases the chance for a memory leak by causing a circular reference. So avoid using View Holders !

See Google I/O video on topic

viewholder

In the following simple ViewHolder, the benefit of making and populating the ViewHolder is only justified if every ViewHolder is reused multiple times. If your device and List layout presents a list which never scrolls, then the ViewHolder is costing you performance, memory and adding code complexity. If your list scrolls one full page (meaning every ViewHolder has been reused once), you may be close to improving performance. If your list scrolls multiple pages you are starting to see a benefit. Looking at the graph provided by Google I/O you can see there is a very small gain even with 10,000 items. The truth is findViewById() is not that expensive if your row layout only contains a few view objects. The find view logic walks a link list and finds a match in a few node steps.

Recommendation - ONLY use a ViewHolder if you have:

  1. Multiple pages to scroll and all ViewHolders will be resused multiple times
  2. and your View layout is complex which could cause findViewById to be expensive.

// Example - simple ViewHolder 

// ViewHolder must be static class to avoid leaking. 
public static class ViewHolder {
    TextView text;
    ImageView icon;
}

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;

    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
        holder = new ViewHolder();
        holder.text = (TextView) convertView.findViewById(R.id.text);
        holder.icon = (ImageView) convertView.findViewById(R.id.image);

        convertView.setTag(holder);  // <--- Risk of leak if ViewHolder has extra objects.
    } else {
        holder = (ViewHolder) convertView.getTag();
    }

    holder.text.setText(DATA[position]);
    holder.icon.setImageBitmap(position & 1) == 1 ? mIcon1 : mIcon2);

    return convertView;
}

ViewHolder leaks

Now back to memory leaks. If you notice the ViewHolder design pattern only works because the ViewHolder is stored in the parent's tag field. Depending on what you store in your ViewHolder, this may cause a circular reference. To avoid the leak you need to break this circular reference when you are done with view. Better solution is to avoid bloating your ViewHolder by just storing view childern and no other objects. The following is a ViewHolder which leaks because it creates a circular reference. The ViewHolder was extended to hold a pointer to the Parent (row) object. A leak will also occur if the ViewHolder is non-static inner class.

// Example - Leaking ViewHolder 

// ViewHolder must be static class to avoid leaking. 
public static class ViewHolderLeak {
    TextView text;
    ImageView icon;
    View parent;    // < -- hold parent forms circular reference.
}

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolderLeak holder;

    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.list_item_icon_text, parent, false);
        holder = new ViewHolderLeak();
        holder.text = (TextView) convertView.findViewById(R.id.text);
        holder.icon = (ImageView) convertView.findViewById(R.id.image);
        holder.parent = convertView;
        
        convertView.setTag(holder);  // < -- Circular reference, may prevent memory from releasing.
    } else {
        holder = (ViewHolder) convertView.getTag();
    }

    holder.text.setText(DATA[position]);
    holder.icon.setImageBitmap(position & 1) == 1 ? mIcon1 : mIcon2);

    return convertView;
}

Avoid ViewHolder Bloat

Avoid bloating up the ViewHolder with extra objects that may create circular references. Read the following for an alternative to using a ViewHolder and read its comments for futher refinement..
   ViewHolder considered harmful [by: Barend Garvelink]


How to avoid a leak

The previous list of ways to "make a leak" is not a true source of leaks in most code. Some of the examples will quickly break their relationship and no longer be at risk of leaking. The bad leaks are only those references which live beyond the LIFE of your class or beyond the point when you expect the objects to stay active. Remember there are two categories of leaks .

To avoid leaking memory you need to break all references back to the root allocation. The process is to reverse the actions you performed during class creation. If during the creation, you:

  1. Registered callbacks
  2. Set/Add listeners
  3. Post/PostDelay/Send/SendDelay Runnables or Messages
  4. Start Threads or AsyncTasks
  5. Create and attach ViewHolders
To avoid memory leaks reverse these actions. The reverse logic is added to the appropriate LIFE cycle state method. The table below lists some of the common Android objects and their typical LIFE cycle state methods where you execute cleanup logic. For you own custom classes you may need to emulate Android LIFE cycle and provide methods to create and release resources.
ObjectLife Cycle State Method Diagram
ActivityonStop() or onDestroy() Activity Life Cycle Diagram
FragmentonDestroyView(), onDestroy() or onDetach() Fragment Life Cycle Diagram
ViewonDetachedFromWindow() View Life Cycle Diagram
RecyclerViewonDetachedFromRecyclerView() or onViewRecycled() Recycler Life Cycle Diagram

The following is a custom layout which has several circular references. By breaking these in the destroy LIFE state onDetachFromWindow() method the object memory leaks are reduced.


// Example custom layout which implements onDetachFromWindow() 
// to avoid oject (memory) leaks.
// 
// Example has Listener (callbacks) and Containers (ArrayList) which
// hold Anonymous classes causing circular references. 
// 
// Class also has a Handler which may have pending messages.
//

public class LeakLayout extends FrameLayout {

    OnClickListener mClickListener;
    
    private final List<SwipeListener> mSwipeListeners = new ArrayList<>();
    private final Map<View, Rect> mViewBoundCache = new HashMap<>();

    private final Handler mHandler = new Handler();
    public final int MSG_SWIPE_LEFT = 1;
    public final int MSG_SWIPE_RIGHT = 2;
    
    public interface SwipeListener {
        void onOpen(LeakLayout layout);
        void onClose(LeakLayout layout);
    }

    public LeakLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void setOnClickListener(OnClickListener listener) {
        // Store listener in parent and in our private container.
        // Note - clear both in destroy LIFE state to avoid leaks.
        
        super.setOnClickListener(listener);
        mClickListener = listener;
    }
    
    public void addSwipeListener(SwipeListener listener) {
        // Note - clear container in destroy LIFE state to avoid leaks.
        
        mSwipeListeners.add(listener);
    }
    
    public void postMessage(int what, int when) {
        // Note - any pending messages will need to be removed
        // in destroy LIFE state to avoid leaks.
        mHandler.obtainMessage(what, when).sendToTarget();
    }
    
    /**
     * Get children's bounds.
     */
    private void captureChildrenBound() {
        for (int idx = 0; idx < getChildCount(); idx++) {
            View child = getChildAt(idx);
            mViewBoundCache.put(child, child.getClipBounds());
        }
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        
        // Create anonymous classes - creating circular references to parent.
        
        if (mClickListener == null) {
            setOnClickListener(new OnClickListener() {
                @Override
                public void onClick(View v) {
                    // performAdapterViewItemClick();
                }
            });
        }
    
    }

    @Override
    protected void onDetachedFromWindow() {
        // Destroy LIFE state - avoid memory leaks by:
        //  1. Remove all pending messages or runnables (always do first)
        //  2. Clear callbacks: unregister or set callbacks to null
        //     ** Depending on case, this may be redundant with step#3
        //  3. Set all objects with extra memory references to null.
        //     Setting to null may be enough to break multiple refs, making step#2 optional.
        //  4. Callup to super

        mHandler.removeMessages(MSG_SWIPE_LEFT);
        mHandler.removeMessages(MSG_SWIPE_RIGHT);
        
        setOnClickListener(null);
        setOnLongClickListener(null);
        
        mSwipeListeners.clear();
        mViewBoundCache.clear();

        super.onDetachedFromWindow();
    }
}
        


General Notes

When doing your cleanup, focus on breaking the memory references which are held outside the allocated classes. A single break in the reference chain is enough to allow GC to reclaim an object chain, unless it has multiple references (paths) back to the root allocation.

If an object has an array of listener callbacks and the object is held by multiple users, you may have to remove ALL of the listener callbacks. But if the object is only held by one object, only that object has a reference, so you either need to remove its associated listener callback OR set the object to null inside the object.


// Owner creates circular reference when addChild is called.
class Owner {
    Child mChild;
    
    public void addChild(Child child) {
        mChild = child;
        mChild.addOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                // do something special
            }
        });
    }
}

// Worker creates a one-to-many circular reference.
class Worker {
    Child mChild = new Child();
    List<Owner> owners = new ArrayList();

    public void addOwners(Owner owner) {
        owners.add(owner);
        owner.addChild(mChild);
    }
}
    

In the above case every Owner instance has a pointer to a Child object which in turn has a clickListener which has a pointer to the Owner (anonymous class has pointer to outer class). Assuming addOnClickListener is storing the listeners in a list we have a single Child instance which is causing every Owner instantance to leak until the circular references are broken.

To free the memory either remove listener from every Owner or clear the Child in every Owner. Assuming the Child view object is also in a layout, clearing the Child from the Owner will not release the memory until the Child is released from its parent layout. The first solution will allow immediate release of the Owner objects once Worker clears its list or goes out of scope.


// Updated - to provide ways to cleanup and avoid leaks.

// Owner creates circular reference when addChild is called.
class Owner {
    Child mChild;
    View.OnClickListener mListener;  // Save listener to use in remove
    
    public void addChild(Child child) {
        mChild = child;
        mListener = new View.OnClickListener() {
            @Override public void onClick(View v) {
                // do something special
            }
        });
        mChild.addOnClickListener(mListener);
    }
    
    // Required for cleanup option #2
    public void removeChild() {
        mChild = null;
    }
    
    // Required for cleanup option #3, use previously saved listener
    public void removeListener() {
        mChild.removeOnClickListener(mListener);
    }
}
class Worker {
    Child mChild = new Child();
    List<Owner> owners = new ArrayList();

    public void addOwners(Owner owner) {
        owners.add(owner);
        owner.addChild(mChild);
    }
    
    public void onDestroy() {
        // or option #1  (if a remove All is available)
        mChild.removeAllListeners();
        
        // or option #2, remove child from Owners
        // Memory not freed until Child view also released from parent layout.
        for (Owner owner : owners) {
            owner.rmoveChild();
        }
        
        // or option #3
        for (Owner owner : owners) {
            owner.removeListener();
        }
    }
}
    
The above code could also be re-written in several ways to avoid any cleanup code:
  1. Use a static inner or external class as Listener to Child. The anonymous class is causing the circular reference so a static class will avoid this unless you add a variable in the static class which points to the parent.
  2. Remove a local copy of Child from Owner. This is only leak-free if there are no other circular references. If the Child view object is in a layout and the layout is held in a variable by the owner, you are back to a circular reference.

    
    // Owner does not hold a copy of Child to avoid circular reference.
    class Owner {
        public void addChild(Child child) {
            child.addOnClickListener(new View.OnClickListener() {
                @Override public void onClick(View v) {
                    // do something special
                }
            });
        }
    }
    

Don't chain calls to LIFE cycle methods

Your cleanup code executed in the destroy LIFE state should be limited to objects which contribute to circular references and you should not go crazy setting all objects to null. You should definitely not call LIFE cycle methods on your member objects unless you know you are in sole control of the objects life cycle.

For example, if you customize a Layout and you have some member View objects which are also stored in the Layout. You can set your member view objects to null but don't call onDetachFromWindow() on them because the Android LIFE cycle will perform the same task. Double calling these methods can create side effects.


@Override
protected void onDetachedFromWindow() {
    // Chaining LIFE cycle into children is NOT recommended. 
    mCustomView.onDetachedFromWindow();

    // Clearing Listener (callbacks) is recommend.
    mCustomView.setOnClickListenern(null);

    // Setting complex objects to null is recommend.
    mCustomView = null;

    // Callup up to super is mandatory. 
    super.onDetachedFromWindow();
}
        

Avoid pending or delayed Messages or Runnables

If you use postDelayed() or sendDelayed APIs be ready to remove these from their parents. On View objects you can call cancelPendingInputEvents().

On a Handler you have to remove the messages by object instance or by what value. Handler removeMessage(int what). You can use remove CallbacksAndMessages(null) to remove pending anonymous Runnables.


void update() {
    // Example of delayed messange and post
    mUIHandler.sendEmptyMessageDelayed(MSG_ON_ITEM_CHANGED, ON_ITEM_CHANGED_DELAY);
    
    mRecyclerView.postDelayed(new Runnable() {
            @Override
            public void run() {
                mRecyclerView.smoothScrollToPosition(position);
            }
        }, 200);
}

@Override
protected void onDetachedFromWindow() {
    // Example of cleanup
    mUIHandler.removeMessages(MSG_ON_ITEM_CHANGED);
    
    // Remove pending anonymous Runnables.
    mUIHandler.removeCallbacksAndMessages(null);
        
    mRecyclerView.cancelPendingInputEvents();
    mRecyclerView = null;
}
    

Avoid private Handlers

Because Handler can contribute to memory leaks you should avoid using them where possible. If you just need to execute a simple action on the UI thread, consider using runOnUiThread(Runnable). Also look at this list of possible options: Stack overflow - running code in ui thread

See also:

Pair construction with destruction

Every time you use an object.addXXXX(objRef) or object.setXXXX(objRef) make sure your destroy LIFE cycle logic reverse the process with a one-to-one pair of the actions.


void init() {
    // Creation - 
    //   1. Setup click on button
    //   2. Add Listener to media player.
    
    mButton1 = findViewById(R.id.at_button1);
    mButton1.setOnClickListener(new View.OnClickListener() {
        @Override public void onClick(View v) {
            // do something special
        }
    });
    mMediaPlayer = new ExoMediaPlayer(RendererBuilderFactory.createRendererBuilder(getContext(), new Media(mUri), true), mUri);
    mMediaPlayer.addListener(playerListener);
}

@Override
protected void onDetachedFromWindow() {
    // Example of cleanup, reverse construction
    //   1. Clear anonymous click listener
    //   2. Remove Media Player listener
    
    mButton1.setOnClickListener(null);
    
    mMediaPlayer.removeListener(playerListener);
    mMediaPlayer.release();
    mMediaPlayer = null;
}
    

ViewTreeObserver listeners are hard to clear

I believe Android has a bug with how it implemented the ViewTreeObserver listeners. The ViewTreeObserver class has a custom internal implementation of a CopyOnWriteArrayList to make it possible for concurrent threads to update the listener lists, example method using custom list is: The problem is the custom CopyOnWriteArrayList maintains two internal lists. One list that is used for dispatching notification and an optional second list holds any pending changes. The two lists are managed by calls to start() and end(). The ViewTreeObserver calls start() prior to iterating on the listeners which causes the optional second list to replace the main list. The problem with this work flow is:
  1. Create object which has ViewTreeObserver
  2. Add listener (goes in 2nd list)
  3. ViewTreeObserver performs action on list, calls start() causing 2nd list to replace main list.
  4. Remove listener (2nd list created and remove executed on 2nd list, primary list still holds listener being removed)
  5. View object is nolong used resulting in possible memory leak if listener has anonymous class or non-static inner class.
In summary - the custom CopyOnWriteArrayList leaks listeners unless you force a call to start().


// Example ViewTreeObserver leaking listener
HorizontalScrollView mScrollView;
    
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mFragmentView = inflater.inflate(R.layout.fragment, container, false);
    mScrollView = (HorizontalScrollView)mFragmentView.findViewById(R.id.console_card_list_horz);

    mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            // Do something...
        }
    });
Because the listener is anonymous we have a parent reference. Lets try and reduce the memory references in the cleaup code onDestroyView().


// Example trying to avoid memory leak by removing listener (still leaks due to flaw in ViewTreeObserver)
HorizontalScrollView mScrollView;
ViewTreeObserver.OnGlobalLayoutListener mGlobalListener;
    
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    mFragmentView = inflater.inflate(R.layout.fragment, container, false);
    mScrollView = (HorizontalScrollView)mFragmentView.findViewById(R.id.console_card_list_horz);

    mGlobalListener = new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            // Do something...
        }
    };
    mScrollView.getViewTreeObserver().addOnGlobalLayoutListener(mGlobalListener);
    
     
@Override
public void onDestroyView() {
    // Still leaks because of flaw in Android with latency on releasing listener.
    
    mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalListener);
}                

The above leaks due to the internal flaw in ViewTreeObserver and the only fix is to call the appropriate dispatch method for your listener to cause the internal lists to update.

Listener Dispatch
addOnDrawListener
removeOnDrawListener
dispatchOnDraw
addOnGlobalFocusChangeListener
removeOnGlobalFocusChangeListener
[package] dispatchOnGlobalFocusChange
addOnGlobalLayoutListener
removeOnGlobalLayoutListener
dispatchOnGlobalLayout
addOnPreDrawListener
removeOnPreDrawListener
dispatchOnPreDraw
addOnScrollChangedListener
removeOnScrollChangedListener
[package] dispatchOnScrollChanged
addOnTouchModeChangeListener
removeOnTouchModeChangeListener
[package] dispatchOnTouchModeChanged
addOnWindowAttachListener
removeOnWindowAttachListener
[package] dispatchOnWindowAttachedChange
addOnWindowFocusChangeListener
removeOnWindowFocusChangeListener
[package] dispatchOnWindowFocusChange
addOnWindowShownListener
removeOnWindowShownListener
dispatchOnWindowShown


// Example which does not leak ViewTreeObserver listener.
@Override
public void onDestroyView() {
    // Leaks because of flaw in Android with latency on releasing listener.
    mScrollView.getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalListener);
    
    // Force internal custom concurrent list to switch to new list and free listener.
    // Listener is now gone, breaking circular reference, avoiding memory leak.
    mScrollView.getViewTreeObserver().dispatchOnGlobalLayout();
}   
Here is code fragments from Android API 25 which shows the problem:


//
//  Code extracted from ViewTreeObserver API 25 
//  ** Custom concurrent CopyOnWriteArray with two lists **
//

/**
 * Copy on write array. This array is not thread safe, and only one loop can
 * iterate over this array at any given time. This class avoids allocations
 * until a concurrent modification happens.
 *
 * Usage:
 *
 * CopyOnWriteArray.Access<MyData> access = array.start();
 * try {
 *     for (int i = 0; i < access.size(); i++) {
 *         MyData d = access.get(i);
 *     }
 * } finally {
 *     access.end();
 * }
 */
static class CopyOnWriteArray<T> {
    private ArrayList<T> mData = new ArrayList<T>();
    private ArrayList<T> mDataCopy;    // <--- Latency due to this 2nd list

    private final Access<T> mAccess = new Access<T>();

    private boolean mStart;

    static class Access<T> {
        private ArrayList<T> mData;
        private int mSize;

        T get(int index) {
            return mData.get(index);
        }

        int size() {
            return mSize;
        }
    }

    CopyOnWriteArray() {
    }

    private ArrayList<T> getArray() {
        if (mStart) {
            if (mDataCopy == null) mDataCopy = new ArrayList<T>(mData);
            return mDataCopy;
        }
        return mData;
    }

    // Replace active list with the temporary list. 
    Access<T> start() {
        if (mStart) throw new IllegalStateException("Iteration already started");
        mStart = true;
        mDataCopy = null;
        mAccess.mData = mData;
        mAccess.mSize = mData.size();
        return mAccess;
    }

    void end() {
        if (!mStart) throw new IllegalStateException("Iteration not started");
        mStart = false;
        if (mDataCopy != null) {
            mData = mDataCopy;
            mAccess.mData.clear();
            mAccess.mSize = 0;
        }
        mDataCopy = null;
    }

    int size() {
        return getArray().size();
    }

    // Create temporary list from active list, perform add on temporary list.
    void add(T item) {
        getArray().add(item);
    }

    // Create temporary list from active list, perform add on temporary list.
    void addAll(CopyOnWriteArray<T> array) {
        getArray().addAll(array.mData);
    }

    // Create temporary list from active list, perform remove on temporary list.
    void remove(T item) {
        getArray().remove(item);
    }

    void clear() {
        getArray().clear();
    }
}

// -----------------------------------------------------------------------------------------
// Code fragment of ViewTreeObserver 
class  ViewTreeObserver {

    // Non-recursive listeners use CopyOnWriteArray
    // Any listener invoked from ViewRootImpl.performTraversals() should not be recursive
    private CopyOnWriteArray<OnGlobalLayoutListener> mOnGlobalLayoutListeners;

    private boolean mAlive = true;

    /**
     * Register a callback to be invoked when the global layout state or the visibility of views
     * within the view tree changes
     *
     * @param listener The callback to add
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     */
    public void addOnGlobalLayoutListener(OnGlobalLayoutListener listener) {
        // checkIsAlive();

        if (mOnGlobalLayoutListeners == null) {
            mOnGlobalLayoutListeners = new CopyOnWriteArray<OnGlobalLayoutListener>();
        }

        mOnGlobalLayoutListeners.add(listener);     // <--- listener added to temporary internal list.
    }

    /**
     * Remove a previously installed global layout callback
     *
     * @param victim The callback to remove
     *
     * @throws IllegalStateException If {@link #isAlive()} returns false
     *
     * @see #addOnGlobalLayoutListener(OnGlobalLayoutListener)
     */
    public void removeOnGlobalLayoutListener(OnGlobalLayoutListener victim) {
        // checkIsAlive();
        if (mOnGlobalLayoutListeners == null) {
            return;
        }
        mOnGlobalLayoutListeners.remove(victim);    // <--- listener removed from temporary internal list only.
    }

    /**
     * Notifies registered listeners that a global layout happened. This can be called
     * manually if you are forcing a layout on a View or a hierarchy of Views that are
     * not attached to a Window or in the GONE state.
     */
    public final void dispatchOnGlobalLayout() {
        // NOTE: because of the use of CopyOnWriteArrayList, we *must* use an iterator to
        // perform the dispatching. The iterator is a safe guard against listeners that
        // could mutate the list by calling the various add/remove methods. This prevents
        // the array from being modified while we iterate it.
        final CopyOnWriteArray<OnGlobalLayoutListener> listeners = mOnGlobalLayoutListeners;
        if (listeners != null && listeners.size() > 0) {
            CopyOnWriteArray.Access<OnGlobalLayoutListener> access = listeners.start(); // <--- use temporary list. 
            try {
                int count = access.size();
                for (int i = 0; i < count; i++) {
                    access.get(i).onGlobalLayout();
                }
            } finally {
                listeners.end();
            }
        }
    }

}

Threads and AsyncTasks

Creating anonymous Thread or AsyncTask objects is identical to anonymous classes. They create a reference to the parent class. Thread activity can be a big source of leaks because they can out live the parent class. Best pratice is to make a static class which extends the Thread, Runnable or AsyncTask and create an instance in your class. During cleanup, stop any pending or active activity on the object and optionally call its cleanup method before setting the member to null.


// Example of possible leaking thread if it never completes
// or runs longer then you expect. 

ImageView imageView = (ImageView)parent.findViewById(R.id.image1);
String image_url = "http//www.gotimages.com/nice.jpg"
new Thread(new Runnable() 
{
    @Override
    public void run() {
        try {
            final Bitmap bitmap = BitmapFactory.decodeStream((InputStream) new URL(image_url).getContent());
            imageView.post(new Runnable() {
                @Override
                public void run() {
                   imageView.setImageBitmap(bitmap);
                }   
            }); 
        } catch (Exception ex)  {
            // TODO: handle exception
        }
    }
}).start();
    
Better to store Thread instance in a variable so you can stop it.


// AVOID leaking thread by keeping hold of thread in a variable, 
// so you can stop execution and cleanup.

void update() {
    ImageView imageView = (ImageView)parent.findViewById(R.id.image1);
    String image_url = "http//www.gotimages.com/nice.jpg"
    mWorkerThread = new Thread(new Runnable() 
    {
        @Override
        public void run() {
            try {
                final Bitmap bitmap = BitmapFactory.decodeStream((InputStream) new URL(image_url).getContent());
                imageView.post(new Runnable() {
                    @Override
                    public void run() {
                       imageView.setImageBitmap(bitmap);
                    }   
                }); 
            } catch (Exception ex)  {
                // TODO: handle exception
            }
        }
    });
    mWorkerThread.start();
}

@Override
protected void onDetachedFromWindow() {
    mImageView.cancelPendingInputEvents();
    mWorkerThread.interrupt();
    mWorkerThread = null;
    mImageView = null;
}

    

Switching threads - can occur after object is destroyed

If you have an object which registers with a data provider and you correctly unregister the callback in your destory LIFE cycle state, you may still get


How to find memory leaks

The process to find memory leaks in your code requires you either intstrument your program to count key objects or you use tools to monitor and analyze your memory.

I used the built-in memory monitor in Android Studio to visualize the active allocate memory heap and the Heap Profiler view to see the outstanding active object counts and size. Once I found and fixed a leak I used dumpsys meminfo <package name> to capture a memory report before (with the leak) and after (leak removed) to verify the changes.

Dumpsys MemInfo is a nice way to get a summary of the current state of memory resources in your package.


    Syntax:
    adb shell dumpsys meminfo <package-name> > save-results.txt

    Example:
    adb shell dumpsys meminfo com.wmur.android.weather  > wmur.txt
    
    On Unix/Linix/OSX you can continuously monitor using the 'watch' command:
    watch "adb shell dumpsys meminfo com.wmur.android.weather"
    
    A DOS/Windows version of watch can be found here:
    llwatch
    

Sample reported generated from dumpsys meminfo.
The key spots I monitor are the Native Heap Alloc, Java Heap summary and Views count.


Applications Memory Usage (kB):
Uptime: 99778973 Realtime: 780584992

** MEMINFO in pid 24644 [com.wmur.android.weather] **
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
                 Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
  Native Heap    28618    28572        0        0    46336    40903     5432
  Dalvik Heap    22969    22896        0        0    41688    27812    13876
 Dalvik Other     2752     2752        0        0                           
        Stack     1440     1440        0        0                           
       Ashmem     1060     1016        0        0                           
      Gfx dev    22688    20776        0        0                           
    Other dev       12        0       12        0                           
     .so mmap    19402      284    12196        0                           
    .apk mmap     2177        0     1716        0                           
    .ttf mmap      251        0      216        0                           
    .dex mmap    21717       32    18296        0                           
    .oat mmap     2948        0     1224        0                           
    .art mmap     1895     1344      292        0                           
   Other mmap      878       16      444        0                           
   EGL mtrack    45520    45520        0        0                           
      Unknown    10940    10940        0        0                           
        TOTAL   185267   135588    34396        0    88024    68715    19308
 
 App Summary
                       Pss(KB)
                        ------
           Java Heap:    24532
         Native Heap:    28572
                Code:    33964
               Stack:     1440
            Graphics:    66296
       Private Other:    15180
              System:    15283
 
               TOTAL:   185267      TOTAL SWAP (KB):        0
 
 Objects
               Views:      316         ViewRootImpl:        1
         AppContexts:        6           Activities:        1
              Assets:        7        AssetManagers:        5
       Local Binders:       64        Proxy Binders:       37
       Parcel memory:       42         Parcel count:      172
    Death Recipients:        5      OpenSSL Sockets:        7
 
    

Using egrep (enhance grep, or grep with -E) you can limit the output of meminfo to the key parts.


    adb shell dumpsys meminfo com.wmur.android.weather | egrep '(Heap|Views)'
    
    
                   Pss  Private  Private  Swapped     Heap     Heap     Heap
  Native Heap    28594    28548        0        0    47872    40205     7666
  Dalvik Heap    29137    29064        0        0    43954    38224     5730
           Java Heap:    30700
         Native Heap:    28548
               Views:      316         ViewRootImpl:        1
    

List of Android Memory Tools: