[Android] ART의 Zygote를 활용한 콜드 스타트 최적화 핥아보기

신민준·2025년 11월 10일
0

안드로이드 빌드

목록 보기
4/4

들어가며

Manifest Android Interview에서 ART를 더 알아보기 위한 글이다.
ART의 코드는 너무나도 방대하여 그중에서 Zygote에 대한 내용만을 아주 살짝 핥아만 볼 것이다.

이 글은 말 그대로 핥아보는 글이라 내부 원리를 너무 깊이 다루진 않는다.

Zygote

일단 Zygote가 무엇인가 안드로이드 공식문서에는 다음과 같이 설명한다.

Zygote는 동일한 애플리케이션 바이너리 인터페이스 (ABI)를 사용하는 모든 시스템 및 앱 프로세스의 루트 역할을 하는 Android 운영체제의 프로세스입니다.

좀더 설명하자면
Zygote는 모든 안드로이드 앱 프로세스의 부모 역할을 수행한다.
기기가 부팅되면 Zygote는 가상 머신을 초기화하고 필요한 시스템 및 앱 클래스를 메모리에 미리 로딩하여, 애플리케이션이 시작될 때마다 이 과정을 반복하지 않고 재사용함으로써 앱 실행 속도를 단축시킨다.

앱은 이 Zygote를 fork()해서 새로운 프로세스를 만들고 이 fork된 자식 프로세스 안에서 앱의 코드(Dex 파일), Application 클래스, Activity 등이 로드된다.

PreZygoteFork()

이것은 fork 직전에 호출되는 함수이다.

void Runtime::PreZygoteFork() {
  if (GetJit() != nullptr) {
    GetJit()->PreZygoteFork();
  }
  if (!heap_->HasZygoteSpace()) {
    Thread* self = Thread::Current();
    // This is the first fork. Update ArtMethods in the boot classpath now to
    // avoid having forked apps dirty the memory.

    // Ensure we call FixupStaticTrampolines on all methods that are
    // initialized.
    class_linker_->MakeInitializedClassesVisiblyInitialized(self, /*wait=*/ true);

    ScopedObjectAccess soa(self);
    UpdateMethodsPreFirstForkVisitor visitor(class_linker_);
    class_linker_->VisitClasses(&visitor);
  }
  heap_->PreZygoteFork();
  PreZygoteForkNativeBridge();
}

복제를 준비하는 코드다.
if (GetJit() != nullptr) { GetJit()->PreZygoteFork(); }
fork전 JIT(Just-In-Time) 컴파일러가 있다면
Zygote fork 전 상태를 정리하고 JIT 캐시를 freeze 상태로 만든다.

이것을 왜 하냐면 Zygote가 fork될 때,
JIT 캐시나 컴파일된 코드 영역이 그대로 복제되면 부모(zygote)와 자식(앱) 간에 메모리 충돌이나 불필요한 Copy-On-Write가 생길 수 있다고 한다.

!heap_->HasZygoteSpace()는 런타임의 힙에 Zygote 전용 공유 메모리 영역(zygote space)이 이미 생성되어 있는지를 반환하는데 false면 초 Zygote 생성(또는 최초 fork 전) 이라는 것을 의미한다.

검사 이유는 최초 한 번만 수행해야 할 초기화를 반복하지 않기 위해서이다.
이후 코드는 다음과 같다.

  • Thread* self = Thread::Current();
    → 현재 실행 중인 스레드 핸들을 가져온다
  • class_linker_->MakeInitializedClassesVisiblyInitialized(self, /*wait=*/ true);
    → 이미 초기화된 클래스들을 완전 초기화된 상태로 다른 스레드에게 보이게(동기화) 만든다.(다른 스레드 중 초기화 진행중이면 대기)
  • ScopedObjectAccess soa(self);
    → 네이티브 코드에서 VM 객체에 안전하게 접근하도록 스코프를 설정한다.
  • UpdateMethodsPreFirstForkVisitor visitor(class_linker_);
    → 각 클래스의 메서드를 미리 업데이트할 방문자(visitor)를 만든다.
  • class_linker_->VisitClasses(&visitor);
    → 모든 클래스들을 순회하며 visitor로 메서드/트램폴린 등을 확정(초기화)한다.

