Combining Android Studio with MAT detection and simple analysis of memory leaks

Posted by Groogy on Wed, 22 May 2019 19:30:25 +0200

1. What is a GC?

Before analyzing memory leaks, you should first understand GC, which is commonly referred to in Java as Garbage Collection, referring to JVM automatically recycling memory data that is not referenced.

2. What is GC Roots?

GC Roots is the set of objects currently living in the Java virtual machine, each of which can act as a GC Root.
When determining whether an object needs to be recycled, accessibility analysis algorithms are often used, that is, to determine whether an object can be recycled by determining whether its reference chain is reachable, as shown in the following figure:

In the figure above, from GC Root to ObjectC, ObjectE, ObjectF are not reachable, so these three objects are objects that will be recycled by GC.

3. What is a memory leak?

A memory leak means that some objects that have not been used are still in memory and cannot be recycled by the GC.If they are allowed to reside in memory, the performance of the program will be affected.
The root cause of memory leaks is that objects still have an accessible path to the GC Root when they are not in use, which prevents the GC from recycling them properly, as shown in the following figure:

ObjectC s that are no longer available are still indirectly connected to the GC Root, which can cause the GC to fail to recycle them, resulting in a memory leak.

4. How do I detect and locate memory leaks?

Here are two main ways to locate memory leaks:

4.1 Use Memory from Android Monitor in Androd Studio

As follows:

First, let's see how it normally works:
Add a button to MainActivity and bind it to the following click events:

public class MainActivity extends AppCompatActivity {
    Button mButton;
    List<TextView> List = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.btn_add);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i = 0 ; i<= 20000;i++){
                    TextView textView = new TextView(MainActivity.this);
                    List.add(textView);
                }

            }

        });

    }
}

If you click Button at this time, the memory will change as follows:

If you press the return key, exit MainActivity, and manually call the GC (click the pickup sign), the memory will change as follows:

You can clearly see that memory has fallen after GC, indicating that the useless TextView and Array List objects have been reclaimed by GC normally.
Build a memory leak scenario where a static Context object holds a reference to the current Activity.(Of course, most of them won't be encountered in actual development)

public class MainActivity extends AppCompatActivity {
    static Context mContext;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mButton = (Button) findViewById(R.id.btn_add);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                for (int i = 0 ; i<= 20000;i++){
                    TextView textView = new TextView(MainActivity.this);
                    List.add(textView);
                }

            }

        });
        mContext = this;
    }
}

Do the same thing as you just did, click Button first, then click the Return key to exit the Activity, and then manually GC to find that memory stays high:

At this point, click the Dump Java Heap button as shown, wait for Dump to finish, and automatically go to the analysis interface:

Click the small green triangle first, then click Leak Activities to see where there is a memory leak and where it is.
From the information in the diagram, it is clear that there is a memory leak in the mContext, and then you can go back to the code to find the mContext to work around this problem.

4.2 Using MAT

MAT (Memory Analyzer) is a powerful Java heap memory analysis tool that can quickly analyze memory conditions. It was introduced by eclipse.It does not tell us the exact location of the memory leak directly, it only gives some memory and reference information, which we need to use to troubleshoot potential memory leaks, suitable for more complex memory leaks.

4.2.1 Get the hprof file

It also needs the help of hprof file to analyze memory, so first you have to get the hprof file. There are two ways to get it, one is Dump Java Heap in Android Monitor you just used, but the hprof file you get needs a little conversion to be used by MAT, as shown in the following figure:

Another way is to obtain the hprof through DDMS, as shown in the figure:


To open DDMS, first select the heap tag, then select the package name of the current application in the box on the left, then click updata heap (small green cylinder) above, then click Operate App (click button, then press Return), then click the Cause GC button on the right, and then click Dump HPROF file (small green cylinder with red arrow) to export the hprof text.Parts.
Similarly, the hprof file obtained this way also needs to be converted, using the hprof-conv command, as shown in the following figure:


The format is: hprof-conv +input file path + output file path and name

4.2.2 Use MAT to troubleshoot memory leaks


