Android에서 애플리케이션 메모리를 분석하는 방법(16) - AS를 사용하여 Android 힙 보기

Android에서 애플리케이션 메모리를 분석하는 방법(16) - AS를 사용하여 Android 힙 보기

앞서 jdb 및 VS 코드를 사용하여 애플리케이션 스택 관련 콘텐츠를 보는 방법을 먼저 소개했습니다.
이번 글에서는 힙의 내용을 보는 방법을 소개하겠습니다. 에 대한:

  1. 힙에 있는 객체는 무엇입니까?
  2. 힙의 객체는 누구에 의해 할당됩니까?
  3. 힙에 있는 객체 간의 참조 관계는 무엇입니까?

힙에 있는 객체와 해당 참조 관계 - 힙 덤프 사용

현재 힙의 개체를 보려면 힙 데이터를 덤프하는 도구를 사용해야 합니다.

다음으로 Android 스튜디오와 함께 제공되는 메모리 프로파일러를 사용하여 작동합니다.

1단계: Android 프로파일러 열기
Android Studio에서 아래 단계에 따라 메모리 프로파일러를 열 수 있습니다.
여기에 이미지 설명을 삽입하세요.

위 그림에는 두 가지 옵션이 표시되며 이에 대한 설명은 다음과 같습니다.

  • 낮은 오버헤드가 있는 프로필 xxx: 성능 분석기는 CPU 성능 분석기와
    메모리 분석기만 활성화합니다. 메모리 분석기에서는 기본 할당 기록만 활성화됩니다
    (즉, 기본 할당 기록 기능이 활성화됩니다).
  • 전체 데이터가 포함된 프로필 xxx: CPU 분석기,
    메모리 분석기, 전력 소비 분석기를 포함한 모든 분석기를 활성화합니다.

참고: 위 그림의 아이콘을 통해 시작하는 것 외에도 Run->Profile 메뉴를 통해 시작할 수도 있으며 그런 다음
성능 분석할 모듈을 선택합니다.

참고: 성능이 낮은 컴퓨터에서는 Android 프로파일러만 실행할 수 있습니다. 다음과 같습니다:
android studio 설치 디렉터리/bin/profiler.xx (mac, linux의 경우 profiler.sh, Windows의 경우 profiler.exe)

시작 후 아래와 같이
여기에 이미지 설명을 삽입하세요.

참고: 세션은 성능 분석을 나타내므로 Android 프로파일러에서 새 세션을 생성하고 다른 애플리케이션 프로세스를 선택할 수 있습니다. 아래 그림과 같이:
여기에 이미지 설명을 삽입하세요.

2단계: 메모리 프로파일러 열기

위 그림에서 메모리의 아무 영역이나 클릭하면 메모리 프로파일러가 열립니다. 아래 그림은 각 영역의 구체적인 의미를 보여줍니다.

여기에 이미지 설명을 삽입하세요.

위와 같이 각 유형에 대해 설명하면 다음과 같습니다. 이 시리즈의 첫 번째 기사인 [안드로이드에서 애플리케이션 메모리를 분석하는 방법(1) - 메모리 개요] http://t.csdn.cn/HN1Ma를 참조할 수도 있습니다.

  • Java: Java 또는 Kotlin에 의해 할당된 객체의 메모리
  • 기본: c 또는 C++에 의해 할당된 메모리
  • 그래픽: GL 표면 및 GL 텍스처와 같은 픽셀을 화면에 표시하기 위해 그래픽 버퍼 큐에서 사용하는 메모리입니다. GPU 특정 메모리가 아니라 CPU와 공유되는 메모리입니다.
  • 스택: Java 스택 및 기본 스택 메모리. 이는 실행 중인 스레드 수와 관련됩니다.
  • 코드: dex 바이트코드, 라이브러리, 글꼴 등과 같은 코드 및 리소스를 처리하기 위해 애플리케이션에서 사용하는 메모리입니다.
  • 기타: 메모리 종류를 확인할 수 없습니다.
  • 할당됨: 애플리케이션에서 할당한 개체 수입니다. 위 그림에서는 N/A입니다. 즉, 셀 수 없는 것인데, 셀 수 있으면 점선으로 표시되며, Y축은 이미지의 오른쪽에 해당하여 개체의 개수를 나타냅니다.

