Zygote 프로세스와 안드로이드 가상머신(Dalvik, ART)에 대하여

SSY·2025년 1월 15일
0

OS

목록 보기
1/1
post-thumbnail

앱을 첫 로딩할 때

우리가 일반적으로 앱을 켰을 때, 1 ~ 2초 이내 빠르게 로딩되는데, 어찌보면 당연하게 느껴진다. 하지만 안드로이드 어플리케이션 실행을 위해 사전 준비되어야 하는 프로세스들과 작업이 존재하기에 이는 당연한게 아니다.

여기서 사전 프로세스들이라 하면 커널 자원, 가상 머신, 안드로이드 기본 API 등이 초기화된 프로세스를 의미한다. 하지만 이 프로세스들이 로딩된다 하더라도 안드로이드 어플리케이션이 바로 로딩되는게 아니다. 앞서 사전 프로세스들의 생성 작업을 거치며 낭비되었던 메모리 회수 작업 또한 진행되어야 한다. 그래서 안드로이드 어플리케이션의 전체 로딩 시간을 계산해 봤을 때, 이는 약 1분정도가 걸릴 수 있으며, 이는 무시할 수준이 아니다.

[어떻게 1분일까?]
실상, 필자도 계산해보진 않았다. 하지만 이를 간접적으로 알 수 있는 방법은 디바이스를 재시작해보는 것이다. 우리도 보통 알겠지만 디바이스가 켜진 직후는 시스템 프로세스를 생성하느라 디바스가 좀 버벅이면서 앱들이 잘 안눌리는걸 느낄 수 있고 좀 불편하기도 하다. 이런 일련의 불편함들이 앱을 실행할때마다 발생한다고 생각하면 아주 불편하다.

하지만 우리가 앱을 켰을 때는 바로 실행이 되는데 어떻게 그럴 수 있을까? 이것을 가능하게 하는 핵심 열쇠는 Zygote 프로세스이다. 이는 기기 첫 부팅 시, 안드로이드 시스템이 어플리케이션의 빠른 부팅을 위해 템플릿을 사전에 만들어 놓는다고 생각하면 된다. 즉, 사용자는 앱을 실행시키고자 할 때, Zygote 프로세스 템플릿을 복사하며 메모리 공유를 통해 앱을 빠르게 실행시킨다.

Zygote 프로세스

1. 프로세스 구조

앱의 빠른 실행을 위해 디바이스 첫 부팅 시, OS는 Zygote 프로세스를 템플릿화해 놓는다. 이때, Zygote프로세스의 구조는 위 사진에서 보여준 사전 필요 프로세스와 동일하며 이는 아래와 같다.

우선, 최 하단에는 커널과 통신함으로써 하드웨어 자원 제어를 위한 c모듈로 생성된 프로세스이다. 그 상위엔 커널 프로세스를 가상의 컴퓨팅 환경으로 제어하기 위한 ART 가상머신이 존재한다. 그 위엔 안드로이드 어플리케이션 기본 실행에 필요한 기본 API를 의미한다. 디바이스 첫 시작 시, AndroidOS는 위 Zygote템플릿 생성을 시작한다.

2. 메모리 동작 방식

사용자가 새 어플리케이션을 시작시켰을 때, Zygote 프로세스는 복사되기 시작한다. 하지만 이때 유의해야할 점이 Zygote프로세스가 참조하고 있는 내부 프로세스들까지 복사되는건 아니라는 점이다. 프로세스 복사에는 오버헤드가 발생하며, 이는 실행되는 어플리케이션의 수에 비례하여 메모리 점유가 곱질이 된단 뜻이다. 따라서 사용자가 새로운 어플리케이션을 시작했을 때, 원래 준비되어 있던 Zygote 프로세스 복사는 진행 되지만 Zygote 프로세스가 보유한 프로세스들(ex. 커널 자원과 통신 가능 모듈)까지 복사되는건 아니라는 뜻이다.

Zygote 프로세스는 메모리 관리 시, COW(Copy On Write)기법을 사용하게 된다. 앱을 처음 시작했을 때엔 아무런 쓰기 작업이 없다. 따라서 메모리 복사를 굳이 불필요하게 진행할 필요가 없으므로 기존 부모 Zygote 프로세스가 참조한 메모리 페이지를 그대로 공유함으로써 메모리 리소스를 절약한다.