Before sorting out, learn a few nouns:
Dominator Tree: Dominator Tree, listing each object, size, and often used to analyze reference structures between objects.(from the perspective of an instance)
Histogram: A histogram that lists the name, size, and number of each object in memory.(standing at the angle of the class)
Shallow heap: The amount of memory consumed by the object itself.
Retained Heap: The total memory occupied by this object and other references it holds, both direct and indirect.
outgoing references: An object referenced by the current object.It allows you to see which objects you are currently "grabbing" on.
incoming references: An object that references the object.It lets you see who is "grabbing" the current instance in memory.

Take another look at a common memory leak scenario:

public class MainActivity extends AppCompatActivity {
    static LeakClazz mLeakClazz;
    List<TextView> List = new ArrayList<>();
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
         //This for loop is just to increase memory usage and make memory more visible
        for (int i = 0 ; i<= 10000;i++){
            TextView textView = new TextView(MainActivity.this);
            List.add(textView);
        }

        if (mLeakClazz == null){
            mLeakClazz = new LeakClazz();
        }
    }

    class LeakClazz{

    }
}

It is well known that a non-static internal class holds references to external classes, but a static instance mLeakClazz is created here. As long as the app process exists, mLeakClazz exists, so the external class MainActivity is always referenced, even if it is no longer in use.This causes a memory leak.After getting the hprof file, let's analyze the memory usage.
First open the obtained leak.hprof file:

Then click on the Dominator Tree to see the reference structure between objects:

We can use the regular search function to directly search for references related to MainActivity:

Find the paths that connect to the GC Roots, and filter out the GC Roots directly because soft, weak, and virtual references do not affect their operation:

This will find the location of the memory leak:

At this point, a place where you can modify memory leaks in your code.The whole process seems simple. Here's a simple demonstration, but experience and tools are needed in actual development to locate memory leaks. Here's a summary of common memory leaks:
1. Create a static instance of an internal class that is not static inside
The example above has already been analyzed.
2. Singleton mode holds references to external objects
Singleton objects always exist as static variables in the JVM. If you have references to external objects, they will not be recycled by the GC, especially when contexts are needed in the singleton mode, if you can use the contexts of the Application, you will try to use them, if not, you will have to consider whether you can use soft references or weak references.Here is a scenario where a single-case pattern may cause a memory leak:

public class Singleton {
    public Context mContext;
    private Singleton(){}
    private static class Holder{
        private static Singleton INSTANCE = new Singleton();
    }
    public Singleton getINSTANCE(){
        return Holder.INSTANCE;
    }

    public void doSomething(Context context){
        mContext = context;
    }
}

If the doSomething(Context context) method is called with the Context passed into the Activity, a memory leak is inevitable.
3. Using threads for time-consuming operations does not align with the life cycle of the Activity
Threads use internal classes or Runnable implementations because they hold references to external classes (Activities), which can cause memory leaks if the thread has not finished executing when the Activity exits.Like this:

public class MainActivity extends AppCompatActivity {

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

          //This for loop is just to increase memory usage and make memory more visible
        for (int i = 0 ; i<= 10000;i++){
            TextView textView = new TextView(MainActivity.this);
            List.add(textView);
        }

         //Anonymous internal classes hold references to external classes
        new Thread(new Runnable() {
            @Override
            public void run() {
                //Simulate a time-consuming operation
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

}

The solution is to end the thread when the activity is finished ():

public class MainActivity extends AppCompatActivity {
    Thread t;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        for (int i = 0 ; i<= 10000;i++){
            TextView textView = new TextView(MainActivity.this);
            List.add(textView);
        }

        t = new Thread(new Runnable() {
            @Override
            public void run() {
                Log.d("test", "run: ");
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        t.interrupt();
    }
}

4. Not shutting down in time after using some system resources
Be sure to shut down BraodcastReceiver, File, Cursor, Stream, Bitmap and other resources in time after using them, otherwise memory leaks will occur.

In summary, familiarizing yourself with some common memory leak scenarios and learning how to analyze memory leaks with tools can greatly avoid this problem in actual development.
In addition, leakcanary It's also somewhat easier to help us detect memory leaks, and it's also an artifact.

Topics: Java jvm Android Eclipse