How to analyze application memory in android (16) - use AS to view the Android heap

How to analyze application memory in android (16) - use AS to view the Android heap

Earlier, I first introduced how to use jdb and VS code to view application stack related content.
This article describes how to view the contents of the heap. About:

  1. What are the objects in the heap
  2. Objects in the heap, allocated by whom
  3. Objects in the heap, what is the reference relationship

What objects are in the heap and their reference relationships - use heap dump

To view the objects in the current heap, you need to use a tool to dump the heap data.

Next, we use the memory profiler that comes with Android studio to operate.

Step 1: Open the Android profiler.
In Android Studio, you can follow the steps below to open the memory profiler.
Insert image description here

In the picture above, two options appear, which are explained as follows:

  • profile xxx with low overhead: The performance analyzer will only enable the cpu performance analyzer and
    memory analyzer. In the memory analyzer, only Record native allocations is enabled
    (that is, the function of recording native allocations is enabled)
  • Profile xxx with complete data: Enable all analyzers, including CPU analyzer,
    memory analyzer, and power consumption analyzer.

Note: In addition to starting through the icon in the above figure, you can also start through the menu: Run->Profile. Then
select the module to be performance analyzed.

Note: On a computer with poor performance, you can run the Android profiler alone. As follows:
android studio installation directory/bin/profiler.xx (profiler.sh for mac, linux, profiler.exe for windows)

After startup, as shown below
Insert image description here

Note: A session represents a performance analysis, so you can create a new session in Android Profiler and select a different application process. As shown below:
Insert image description here

Step 2: Open Memory profiler

In the picture above, click on any area of ​​memory to open the memory profiler. The figure below shows the specific meaning of each area.

Insert image description here

As shown above, each type is explained as follows. You can also refer to the first article in this series: [How to analyze application memory in android (1) - Memory Overview] http://t.csdn.cn/HN1Ma

  • Java: memory of objects allocated by java or kotlin
  • Native: memory allocated by c or c++
  • Graphics: The memory used by the graphics buffer queue to display pixels to the screen, such as GL surfaces and GL textures. They are not GPU-specific memory, but memory shared with the CPU
  • Stack: Java stack and native stack memory, this is related to the number of running threads
  • Code: The memory used by the application to process codes and resources, such as dex bytecode, so library, fonts, etc.
  • Others: Unable to determine sorted memory
  • Allocated: The number of objects allocated by the application. In the picture above it is: N/A. That is, it cannot be counted. If it can be counted, a dotted line will be displayed, and the Y-axis corresponds to the right side of the image, indicating the number of objects.

Step 3: Use capture heap dump

To see how many objects are in the heap, use capture heap dump to capture the current heap. As shown below:
Insert image description here

As you can see from the picture above, there are a total of 727 classes, and all objects are listed in the list according to their class names.

The several marks in the above figure are explained as follows:

  • Mark 1: Select a different heap to view. There are the following heaps to view

    • image heap: Image heap, containing classes preloaded during startup. Allocations here do not move or disappear.
    • zygote heap: zygote heap, inherited from the zygote process, including system resources and class libraries.
    • app heap: The main heap allocated by the application
    • JNI heap: the heap referenced by jni

    Note: How to understand these four heaps, please see the later part of this article: How to understand Image heap, zygote heap, app heap, JNI heap

  • Mark 2: Choose different sorting methods, including the following:

    • arrange by class: Sort by class name
    • arrange by package: Sort by registration
    • arrange by callstack: Sort by call stack.

    Note: Sorted by call stack, this function is not supported in capture heap dump. To support this function, you need to use: Record java/kotlin allocations. See below: Who allocates objects in the heap?

  • Mark 3: Conditional filtering of classes, including the following:

    • show all classes: show all classes
    • show activity/fragments classes: Display possible Activity and fragment leaks

    Note: The word "possible" is used here. In fact, the leak shown by the memory profiler is not necessarily a real leak

    • show project classes: Display the classes in this project.
  • Mark 4: Allocations indicates the number of allocations. For example, the first row indicates that MaterialTextView is allocated once, that is, an object is allocated.

  • Mark 5: Native size indicates the native size of the object. Although there is only java code or kotlin code, in some cases, there will still be native size, because java can use jni to operate native memory. The first line in the above figure indicates that the native size is 0

  • Mark 6: shallow size indicates the size of the object itself, sometimes called flat size. It does not include the size of the internal reference object.

  • Mark 7: Retained size indicates the size of the object itself plus the size of the internal reference object. It can be directly understood as: if the object is recycled, the size of the heap will be released. The first line of the above figure indicates: if the MaterialTextView is recycled, 10381 bytes will be released.