최초 Zygote 초기화 블록 종료

heap_->PreZygoteFork(); 는 Runtime의 PreZygoteFork()가 아닌 힙의 fork를 수행하는 것으로 GC heap 내부의 zygote 관련 영역을 준비한다.

PreZygoteForkNativeBridge()는 Zygote가 fork되기 전에
CPU 변환 계층(Native Bridge)의 상태를 고정시키는 함수이다.

Native Bridge

이게 뭔가 하니 GPT는 다음과 같이 설명한다.

안드로이드에서는앱이 꼭 ARM용 기기에서 ARM 코드만 쓸 필요는 없어.
예를 들어,
기기는 x86 CPU 인데
앱이 ARM용 네이티브 코드(.so 파일) 만 제공하는 경우가 있어.
이럴 때 “Native Bridge” 라는 녀석이
👉 ARM용 코드를 x86에서 돌릴 수 있게 중간에서 변환 해줘.
즉,
“CPU가 못 읽는 네이티브 코드를 대신 번역해서 실행시켜주는 번역기”

PostZygoteFork

이것은 fork 직후 실행되는 함수이다.

void Runtime::PostZygoteFork() {
  jit::Jit* jit = GetJit();
  if (jit != nullptr) {
    jit->PostZygoteFork();
    // Ensure that the threads in the JIT pool have been created with the right
    // priority.
    if (kIsDebugBuild && jit->GetThreadPool() != nullptr) {
      jit->GetThreadPool()->CheckPthreadPriority(
          IsZygote() ? jit->GetZygoteThreadPoolPthreadPriority()
                     : jit->GetThreadPoolPthreadPriority());
    }
  }
  // Reset all stats.
  ResetStats(0xFFFFFFFF);
}
jit::Jit* jit = GetJit();
if (jit != nullptr) {
  jit->PostZygoteFork();
  ...
}

fork 전 중단했던 JIT을 다시 활성화하고 새로 생성된 앱 프로세스에서 JIT thread pool을 다시 세팅한다.
Zygote 시절의 스레드들은 fork로 복제되면 동작 보장이 안 되기 때문에,
새로 앱 프로세스 전용의 JIT thread pool 을 만드는 것이다.

if (kIsDebugBuild && jit->GetThreadPool() != nullptr) {
      jit->GetThreadPool()->CheckPthreadPriority(
          IsZygote() ? jit->GetZygoteThreadPoolPthreadPriority()
                     : jit->GetThreadPoolPthreadPriority());
    }

이건 디버그용 코드로 방금 생성된 스레드풀의 스레드들의 우선순위를 점검하는 코드이다.

ResetStats(0xFFFFFFFF); 를 통해 최종적으로 ART 내부의 런타임 통계(메서드 호출 수, GC 횟수 등)를 전부 초기화한다.
초기화하는 이유는 Zygote fork시 그 통계 데이터도 그대로 복사되어 자식 앱 프로세스에 남기 때문이라고 한다.

결론

ART 너무 어렵다!
이건 또 언제 공부해야 하나 싶다..!!!
물론 코드를 뒤집어 깔 필요는 없겠지만 그래도 동작 원리는 알면 좋을 것 같은데...
심지어 이 글에서 다룬 내용도 이해 못했다...ㅎ

내부 코드가 정확히 어떻게 동작하는지까지 알아보는 건 너무도 시간이 오래 걸리고 어려워 추후 ART를 정말 싹싹 긁어먹을 때 해봐야지....

용어

  • 트램폴린 : ART 런타임에서 JNI(Java Native Interface) 메서드와 같은 네이티브 코드를 호출하기 위한 내부적인 기술

참고

https://source.android.com/docs/core/runtime/zygote?hl=ko
https://cs.android.com/android/platform/superproject/+/master:art/runtime/runtime.cc

profile
안드로이드 외길

0개의 댓글