이전 글
2026.02.19 - [안드로이드] - 안드로이드 컴파일 과정 완전정리 ⑥ 부록 - 앱 프로세스는 어떻게 시작되나?
안드로이드 컴파일 과정 완전정리 마지막 편 - ART에서 CPU로
핵심 결론
- 실행하는 주체는 항상 CPU다. CPU는 오직 기계어만 fetch/decode/execute 한다.
- “ART가 실행한다”는 말의 정확한 의미는 **CPU가 ‘ART라는 프로그램(기계어)’을 실행하고, 그 ART 프로그램이 DEX를 해석하거나(인터프리터) 기계어로 컴파일한다(JIT/AOT)**는 뜻이다.
- 인터프리터만으로는 느릴 수밖에 없다. DEX opcode마다 “읽기→분기(dispatch)→동작 수행” 오버헤드가 반복된다.
- 그래서 JIT가 “자주 도는 메서드”를 기계어로 컴파일해 이후 실행을 빠르게 만들고, 프로파일이 쌓이면 **AOT(dex2oat)**가 그 프로파일을 읽어 더 안정적인 성능을 내는 구조가 된다. (Android Open Source Project)
- 마지막 단계는 단순하다. JIT/AOT가 만든 ARM64 기계어를 CPU가 실행한다. 이후 성능은 L1/L2 캐시, TLB, 메모리 계층(캐시 미스 등)의 영향을 크게 받는다(이 부분은 컴퓨터 구조 일반 원리).
0) 출발점: “런타임이 실행한다”는 말의 정확한 의미
“ART가 실행한다”는 표현은 기술적으로는 축약된 말이다.
런타임은 하드웨어가 아니다. 런타임 역시 CPU 위에서 동작하는 소프트웨어다.
정확한 계층 구조는 이렇게 이해해야 한다.
[CPU] → (기계어만 실행)
↑
[ART 런타임] → (이미 기계어로 컴파일된 시스템 프로그램)
↑
[DEX 바이트코드] → (CPU는 직접 이해하지 못함)
↑
[앱 코드]
CPU는 오직 자신의 명령어 집합(예: ARM64 기계어)만 실행한다.
DEX 바이트코드는 CPU가 직접 해석할 수 있는 형식이 아니다.
따라서 중간에 ART가 존재한다.
한 문장으로 가장 정확하게 말하면 다음과 같다.
ART는 CPU 위에서 동작하는 소프트웨어이며,
DEX 바이트코드를 인터프리터 또는 JIT/AOT 컴파일을 통해
CPU가 실행 가능한 기계어로 연결해주는 실행 환경이다.
이 정의를 CPU 관점에서 다시 풀면 이렇게 된다.
- CPU는 ART라는 프로그램(기계어)을 실행한다.
- ART는 DEX를 해석(인터프리터)하거나 기계어로 변환(JIT/AOT)한다.
- 최종적으로 생성된 네이티브 기계어를 CPU가 실행한다.
즉 실행 주체는 항상 CPU다.
ART는 “실행하는 존재”가 아니라, **DEX를 CPU 실행 경로에 올려주는 중간 계층(runtime system)**이다.
“ART가 실행한다”는 말은 정확히는,
CPU가 ART를 실행하는 동안,
ART가 DEX를 처리함으로써 앱 코드가 동작한다
는 의미의 간략화된 표현일 뿐이다.
1) 인터프리터 실행의 실제 의미
1-1) CPU가 DEX를 직접 실행하지 않는다
DEX는 Android 바이트코드다. DEX 명령은 v0, v1 같은 “가상 레지스터”를 쓰는 형태로 정의된다. 예를 들어 문서에서 레지스터 범위(v0~v65535) 같은 규칙도 스펙으로 박혀 있다. (Android Open Source Project)
CPU는 이런 “DEX opcode/가상 레지스터”를 모른다. CPU가 아는 건 ARM64 같은 ISA의 기계어 opcode뿐이다.
1-2) 인터프리터는 “이미 기계어인 프로그램”이다
인터프리터는 마법이 아니다. 인터프리터는 그냥 이렇게 생긴 프로그램이다(개념 의사코드).
pc = dex_code_start
loop:
opcode = *pc++
switch(opcode):
case ADD_INT:
vA = vB + vC
goto loop
case INVOKE_VIRTUAL:
... (호출 규약/리졸브/디스패치)
goto loop
여기서 중요한 포인트:
- 위 switch를 돌리는 인터프리터 자체는 **ART의 네이티브 코드(기계어)**다.
- 그래서 “인터프리터 실행 중 스택 프레임”이 잡히면, 그 프레임은 ART 인터프리터 구현에서 온 것이라고 AOSP 디버깅 문서가 직접 말한다. (Android Open Source Project)
정리하면 이렇게 된다.
- CPU는 인터프리터(기계어)를 실행한다.
- 인터프리터는 DEX opcode를 하나씩 읽고
- opcode 의미에 따라 동작을 수행한다.
- 그러니까 CPU는 끝까지 기계어만 실행한다.
더보기 — opcode란
- “operation code”. 명령의 종류를 나타내는 숫자 값이다.
ARM64도 opcode가 있고, DEX도 opcode가 있다. 단, CPU는 ARM64 opcode만 이해한다.
2) 인터프리터만 쓰면 왜 느려지나: dispatch 비용
인터프리터 실행은 DEX opcode마다 다음을 반복한다.
- opcode fetch(DEX 코드에서 1개 읽기)
- dispatch(분기/테이블 점프)
- 해당 구현 실행
- 다음 opcode로 이동
이때 2)번이 핵심 오버헤드다. 매 opcode마다 분기가 들어간다.
바이트코드는 “작은 명령이 많이” 나오는 경향이 있어서 이 비용이 누적된다.
예시: sum(a, b)가 반복 호출될 때
sum() 자체는 덧셈 한 번이지만, 인터프리터 경로에서는 “덧셈 opcode 처리 루프”를 매번 거친다.
덧셈 자체보다 dispatch/루프/간접 분기가 상대적으로 커질 수 있다.
그래서 등장하는 게 JIT/AOT다.
3) JIT가 왜 필요한가: “중간 단계를 제거”해서 CPU가 바로 실행하게 만든다
AOSP 문서가 확정적으로 말하는 사실부터 잡는다.
- ART에는 코드 프로파일링을 포함한 JIT 컴파일러가 있다. (Android Open Source Project)
- JIT는 ART의 AOT 컴파일러를 보완하고, 런타임 성능을 개선하며, 저장공간/업데이트 측면에서도 이점이 있다고 설명한다. (Android Open Source Project)
3-1) “JIT가 한다”를 CPU 관점으로 번역
JIT의 본질은 이거다.
- DEX를 인터프리터로 매번 돌리지 말고
- **자주 실행되는 메서드(핫 메서드)**는 ARM64 같은 네이티브 기계어로 컴파일해 둔다
- 이후에는 CPU가 그 기계어를 직접 실행하게 한다
즉, 실행 경로가 이렇게 바뀐다.
- (처음) DEX → 인터프리터(기계어) → CPU 실행
- (JIT 후) DEX → (JIT 컴파일) → 네이티브 기계어 → CPU 실행
여기서 흔히 “엔트리포인트가 바뀐다”고 말한다. 구현 디테일을 단정하지 않고 CPU 관점으로만 말하면:
같은 메서드 호출이 이후에는 “인터프리터 경로”가 아니라 “컴파일된 코드 경로”로 들어갈 수 있다.
AOSP JIT 문서는 “oat가 있으면 그걸 직접 쓰고, 없거나 컴파일 코드가 없으면 JIT/인터프리터로 실행한다”처럼 “경로 선택”을 직접 설명한다. (Android Open Source Project)
3-2) OSR이 왜 나오나(중간 난이도, 하지만 중요한 부분)
AOSP JIT 문서에는 JIT가 **OSR(on-stack replacement)**를 가능하게 만든다고 적혀 있다. (Android Open Source Project)
이게 의미하는 바는 이런 거다.
- 루프가 한창 도는 중(이미 스택이 쌓인 상태)에도
- “이 루프 너무 핫하다”가 감지되면
- 중간에 JIT 컴파일을 만들어서
- 다음 반복부터 컴파일된 코드로 갈 수 있는 길을 열어준다(개념적으로)
더보기 — OSR(on-stack replacement)
- “스택 위에서 교체”. 실행 중인 프레임/상태를 유지한 채로 더 최적화된 코드로 갈아타는 아이디어다.
- “앱이 이미 돌아가는 중인데도 빨라진다”의 핵심 메커니즘 중 하나다. (Android Open Source Project)
4) 프로파일이 쌓이면 AOT가 왜 더 강해지나: “실행 데이터로 다음 컴파일을 더 정확히”
AOSP는 “Android 7부터 하이브리드(AOT+JIT+인터프리터) 조합”이고, AOT가 **프로파일 기반(profile-guided)**일 수 있다고 설명한다. (Android Open Source Project)
그리고 JIT 문서에는 더 직설적인 흐름이 있다.
- 앱 실행이 .dex 로드를 트리거하고
- JIT가 프로파일 데이터를 파일로 덤프하며
- AOT 컴파일(dex2oat) 데몬이 그 파일을 파싱해서 컴파일을 구동한다 (Android Open Source Project)
또 ART Service 설정 문서에도:
- 원칙적으로 profile-guided compilation(speed-profile)을 가능하면 모든 앱에 적용하고
- 보통 background dexopt 때 수행한다고 적혀 있다. (Android Open Source Project)
즉, “JIT로 프로파일 수집 → 이후 AOT가 그걸 읽어 더 잘 컴파일”이라는 구조는 문서로 뒷받침된다. (Android Open Source Project)
더보기 — background dexopt
- 기기가 idle/charging 같은 조건일 때(정책은 기기/버전에 의해 달라질 수 있음)
- dex2oat가 돌아가며 앱을 재컴파일/최적화할 수 있는 백그라운드 작업을 말한다. (Android Open Source Project)
5) 최종 CPU 레벨: 결국 ARM64 기계어가 돈다
여기부터는 더 이상 “DEX/ART” 이야기가 아니다. 최종 실행은 CPU 마이크로아키텍처 영역이다.
예를 들어 a+b 같은 연산이 네이티브 코드로 컴파일되면(예시):
add w0, w0, w1
ret
이제 CPU는 이 명령을:
- fetch: I-cache(명령 캐시)에서 가져오고
- decode: add, ret를 해석하고
- execute: ALU/분기 유닛 등으로 실행한다
여기서 “ART가 실행한다”는 말이 들어갈 자리는 없다.
CPU가 기계어를 실행한다. 끝이다.
6) 그런데 왜 같은 기계어인데도 느려지나: L1/L2 캐시, TLB, 메모리 계층
“JIT/AOT로 기계어가 됐으니 빠르겠네”는 절반만 맞다.
기계어가 빠르게 돌려면 데이터/코드가 메모리 계층에서 잘 먹혀야 한다.
- L1/L2 캐시 히트: 빠름
- 캐시 미스: 더 느린 계층(다음 캐시/메모리)로 내려가면서 지연이 커짐
- TLB 미스: 가상주소→물리주소 변환 캐시가 비어서 추가 비용 발생
즉, 실제 성능은 다음이 동시에 맞아야 나온다.
- “핫 코드가 네이티브로 컴파일되어 있고”(JIT/AOT)
- “그 코드와 데이터가 캐시/메모리 계층에서 유리하게 배치되어 있고”
7) 전체를 한 장으로 요약(DEX 편 + ART 편 + 이번 편 연결)
- DEX는 Android 바이트코드이고, 레지스터(예: v0~)를 쓰는 명령 체계를 가진다. (Android Open Source Project)
- ART는 Android 7부터 AOT+JIT+인터프리터의 하이브리드 실행 모드를 사용하며, AOT는 프로파일 기반일 수 있다. (Android Open Source Project)
- JIT는 프로파일링을 포함하고, .oat가 있으면 그걸 쓰고, 아니면 JIT/인터프리터로 .dex를 실행하며, 프로파일을 파일로 덤프하고 AOT 데몬이 그걸 파싱해 컴파일을 구동한다. (Android Open Source Project)
- 인터프리터 프레임이 스택에 잡힌다는 건, 인터프리터 자체가 “기계어로 된 ART 코드”라는 뜻이다. (Android Open Source Project)
- 결론: CPU는 끝까지 기계어만 실행한다. 런타임은 CPU 위에서 돌면서 바이트코드를 기계어 실행으로 연결해주는 소프트웨어 계층이다.
더보기
더보기 — 런타임(Runtime)
- “프로그램을 실행하기 위해 필요한 공용 기능을 제공하는 소프트웨어 계층”이다.
- ART는 DEX 로딩/실행, JIT/AOT 컴파일, GC 등 런타임 기능을 제공한다. (GC 등은 이번 편 범위 밖)
더보기 — 인터프리터(Interpreter)
- 바이트코드(opcode)를 한 개씩 읽고 의미에 따라 동작을 수행하는 실행 방식.
- CPU는 인터프리터(기계어)를 실행하고, 인터프리터가 DEX를 처리한다. (Android Open Source Project)
더보기 — dispatch 비용
- 인터프리터가 매 opcode마다 “분기해서 구현 코드를 선택”하는 오버헤드.
- opcode가 많을수록 누적된다.
더보기 — JIT / AOT
- JIT: 실행 중 컴파일. ART는 코드 프로파일링을 포함한 JIT를 가진다. (Android Open Source Project)
- AOT: 실행 전에 컴파일. Android 7부터 하이브리드 실행 모드에서 AOT는 프로파일 기반일 수 있다. (Android Open Source Project)
더보기 — speed-profile / background dexopt
- ART Service 문서: 원칙적으로 profile-guided compilation(speed-profile)을 가능하면 적용하며, 보통 background dexopt에서 수행한다고 설명한다. (Android Open Source Project)
출처(팩트 근거)
- ART 하이브리드(AOT+JIT+인터프리터), 프로파일 기반 AOT 가능, 예시 플로우, 산출물 설명(.vdex/.odex 등 “릴리즈에 따라 변동 가능” 포함): (Android Open Source Project)
- ART JIT 컴파일러(프로파일링 포함), oat 사용/미사용 시 경로, 프로파일 덤프 및 AOT 데몬 파싱, OSR 언급: (Android Open Source Project)
- ART Service: speed-profile의 원칙적 적용 및 background dexopt, 기본 dexopt reason 설정 예시: (Android Open Source Project)
- ART 인터프리터 프레임이 스택에 잡힌다는 디버깅 문서(“인터프리터가 실제 실행 코드”라는 증거로 사용): (Android Open Source Project)
- Dalvik/DEX 바이트코드 레지스터 규칙 예시(v0~v65535 등): (Android Open Source Project)
'안드로이드' 카테고리의 다른 글
| 안드로이드 컴파일 과정 완전정리 ⑥ 부록 - 앱 프로세스는 어떻게 시작되나? (0) | 2026.02.25 |
|---|---|
| 안드로이드 컴파일 과정 완전정리 ⑥ - ART란? (0) | 2026.02.24 |
| 안드로이드 컴파일 과정 완전정리 ⑤ 부록 - DEX 는 알겠고 APK 는요? (0) | 2026.02.23 |
| 안드로이드 컴파일 과정 완전정리 ⑤ - DEX 란? (1) | 2026.02.22 |
| 안드로이드 컴파일 과정 완전정리 ④ - D8/R8 란? (0) | 2026.02.21 |
댓글