Note: If object A refers to objects E and C, and object B also refers to objects E and C. So will the retained size of object A include E and C? Will the retained size of the B object include E and C? Regarding the calculation of retained size, please see the following article: How to calculate Shallow Size and retained size

  • Mark 8: Search box, the latter two check boxes indicate whether it is case-sensitive or not, and whether to use regular expressions.

Step 4: Check the reference relationships of each object

In order to illustrate the reference relationship, now write a linked list for testing. as follows

//首先定义一个测试类
public class WanbiaoTest{
    
    
    public String value = "wanbiao_test";
    public WanbiaoTest next ;
}
//链表的头,用字母o表示
private WanbiaoTest o ;
//构建测试链表
public void do(){
    
    
    for(int i=0;i<10;++i){
    
    
        if( o == null){
    
    
            o = new WanbiaoTest();
        }else{
    
    
            WanbiaoTest p = o;
            while(p.next != null){
    
    
                p = p.next;
            }
            p.next = new WanbiaoTest();
        }
    }
}

After running the above code, follow the first and second steps to dump the heap as shown below. Then follow the steps in the figure to view the reference
Insert image description here

In the picture above.

  1. By typing the class name, you can quickly locate the class you are looking for.
  2. Then after clicking on the class name, a list of objects appears.
  3. In the object list, we see various objects. Because WanbiaoTest objects are organized according to linked lists. So in the Depth column, there will be different depths.
  4. Select an object at will, and the detailed information of the object will appear on the right.
  5. Click the references column to view its references.

As you can see from the picture: the selected object is always referenced through next. The top-level WanbiaoTest is referenced by o, which exists in MainActivity. The MainActivity object exists in the ActivityThread$ActivityClient object.
The entire reference chain is as shown below
Insert image description here

If you want to view a specific object, you can also right-click the object and select: go to instance

Note: In addition to viewing the "reference chain to the nearest GC root", you can also view all references, that is, remove: show nearest GC root only to view all objects referenced by the object.

The above just shows how to view the reference relationships between objects. So how to determine whether the memory is leaked? In order to answer this question, we also need to first learn how to see who these objects are allocated by.

Who allocates objects in the heap - using Record java/kotlin allocations

If you want to know who allocated the object, you need to know the call stack that allocated the object. Therefore, you can follow the steps below to record the call stack information of the application. as follows:

Step 1: Record call stack information.
Start recording as shown below
Insert image description here

End recording as shown below
Insert image description here

The recording results are as follows
Insert image description here

Detailed information is marked in the figure. Unmarked places have been introduced previously.

There is one more thing to note here: there is a drop-down radio button next to the end recording button, which is currently Full. The available options are:

  • Full: Record all object allocations, which will cause a significant decrease in app performance
  • Sample: Samples memory allocations at a specific sampling interval (sampling rate). For detailed descriptions of sampling intervals and sampling rates, see: [How to analyze application memory in android (13) - perfetto] http://t.csdn.cn/laqYB : Why heapprofd has good performance

Step 2: View the object call stack

Select the class to be viewed, then the object list appears, and then select an object. As shown below
Insert image description here

At this point, you can view the call stack information of an object.

In addition to the table view introduced earlier, you can also view it through the flame graph. Select Visualization next to the table to switch to flame graph mode. As shown below
Insert image description here

In the flame graph above, the larger the span of the function, the larger the value corresponding to the selection (i.e. one of Allocation count, Allocation Size, Total Remaining Size, Total Remaining Count)

Since then, we have introduced how the tool can view objects in the heap, the reference relationships of the objects, and the call stack information of the objects.

Next, two small examples will be used as practical examples.

Comprehensive use of the above tools - actual combat 1, Activity leakage

In this example, we manually created an Activity leak. We consider the following scenarios:

  1. We have a device manager called DeviceManager. It is a singleton object.
  2. The device manager has two interfaces, which are used to register and destroy device status listeners. as follows
public class DeviceManager {
    
    
    //单例对象
    private DeviceManager() {
    
    
    }
    private static class DeviceManagerHolder {
    
    
        private static final DeviceManager INSTANCE = new DeviceManager();
    }

    public static final DeviceManager getInstance() {
    
    
        return DeviceManagerHolder.INSTANCE;
    }


   //定义监听器接口
    public interface DeviceChangedListener{
    
    
        void onChanged(int oldStatus,int newStatus);
    }