그 후, 해당 메모리에 쓰기 작업이 발생했을 때는 어떻게 할까? 위 사진의 '메모리 페이지1'의 경우는 3개의 Zygote프로세스가 참조중이다. 따라서 이 부분에 데이터 수정 작업이 발생한다면 기존에 참조하고 있던 프로세스들에게도 문제가 발생한다. 따라서 이때서야 메모리 복사 작업이 일어나며 이는 'Copy On Write'라는 말의미에도 잘 들어맞는다. 즉, 공유 메모리 페이지에 쓰기 작업이 발생했을 때, 새로운 메모리 페이지로 복사해서 그 곳에 쓰기 작업을 진행한다는 의미이다.

모바일 런타임 가상머신

기존 자바를 사용한 데스크톱 어플리케이션엔 JVM이 쓰인다면, 안드로이드 어플리케이션엔 ART가 쓰인다. 데스크톱과 모바일 어플리케이션의 1가지 중요 차이점이 존재하는데, 이는 '배터리 효율'이다. 데스크톱 어플리케이션은 최대한의 CPU파워와 메모리를 100% 활용하거나 자주 쓰일 확률이 있는 연산을 사전 계산하는 등 컴퓨팅 파워를 많이 사용하곤 한다. 하지만 모바일은 그렇지 않다. 데스크톱 어플리케이션처럼 하드웨어 리소스 자원을 사용하면 배터리가 빨리 소모된다는 문제가 있다. 둘의 컴퓨팅 환경이 다른 만큼, 가상 컴퓨팅 환경 또한 다를 것이고, 그에 따른 JVM과 ART의 설계 방식은 매우 다르다.

안드로이드 어플리케이션에 대응하는 가상머신으로 DalvikART가 있다. 이 두 가상머신의 중요한 차이점은 중간언어 바이너리의 컴파일 시기이다.

[중간언어 바이너리란?]
안드로이드 어플리케이션이 기계어로 되기까지 대략적 3번의 컴파일 과정이 들어간다.
1. javac or kotlinc의 컴파일 : .java, .kt등 고수준 언어를 컴파일하여 .class파일을 생성.
2. R8의 컴파일 : .class를 컴파일하여 .dex(Dalvik Executable)을 생성.
3. JIT or AOT의 컴파일 : 가상머신(eg. ART, Dalvik, JVM)이 .dex 파일을 컴파일하하여 기계어로 변환
즉, 위 과정에서 첫 번째 컴파일을 완료해 생성된 .dex, .class파일을 의미한다.

어플리케이션 컴파일 방식

[Dalvik]
Dalivk은 JIT(Just In Time)컴파일 방식을 사용하며 보통 2가지의 경우에 바이너리를 컴파일한다.

  1. 앱의 첫 실행 시 컴파일
  2. 앱의 런타임 시, 필요 기능에 대한 점진적 컴파일

Dalvik가상머신 기준, 앱은 실행 전, 바이너리(.dex)로 디바이스에 존재한다. 그 후, 사용자가 앱을 실행시킬 때, .dex가 Dalvik에 의해 컴파일되어 기계어로 변환된다. 또한 사용자가 어플리케이션을 사용하며 추가 기능을 동작시킬 때 또한 실행에 필요한 .dex 부분을 기계어로 컴파일한다. 이렇게 Dalvik가상머신은 앱의 시작과 런타임 추가 동작을 위해 실시간 JIT 컴파일을 진행한다.

[ART]
ART는 AOT(Ahead Of Time)컴파일 방식을 사용하며, 사용자가 앱을 설치할 때, 해당 앱의 .dex를 기계어로 미리 전환하는 방식을 사용한다. 이는 장단점이 존재한다. 우선 장점의 경우, 기 설치된 앱의 실행과 동작에 추가적인 컴파일이 거의 필요치 않아 속도가 빠르다는 점이다. 단점이라 하면 앱을 설치할 때, .dex의 기계어 변환 과정때문에 설치 속도가 느릴 수 있다는 점이다. (다만, Android 7.0이후엔 JIT + AOT 혼합 방식을 사용한다)