3단계: 캡처 힙 덤프 사용

힙에 있는 개체 수를 확인하려면 캡처 힙 덤프를 사용하여 현재 힙을 캡처하세요. 아래 그림과 같이:
여기에 이미지 설명을 삽입하세요.

위 그림을 보면 알 수 있듯이 총 727개의 클래스가 있으며, 모든 객체는 클래스 이름에 따라 목록에 나열되어 있습니다.

위 그림의 여러 표시는 다음과 같이 설명됩니다.

  • 마커 1: 보려는 다른 힙을 선택합니다. 다음 힙을 볼 수 있습니다.

    • 이미지 힙: 시작 중에 미리 로드된 클래스를 포함하는 이미지 힙입니다. 여기의 할당은 이동되거나 사라지지 않습니다.
    • zygote 힙: 시스템 리소스 및 클래스 라이브러리를 포함하여 zygote 프로세스에서 상속된 zygote 힙입니다.
    • app heap: 애플리케이션이 할당한 메인 힙
    • JNI 힙: jni가 참조하는 힙

    참고: 이 네 가지 힙을 이해하는 방법은 이 문서의 뒷부분인 이미지 힙, zygote 힙, 앱 힙, JNI 힙을 이해하는 방법을 참조하세요.

  • 표시 2: 다음을 포함하여 다양한 정렬 방법을 선택하십시오.

    • 클래스별로 정렬: 클래스 이름별로 정렬
    • 패키지별로 정렬: 등록별로 정렬
    • 콜스택별로 정렬: 콜스택별로 정렬합니다.

    참고: 호출 스택별로 정렬된 이 기능은 캡처 힙 덤프에서 지원되지 않습니다. 이 기능을 지원하려면 다음을 사용해야 합니다. java/kotlin 할당을 기록합니다. 아래를 참조하세요. 누가 힙에 객체를 할당하나요?

  • 마크 3: 다음을 포함한 클래스의 조건부 필터링:

    • 모든 수업 표시: 모든 수업 표시
    • show Activity/fragments 클래스: 가능한 활동 및 조각 누출을 표시합니다.

    참고: 여기서는 "가능"이라는 단어가 사용되었습니다. 실제로 메모리 프로파일러에 표시되는 누수는 반드시 실제 누수가 아닐 수도 있습니다.

    • show project class: 이 프로젝트의 클래스를 표시합니다.
  • 표시 4: Allocations는 할당 횟수를 나타냅니다. 예를 들어 첫 번째 행은 MaterialTextView가 한 번 할당되었음을 나타냅니다. 즉, 개체가 할당됩니다.

  • 표시 5: 기본 크기는 개체의 기본 크기를 나타냅니다. Java 코드나 Kotlin 코드만 있지만 Java에서는 JNI를 사용하여 네이티브 메모리를 작동할 수 있으므로 네이티브 크기가 유지되는 경우도 있습니다. 위 그림의 첫 번째 줄은 기본 크기가 0임을 나타냅니다.

  • 표시 6: 얕은 크기는 개체 자체의 크기를 나타내며 때로는 플랫 크기라고도 하며 내부 참조 개체의 크기는 포함되지 않습니다.

  • 마크 7: 보유 크기는 객체 자체의 크기에 내부 참조 객체의 크기를 더한 것을 나타냅니다. 이는 객체가 재활용되면 힙 크기가 해제된다는 의미로 직접적으로 이해할 수 있습니다. 위 그림의 첫 번째 줄은 MaterialTextView가 재활용되면 10381바이트가 해제된다는 의미입니다.

참고: 객체 A가 객체 E와 C를 참조하고 객체 B가 객체 E와 C도 참조하는 경우 그렇다면 객체 A의 유지 크기에는 E와 C가 포함됩니까? B 객체의 유지 크기에 E와 C가 포함됩니까? 보유 크기 계산에 대해서는 다음 문서를 참조하십시오. 얕은 크기 및 보유 크기를 계산하는 방법

  • 마크 8: 검색 상자, 다음 두 개의 확인란은 대소문자 구분 여부와 정규식 사용 여부를 나타냅니다.

4단계: 각 객체의 참조 관계 확인