    private ArrayList<DeviceChangedListener> listeners = new ArrayList<>();

    public void addListener(DeviceChangedListener listener){
    
    
        if(!listeners.contains(listener)){
    
    
            listeners.add(listener);
        }
    }

    public void removeListener(DeviceChangedListener listener){
    
    
        listeners.remove(listener);
    }

}
  1. Now that we have our business object Class Task, we need to do the following operations. When the Task is created, register a listener with the DeviceManager. When the Task is destroyed, log out the listener from the DeviceManager. code show as below:
//Task自我监听,Device的状态改变 
//看上去这是一个较好的封装
public class Task implements DeviceManager.DeviceChangedListener {
    
    
    private Runnable mTaskRunnable;
    public Task(Runnable task){
    
    
        mTaskRunnable = task;
        DeviceManager.getInstance().addListener(this);
        task.run();//运行其他业务
    }
    @Override
    protected void finalize(){
    
    
      //回收的时候,注销掉监听器
        DeviceManager.getInstance().removeListener(this);
    }
    @Override
    public void onChanged(int oldStatus, int newStatus) {
    
    
        Log.i("Task","oldStatus = "+oldStatus+"newStatus = "+newStatus);
    }
}
  1. Next, create a task in Activity's onCreate and start executing it. code show as below:
@Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        //执行业务
        class TaskRunable implements Runnable{
    
    
            private Context mContext;
            public TaskRunable(Context context){
    
    
                mContext = context;
            }

            @Override
            public void run() {
    
    
                // do something
            }
        }

        Task t = new Task(new TaskRunable(this));
    }
  1. Next we simulate the user's operations.

We flipped the screen upside down a few times. Then force a GC call. As shown below
Insert image description here

Then use capture heap dump to see if there is a memory leak. As shown below
Insert image description here

From the picture above, we can see that the memory profiler has prompted Activity leaks.

Thinking: Why can the memory profiler detect Activity leaks?
Answer: Because after the Activity is destroyed, if it is still reachable from the GC root, it means a leak.

Here, we don’t know where the code causes the leak. In order to find the leak, we follow the steps in the figure below.
Insert image description here
From the picture above we can see the following call chain
Insert image description here

From this we found the reason for Activity leakage, TaskRunnable holds its strong reference. So if we change it to a weak reference, will it be better? as follows:

@Override
    protected void onCreate(Bundle savedInstanceState) {
    
    
        super.onCreate(savedInstanceState);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());
        //执行业务
        class TaskRunable implements Runnable{
    
    
        //将强引用改为弱引用
          private WeakReference<Context> mContext;
          public TaskRunable(Context context){
    
    
              mContext = new WeakReference<>(context);
          }

          @Override
          public void run() {
    
    
              // do something
          }
      }
    }

After changing to the above code, use Memory profiler again to perform heap dump. You will see the following results
Insert image description here

Very unfortunate! ! ! Still haven't resolved the leak. Where does this problem arise? Follow the steps above to view references and get the following picture:
Insert image description here

As you can see from the picture, there is a reference to this$0 inside the TaskRunable object pointing to MainActivity.

oh! ! , It turns out that this $0 is a reference from the inner class to the external object, so in order to solve this problem, move
class TaskRunable outside MainActivity.

Perform a heap dump again. This time, no activity is leaked! ! !

Comprehensive use of the above tools - actual combat 2, object leakage

Is there really no leakage in Actual Combat 1 above? What if the TaskRunnable contains a large object? How will it perform?

In order to simulate that TaskRunnable contains a large object, we add an integer array internally, as follows:

class TaskRunable implements Runnable{
    
    
    private WeakReference<Context> mContext;

    //1024*4=4096byte 等于4KB.模拟一个大对象
    private int[] values = new int[1024];
    public TaskRunable(Context context){
    
    
        mContext = new WeakReference<>(context);
    }

    @Override
    public void run() {
    
    
        // do something
    }
}

After a series of memory tests, it was found that Java memory has been increasing, and GC cannot recycle it (blue part), as shown below:
Insert image description here

Note: Regarding how to test the memory of Android applications, I will write about it later when I have time. This series focuses on how to analyze the memory. Fortunately, memory testing is relatively easy. In fact, you can follow: http://t.csdn.cn/ovFmO . Just write a real-time monitoring script. Of course, it can also be paired with the procstats service. See the procstats recommendation in the last section of http://t.csdn.cn/4Qp9t

In order to find this memory problem, we will use a heap dump and look at the internal details. As shown below:
Insert image description here