가비지 콜렉터를 통한 메모리 관리

틀린 부분이 있을 수 있으니 짚어주시면 감사하겠습니다.

가상머신의 메모리 관리는 우리가 익히 알고 있는 Garbage Collector를 의미한다. 이 또한 Dalvik이냐, ART냐에 따라 방식이 발전해왔으며, 사용자가 안드로이드 어플리케이션 사용을 매끄럽게 할 수 있도록 진화해왔다.

[Dalvik]
가비지 콜렉터를 사용한 메모리 관리는 Mark-And-Sweep방식을 사용한다. 단어 풀이로도 직관적으로 알 수 있는게, 사용하는 객체와 그렇지 않은 객체를 Mark 즉, 표시한다. 표시되지 않은 객체는 사용되지 않은 객체로 메모리 상에서 지워버리는 방식이다. 사실 여기까지만 알고있다는 것은 Mark-And-Sweep이라는 단어를 붙여 겉멋들어보이게 설명한것에 지나지 않는다. 따라서 조금이라도 더 알 필요가 있어보인다.

Dalvik의 가비지 콜렉터는 초기 안드로이드 디바이스를 타겟으로 만들어진 만큼, 최적화가 필요한 부분이 많다. 우선 메모리를 정리할 때 앱의 모든 스레드를 붙잡음으로써 앱이 버벅거릴 수 있다.

ART의 경우, Young Generation/Old Generation으로 메모리 구역이 나뉘어져 있다. Young Generation은 최근에 사용된 객체이거나 금방 사라질 객체가 배치되는 곳으로 가비지 콜렉터의 주 청소 타겟이 된다. 반면, Old Generation은 오랫동안 사용되었고 앞으로도 그럴 객체들이 자리하는 곳으로 가비지 콜렉터가 상대적으로 덜 방문하게 된다.

위 처럼 ART는 객체의 수명주기에 따른 heap영역이 나뉘어져 있고 메모리 정리를 덜 해도되는 구역이 따로 존재한다. 하지만 Dalvik가상머신의 메모리는 나뉘어진 구역이 따로 존재하지 않는다. 따라서 Dalvik가상머신은 모든 메모리를 탐색하는 비효율이 존재하며 또한 가비지 콜렉팅 이후 메모리 정렬을 진행하지 않음으로써 메모리 파편화가 이뤄진다는 단점이 존재한다. 메모리에 파편화되어 존재하는 데이터는 추후 메모리에 데이터 접근시간이 늘어나 효율이 떨어진다.

[ART]
실행되는 객체는 수명주기에 따라 Young Generation/Old Generation에 배치된다. 가비지 콜렉터는 그 중 Young Generation을 높은 우선순위로 타게팅함으로써 앱의 불필요한 성능 저하를 막는다.

메모리 정리는 Dalvik처럼 Mark-And-Sweep방식을 따른다. 하지만 메모리 정리 완료 후 메모리를 추가 정리하고 정렬함으로써 메모리 파편화 또한 방지하는 장점도 존재한다.

ART의 메모리 정리는 동시성을 지원한다. 따라서 메모리 정리할 때 UI 스레드가 버벅거린다는 이슈 또한 Dalvik보다 덜 하다는 장점이 존재한다. 하지만 안드로이드 공식 홈페이지를 보면 ART가상머신 또한 가비티 콜렉팅 시, 버벅거림이 일어날 수 있다고 하며, 공식문서에 나와있는 주의사항을 숙지하는걸 권장한다.


참고 : https://developer.android.com/topic/performance/memory-overview?hl=ko#gc

따라서 ART가 Dalvik보다 메모리 관리 상의 장점은 아래정도로 요약 가능하다.

  • 메모리를 관리할 때, 메모리 정리가 병렬적으로 이뤄져 메인스레드를 붙잡지 않음
  • 가상머신의 heap메모리가 분리로 메모리 정리가 더 빨리 이뤄질 수 있음
  • 메모리 정리 후, 메모리에 있는 객체를 재정렬하여 파편화를 막는다.
profile
불가능보다 가능함에 몰입할 수 있는 개발자가 되기 위해 노력합니다.

0개의 댓글