참조 관계를 설명하기 위해 이제 테스트용 연결 목록을 작성합니다. 다음과 같이

//首先定义一个测试类
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();
        }
    }
}

위 코드를 실행한 후 아래와 같이 첫 번째와 두 번째 단계에 따라 힙을 덤프합니다. 그런 다음 그림의 단계에 따라 참조를 확인하세요.
여기에 이미지 설명을 삽입하세요.

위의 그림에서.

  1. 수업명을 입력하시면 원하는 수업을 빠르게 찾으실 수 있습니다.
  2. 그런 다음 클래스 이름을 클릭하면 개체 목록이 나타납니다.
  3. 개체 목록에는 다양한 개체가 표시됩니다. WanbiaoTest 개체는 연결된 목록에 따라 구성되기 때문입니다. 따라서 Depth 열에는 다양한 깊이가 있습니다.
  4. 원하는 객체를 선택하면 오른쪽에 해당 객체의 상세 정보가 나타납니다.
  5. 해당 참조 항목을 보려면 참조 열을 클릭하세요.

그림에서 볼 수 있듯이 선택된 객체는 항상 next를 통해 참조됩니다. 최상위 WanbiaoTest는 MainActivity에 존재하는 o에서 참조됩니다. MainActivity 개체는 ActivityThread$ActivityClient 개체에 존재합니다.
전체 참조 체인은 아래와 같습니다.
여기에 이미지 설명을 삽입하세요.

특정 개체를 보려면 개체를 마우스 오른쪽 버튼으로 클릭하고 다음을 선택하면 됩니다. 인스턴스로 이동

참고: "가장 가까운 GC 루트에 대한 참조 체인"을 보는 것 외에도 모든 참조를 볼 수도 있습니다. 즉, 제거: 개체가 참조하는 모든 개체를 보려면 가장 가까운 GC 루트만 표시합니다.

위의 내용은 객체 간의 참조 관계를 보는 방법을 보여줍니다. 그렇다면 메모리 누출 여부를 확인하는 방법은 무엇입니까? 이 질문에 답하기 위해서는 먼저 누가 이러한 객체를 할당했는지 확인하는 방법도 배워야 합니다.

힙에 객체를 할당하는 사람 - java/kotlin 할당 기록 사용

누가 객체를 할당했는지 알고 싶다면 해당 객체를 할당한 콜스택을 알아야 하므로, 아래의 단계에 따라 해당 애플리케이션의 콜스택 정보를 기록하면 된다. 다음과 같이:

1단계: 호출 스택 정보를 기록하고
아래와 같이 기록을 시작합니다 .
여기에 이미지 설명을 삽입하세요.

아래와 같이 녹음을 종료합니다.
여기에 이미지 설명을 삽입하세요.

녹음 결과는 다음과 같습니다
여기에 이미지 설명을 삽입하세요.

자세한 정보는 그림에 표시되어 있으며, 표시되지 않은 곳은 앞서 소개한 곳입니다.

여기서 주목해야 할 사항이 한 가지 더 있습니다. 녹음 종료 버튼 옆에 드롭다운 라디오 버튼이 있으며 현재 전체 상태입니다. 사용 가능한 옵션은 다음과 같습니다.

  • 전체: 모든 개체 할당을 기록합니다. 이로 인해 앱 성능이 크게 저하될 수 있습니다.
  • 샘플: 특정 샘플링 간격(샘플링 속도)으로 메모리 할당을 샘플링합니다. 샘플링 간격 및 샘플링 속도에 대한 자세한 설명은 [Android에서 애플리케이션 메모리를 분석하는 방법(13) - perfetto] http://t.csdn.cn/laqYB : heapprofd의 성능이 좋은 이유를 참조하세요.

2단계: 객체 호출 스택 보기

보려는 클래스를 선택하면 개체 목록이 나타나며, 개체를 선택합니다. 아래 그림과 같이
여기에 이미지 설명을 삽입하세요.

이 시점에서 개체의 호출 스택 정보를 볼 수 있습니다.

위에서 설명한 테이블 보기 외에 플레임 그래프를 통해서도 볼 수 있습니다. 테이블 측면의 시각화를 선택하여 Flame 그래프 모드로 전환합니다. 아래 그림과 같이
여기에 이미지 설명을 삽입하세요.