As you can see in the picture above, there is no indication that any objects have been leaked. So how to find this kind of leakage problem? We have observed that Java memory continues to increase. We suspect that an object in the heap has been allocated too many times and has not been released. To do this, we click on Allocations (that is, the number of allocations) to view it in reverse order.
Insert image description here

As you can see from the picture above, the categories with the most allocations are: int[], WeakReference, FinalizerReference, TaskRunable, and Task.
From the Shallow size, we can see that the highest one is int[]. It is almost certain that the object causing the memory leak is int[]. According to the above learning, let's take a look at its reference chain. as follows:
Insert image description here

As can be seen from the figure, the leak is caused by the objects registered to DeviceManager not being logged out in time.

In order to solve this problem, DeviceManager should remove listeners that are no longer used when appropriate. From the Task's finalize function, we can know that when the class is no longer used, the listener should be removed by the GC. Therefore, the listener of DeviceManager is designed as a weak reference. The code is too simple and is no longer attached.

After changing to weak reference, the object will no longer be leaked.

Note: The above code still cannot be regarded as engineering practice. Because when the Task is no longer used and the GC has not yet recycled, if there is indeed a status change in the DeviceManager, the Task will be notified. At this time, if the Task has corresponding processing logic, it may cause problems. Therefore, it should be handled with caution here. However, the above example is only to illustrate the use of memory tools.

The above two examples are too clear. They are just to illustrate the use of memory profiler. In fact, the real memory relationship may be much more complicated than the above. However, since the space will not be expanded, if there is a chance, it will be added later.

Before ending this article, there seem to be two small questions that need to be answered: First, what are Android's Image heap, zygote heap, app heap and JNI heap. Second: How to calculate retained size

How to understand Image heap,zygote heap,app heap,JNI heap

In order to illustrate these four heaps, we start with startup and then briefly summarize as follows

  1. When the Android system starts, a process is created called the zygote process. When the zygote process starts for the first time, it will load a lot of resources, including some system resources. Then run again.

  2. When Android wants to start another process, it will not start from scratch like zygote. Instead, fork the zygote process directly. Then some resources in the zygote process are reused, and a special heap of zygote will be reused. This heap is the heap where system resources are loaded in the first step. The name of this heap is called zygote heap. If the application needs to modify the contents of this heap, the application will create a new heap at this time, then copy the contents of the zygote heap to the new heap, and then modify it, that is: copy-on-write

  3. When the Android virtual machine starts, it needs to load the optimized bytecode. These optimized bytecodes are mapped to a special heap for direct use in the future. This heap is called Image heap

  4. When the Android application is started, objects need to be allocated, and objects are allocated from the app heap. This heap is the main heap of our application.

  5. When an Android application uses JNI references during use, these JNI references will be placed in a separate heap, which is the JNI heap

How to calculate Shallow Size and retained size

Shallow size: The size of the object itself, without calculating the size of its internal reference objects. as follows:

class A{
    
    
  int a;
  A aInner;
 }

Then the shallow size of object A = 4 (a is an int and occupies four bytes) + 4 (aInner is a reference and occupies four bytes) + 8 (a field in the Object object) = 16 bytes

Note: Sometimes, in order to ensure 4-byte alignment, when it is not a multiple of 4 bytes, it will also become a multiple of 4 bytes. Why four-byte alignment? This is due to the memory bus's purpose of improving memory access efficiency. Not shown here, you can search it on Baidu yourself

retained size: the size of the object itself, plus the size of its internal reference objects. But what if an object is referenced by multiple objects? As shown below
Insert image description here

To solve this problem, we need to know about the dominator tree.

Its definition is as follows: In a directed graph, if from the source point to point B, one must pass through point A anyway, then A is the dominating point of B, and A is said to dominate B. The dominating point closest to B is called the direct dominating point. As shown above. B is the direct control point of D and G.

According to the above relationship, the following dominance tree graph can be obtained (right picture)
Insert image description here

Description of the above picture:

  1. D directly controls: E, F
  2. B directly controls: D, G, H

那么reatined size就是根据这个支配树来计算。
E retained size = E shallow size
F retained size = F shallow size
H retained size = H shallow size
G retained size = G shallow size
D retained size= E shallow size+ F retained size + D shallow size
B retained size = D retained size + H retained size + G retained size + B shallow size

This is the end of this article.

The next article is still about Java's heap memory. We will use two other tools, namely perfetto and mat, to analyze heap memory. Stay tuned.

Guess you like

Origin blog.csdn.net/xiaowanbiao123/article/details/132153356