위의 Flame 그래프에서 함수의 범위가 클수록 선택에 해당하는 값(예: 할당 개수, 할당 크기, 총 남은 크기, 총 남은 개수 중 하나)이 커집니다.

이후 도구가 힙의 개체, 개체의 참조 관계, 개체의 호출 스택 정보를 볼 수 있는 방법을 소개했습니다.

다음으로 두 가지 작은 예를 실제 사례로 사용하겠습니다.

위 도구의 종합적 활용 - 실전 1, 활동 유출

이 예에서는 활동 누수를 수동으로 생성했습니다. 우리는 다음 시나리오를 고려합니다.

  1. DeviceManager라는 장치 관리자가 있는데, 이는 싱글톤 개체입니다.
  2. 장치 관리자에는 장치 상태의 수신기를 등록하고 삭제하는 데 각각 사용되는 두 개의 인터페이스가 있습니다. 다음과 같이
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. 이제 비즈니스 개체 클래스 작업이 있으므로 다음 작업을 수행해야 합니다. 작업이 생성되면 DeviceManager에 리스너를 등록합니다. 작업이 삭제되면 DeviceManager에서 리스너를 로그아웃합니다. 코드는 아래와 같이 표시됩니다.
//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. 다음으로 Activity의 onCreate에서 작업을 생성하고 실행을 시작합니다. 코드는 아래와 같이 표시됩니다.
@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. 다음으로 사용자의 작업을 시뮬레이션합니다.

화면을 가로와 세로로 몇 번 반전시킵니다. 그런 다음 강제로 GC 호출을 수행합니다. 아래 그림과 같이
여기에 이미지 설명을 삽입하세요.

그런 다음 캡처 힙 덤프를 사용하여 메모리 누수가 있는지 확인합니다. 아래 그림과 같이
여기에 이미지 설명을 삽입하세요.

위 그림에서 메모리 프로파일러가 이미 활동 누출을 촉발했음을 알 수 있습니다.

생각: 메모리 프로파일러가 활동 누출을 감지할 수 있는 이유는 무엇입니까?
답변: 활동이 소멸된 후에도 GC 루트에서 여전히 접근할 수 있다면 이는 누수가 있음을 의미하기 때문입니다.

여기서는 코드가 누수를 일으킨 위치를 알 수 없으므로 누수를 찾기 위해 아래 그림의 단계를 따릅니다.
여기에 이미지 설명을 삽입하세요.
위 그림에서 다음 콜 체인을 볼 수 있습니다.
여기에 이미지 설명을 삽입하세요.

이를 통해 Activity 유출의 원인을 찾았으며 TaskRunnable은 이에 대한 강력한 참고 자료를 보유하고 있습니다. 그럼 약한 참조로 바꾸면 괜찮을까요? 다음과 같이:

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

위의 코드로 변경한 후 다시 메모리 프로파일러를 사용하여 힙 덤프를 수행합니다. 다음과 같은 결과가 표시됩니다.
여기에 이미지 설명을 삽입하세요.

매우 불행한 일입니다! ! ! 아직 누출에 대한 해결 방법이 없습니다. 이 문제는 어디에서 발생합니까? 위의 단계에 따라 참조를 보고 다음 그림을 얻으십시오.
여기에 이미지 설명을 삽입하세요.

그림에서 볼 수 있듯이 TaskRunable 개체 내부에는 MainActivity를 가리키는 this$0에 대한 참조가 있습니다.

오! ! , 이 $0는 내부 클래스에서 외부 개체에 대한 참조인 것으로 밝혀졌으므로 이 문제를 해결하려면
TaskRunable 클래스를 MainActivity 외부로 이동합니다.

힙 덤프를 다시 수행합니다. 이번에는 활동이 유출되지 않습니다! ! !

위 도구의 종합 활용 - 실전 2, 물체 유출

위의 Actual Combat 1에는 정말 누출이 없나요? TaskRunnable에 대형 개체가 포함되어 있으면 어떻게 되나요? 성능은 어떻게 되나요?

TaskRunnable에 대형 개체가 포함되어 있음을 시뮬레이션하기 위해 다음과 같이 내부적으로 정수 배열을 추가합니다.

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

일련의 메모리 테스트 후에 아래와 같이 Java 메모리가 증가하고 있으며 GC가 이를 재활용할 수 없음(파란색 부분)이 발견되었습니다.
여기에 이미지 설명을 삽입하세요.

참고: Android 애플리케이션의 메모리 테스트 방법에 대해서는 나중에 시간이 나면 작성하겠습니다. 이번 시리즈에서는 메모리 분석 방법에 중점을 두었습니다. 다행히도 메모리 테스트는 비교적 쉽습니다. 실제로 http://t.csdn.cn/ovFmO 를 따라갈 수 있습니다 . 실시간 모니터링 스크립트를 작성하면 됩니다. 물론 procstats 서비스와도 페어링할 수 있습니다. http://t.csdn.cn/4Qp9t 마지막 섹션의 procstats 권장 사항을 참조하세요.

이 메모리 문제를 찾기 위해 힙 덤프를 사용하고 내부 세부 사항을 살펴보겠습니다. 아래 그림과 같이:
여기에 이미지 설명을 삽입하세요.

위 사진에서 볼 수 있듯이 어떤 물체도 유출된 흔적은 없습니다. 그렇다면 이런 종류의 누출 문제를 어떻게 찾을 수 있을까요? 우리는 Java 메모리가 계속해서 증가하는 것을 관찰했으며 힙에 있는 객체가 너무 많이 할당되어 해제되지 않은 것으로 의심합니다. 이를 위해 Allocations(즉, 할당 개수)를 클릭하여 역순으로 조회합니다.
여기에 이미지 설명을 삽입하세요.

위 그림에서 볼 수 있듯이 할당이 가장 많은 범주는 int[], WeakReference, FinalizerReference, TaskRunable 및 Task입니다.
Shallow 크기를 보면 가장 높은 것이 int[]임을 알 수 있으며, 메모리 누수를 일으키는 객체는 int[]인 것이 거의 확실합니다. 위의 학습에 따라 해당 참조 체인을 살펴보겠습니다. 다음과 같이:
여기에 이미지 설명을 삽입하세요.

그림에서 알 수 있듯이 DeviceManager에 등록된 개체가 제 시간에 로그아웃되지 않아 누수가 발생합니다.

이 문제를 해결하려면 DeviceManager는 적절한 경우 더 이상 사용되지 않는 수신기를 제거해야 합니다. Task의 finalize 함수를 통해 클래스가 더 이상 사용되지 않으면 GC에서 리스너를 제거해야 함을 알 수 있습니다. 따라서 DeviceManager의 수신기는 약한 참조로 설계되었습니다. 코드가 너무 간단해서 더 이상 첨부되지 않습니다.

약한 참조로 변경하면 객체가 더 이상 누출되지 않습니다.

참고: 위의 코드는 여전히 엔지니어링 관행으로 간주될 수 없습니다. Task가 더 이상 사용되지 않고 GC가 아직 재활용되지 않은 경우 실제로 DeviceManager에 상태 변경이 있으면 Task에 통보하는데, 이때 Task에 해당 처리 로직이 있으면 문제가 발생할 수 있습니다. 그러므로 여기서는 주의가 필요합니다. 그러나 위의 예는 메모리 도구의 사용을 설명하기 위한 것입니다.

위의 두 가지 예는 너무 명확하고 명확합니다. 이는 단지 메모리 프로파일러의 사용을 설명하기 위한 것입니다. 실제로 실제 메모리 관계는 위의 것보다 훨씬 더 복잡할 수 있습니다. 다만, 글이 길어져서 더 이상 확장하지는 않고 나중에 기회가 된다면 추가하도록 하겠습니다.

이 기사를 끝내기 전에 대답해야 할 두 가지 작은 질문이 있는 것 같습니다. 첫째, Android의 이미지 힙, zygote 힙, 앱 힙 및 JNI 힙이 무엇입니까? 두 번째: 보유 크기를 계산하는 방법

어떻게 이해Image heap,zygote heap,app heap,JNI heap

이 네 가지 힙을 설명하기 위해 시작부터 시작하여 다음과 같이 간략하게 요약합니다.

  1. Android 시스템이 시작되면 zygote 프로세스라는 프로세스가 생성됩니다. zygote 프로세스가 처음 시작되면 일부 시스템 리소스를 포함하여 많은 리소스가 로드됩니다. 그런 다음 다시 실행하십시오.

  2. Android가 다른 프로세스를 시작하려고 하면 zygote처럼 처음부터 시작되지 않습니다. 대신 zygote 프로세스를 직접 분기하세요. 그런 다음 zygote 프로세스의 일부 리소스가 재사용되고 zygote의 특수 힙이 재사용됩니다. 이 힙은 첫 번째 단계에서 시스템 리소스가 로드되는 힙입니다. 이 힙의 이름을 zygote heap이라고 합니다. 애플리케이션이 이 힙의 콘텐츠를 수정해야 하는 경우 애플리케이션은 이때 새 힙을 생성한 다음 zygote 힙의 콘텐츠를 새 힙에 복사한 다음 수정합니다. 즉, 쓰기 중 복사입니다.

  3. Android 가상 머신이 시작되면 최적화된 바이트코드를 로드해야 합니다. 이렇게 최적화된 바이트코드는 나중에 직접 사용할 수 있도록 특수 힙에 매핑되는데, 이 힙을 이미지 힙이라고 합니다.

  4. Android 애플리케이션이 시작된 후 객체를 할당해야 하며, 객체는 앱 힙에서 할당됩니다. 이 힙은 애플리케이션의 기본 힙입니다.

  5. Android 애플리케이션이 사용 중에 JNI 참조를 사용하는 경우 이러한 JNI 참조는 별도의 힙에 배치됩니다. 이 힙이 JNI 힙입니다.

얕은 크기와 유지된 크기를 계산하는 방법

얕은 크기: 내부 참조 개체의 크기를 계산하지 않고 개체 자체의 크기입니다. 다음과 같이:

class A{
    
    
  int a;
  A aInner;
 }

그러면 객체 A의 얕은 크기는 4(a는 int이고 4바이트를 차지함) + 4(aInner는 참조이고 4바이트를 차지함) + 8(Object 객체의 필드) = 16바이트입니다.

참고: 경우에 따라 4바이트 정렬을 보장하기 위해 4바이트의 배수가 아닌 경우에도 4바이트의 배수가 됩니다. 왜 4바이트 정렬인가? 이는 메모리 액세스 효율성을 향상시키려는 메모리 버스의 목적 때문입니다. 여기에는 표시되지 않습니다. Baidu에서 직접 검색할 수 있습니다.

유지된 크기: 객체 자체의 크기에 내부 참조 객체의 크기를 더한 것입니다. 하지만 한 객체가 여러 객체에 의해 참조된다면 어떻게 될까요? 아래 그림과 같이
여기에 이미지 설명을 삽입하세요.

이 문제를 해결하려면 도미네이터 트리에 대해 알아야 합니다.

그 정의는 다음과 같습니다. 유향 그래프에서 소스 점에서 점 B로 이동하려면 어쨌든 점 A를 통과해야 합니다. 그러면 A가 B의 지배점이고 A가 B를 지배한다고 합니다. B에 가장 가까운 지배점을 직접 지배점이라고 합니다. 위에 표시된 대로. B는 D와 G의 직접 제어점입니다.

위의 관계에 따르면 다음과 같은 지배력 트리 그래프를 얻을 수 있다(오른쪽 그림).
여기에 이미지 설명을 삽입하세요.

위 사진에 대한 설명:

  1. D가 직접 제어: E, F
  2. B는 D, G, H를 직접 제어합니다.


E 유지된 크기 = E 얕은 크기 F
유지된 크기 = F 얕은 크기 H
유지된 크기 = H 얕은 크기 G 유지된 크기
= G 얕은 크기
D 유지된 크기= E 얕은 크기+ F 유지된 크기 + D 얕은 크기
B 유지된 크기 = D 유지된 크기 + H 유지된 크기 + G 유지된 크기 + B 얕은 크기

이것이 이 글의 끝입니다.

다음 기사에서는 여전히 Java의 힙 메모리에 대해 다루며, 힙 메모리를 분석하기 위해 perfetto와 mat라는 두 가지 다른 도구를 사용할 것입니다. 계속 지켜봐 주시기 바랍니다.

추천

출처blog.csdn.net/xiaowanbiao123/article/details/132153356