가비지 컬렉션을 통한 자동 메모리 관리는 ART 가상 머신을 기반으로 하는 Android 플랫폼의 핵심 메커니즘입니다. Google Android Documentation, 2026에 따르면, 가비지 컬렉터는 개발자를 수동 메모리 관리에서 해방시켜 더 이상 참조되지 않는 객체를 자동으로 제거합니다. GC가 없으면 모든 객체 할당마다 명시적으로 free 또는 delete를 호출해야 하며, 초당 수백만 개의 객체를 처리하는 Java 생태계에서는 물리적으로 불가능합니다.
핵심 요점
Garbage Collection (GC)은 프로그램에서 더 이상 사용하지 않는 객체가 차지하는 메모리를 감지하고 해제하는 자동 프로세스입니다. 모바일 개발의 맥락에서 GC는 ART 가상 머신을 통해 Android 플랫폼에서 사용되며, 표준 Java Virtual Machine에서도 사용됩니다.
수동 메모리 관리 언어(C, C++)와 달리 프로그래머가 명시적으로 free 또는 delete를 호출해야 하는 반면, GC는 객체 수명 주기 추적 작업을 완전히 담당합니다. 개발자는 new 연산자를 통해 새 객체를 생성하고, 컬렉터는 객체에 더 이상 활성 참조가 없을 때 — 즉 객체가 도달 불가능해질 때를 결정합니다.
GC 효율성의 주요 지표는 일시 중지 시간(pause time)과 처리량(throughput)입니다. 일시 중지는 컬렉션을 위해 애플리케이션 실행이 중단되는 기간입니다. 모바일 환경에서는 8–16밀리초를 초과하는 일시 중지가 드롭된 프레임(jank)으로 인지됩니다.
Google I/O 2019에 따르면, Android 10의 ART는 일반적인 GC 일시 중지를 2–4ms로 줄여 Android 4.4의 Dalvik보다 70% 감소했습니다. 그럼에도 불구하고, 부적절한 메모리 관리 — 루프에서의 빈번한 객체 할당, 불필요한 임시 인스턴스 생성 — 는 여전히 성능 문제의 주요 원인입니다.
Java 및 Android의 모든 GC 구현은 일시 중지 시간과 컬렉션 완전성 간의 균형을 달성하기 위해 결합된 여러 기본 알고리즘을 기반으로 합니다. 이러한 알고리즘을 이해하는 것은 GC 친화적인 코드를 작성하는 데 필수적입니다.
Mark-and-Sweep은 두 단계로 작동하는 가장 간단한 알고리즘입니다. Mark 단계에서 컬렉터는 루트 참조(루트 세트) — 지역 변수, 정적 필드, 스레드 스택 — 에서 시작하여 객체 그래프를 탐색합니다. 도달 가능한 각 객체는 라이브 플래그로 표시됩니다. Sweep 단계에서 컬렉터는 전체 힙을 스캔하고 표시되지 않은 객체의 메모리를 해제합니다.
단점은 메모리 단편화입니다. Sweep 후에 빈 영역과 점유된 영역이 번갈아 나타나 큰 객체 할당이 어려워집니다. 모바일 시나리오에서는 힙이 일반적으로 작기 때문에(Android에서 64–512MB) 이것이 중요합니다.
Copying Collection은 힙을 두 개의 반쪽 공간(semi-space)으로 나눕니다. 활성 객체는 간격 없이 한 반쪽 공간에서 다른 공간으로 압축되어 복사됩니다. 복사 후 이전 반쪽 공간은 완전히 비어 있는 것으로 선언됩니다. 이 알고리즘은 단편화를 완전히 제거하지만 두 배의 메모리가 필요합니다.
모바일 환경에서 Copying Collection은 통계적으로 일찍 소멸하는 젊은 객체(약한 세대 가설)의 빠른 정리를 위해 세대별 컬렉터에 의해 사용됩니다.
Generational Collection은 힙을 세대로 나눕니다: Young Generation(젊은 객체)과 Old Generation(여러 컬렉션에서 살아남은 오래된 객체). 젊은 세대의 컬렉션(Minor GC)은 대부분의 객체가 젊을 때 소멸하므로 자주 그리고 빠르게 수행됩니다. 오래된 세대의 컬렉션(Major GC 또는 Full GC)은 덜 자주 발생하지만 더 오래 걸립니다.
// 세대별 GC 데모: 젊은 객체는 빠르게 소멸
void processItems(List<Item> items) {
List<Result> results = new ArrayList<>(); // 전체 메서드 동안 생존
for (Item item : items) {
Result r = new Result(item.getValue()); // 즉시 소멸
if (r.isValid()) {
process(r); // r이 가비지가 됨
}
}
saveResults(results); // results가 Old Gen으로 이동
}
이 예제에서 Result 객체는 루프 내에서 생성되고 즉시 가비지가 됩니다 — 이들은 Young GC의 이상적인 후보입니다. results 객체는 더 오래 살아남아 Old Generation으로 이동합니다. 세대 분리를 통해 Minor GC는 이전 힙을 건드리지 않고 밀리초 단위로 젊은 객체를 정리할 수 있습니다.
Android는 Dalvik VM에서 ART(Android Runtime)로 발전했으며, GC 구현은 둘 사이의 주요 차이점 중 하나입니다. Android의 GC 아키텍처를 이해하면 실제 기기에서 일시 중지를 최소화하는 코드를 작성하는 데 도움이 됩니다.
| 특성 | Dalvik (4.4까지) | ART (5.0+) |
|---|---|---|
| GC 유형 | Concurrent Mark가 있는 Mark-and-Sweep | Generational + Concurrent |
| 일반적인 일시 중지 | 10–30 ms | 2–4 ms |
| 압축 | 아니요 (단편화만 증가) | 예 (백그라운드에서, 앱 중단 없음) |
| AOT 컴파일 | JIT (Just-In-Time) | AOT + JIT (하이브리드) |
Dalvik은 Mark-and-Sweep과 동시 단계의 조합을 사용했습니다. Concurrent Mark는 객체 그래프 탐색 중에도 애플리케이션이 계속 작동할 수 있게 했지만, Sweep 단계에서는 모든 스레드를 중지해야 했습니다(Stop-The-World). RAM이 적은 기기(512MB — 1GB)에서는 일시 중지가 30ms에 도달하여 인터페이스에서 눈에 띄는 지연을 유발했습니다. 또한 Dalvik은 힙을 압축하지 않았기 때문에 장시간 사용 후 단편화가 증가했고, 충분한 총 여유 메모리가 있음에도 큰 객체(예: Bitmap) 할당 시 OutOfMemoryError가 발생할 수 있었습니다.
ART(Android Runtime)은 동시 압축 기능을 갖춘 세대별 컬렉터를 도입했습니다. 힙은 Young, Mature(Old Generation과 유사), Large Object Space(12KB보다 큰 객체용)의 세 영역으로 나뉩니다. 대부분의 경우 Young Region 컬렉션은 스레드를 중지하지 않고 병렬로 수행됩니다. Android 10+에서는 Concurrent Copying이 도입되었습니다 — 압축은 Stop-The-World 없이 백그라운드 스레드에서 실행됩니다.
ART 아키텍처 덕분에 일반적인 GC 일시 중지가 2–4ms로 줄었고, 젊은 객체가 지배적인 시나리오에서는 0.5–1ms로 줄었습니다. 이를 통해 Android 기기는 활성 메모리 작업 중에도 안정적인 60FPS를 유지할 수 있었습니다.
Java 생태계에는 각각 고유한 성능 프로필을 가진 여러 GC 구현이 있습니다. Android 개발의 경우 선택은 ART로 제한되지만, Java GC에 대한 지식은 모바일 애플리케이션의 서버 측 코드를 작성할 때와 Kotlin Multiplatform으로 개발할 때 유용합니다.
Serial GC는 전체 애플리케이션 중지(Stop-The-World)가 있는 단일 스레드 컬렉터입니다. 각 Mark, Sweep 및 Compact 작업은 하나의 스레드로 수행됩니다. 성능이 낮아 — 모바일 서버에는 사용되지 않습니다. 최대 100MB 힙의 소규모 애플리케이션에만 적합합니다.
Parallel GC(처리량 컬렉터라고도 함)는 모든 컬렉션 단계에 여러 스레드를 사용합니다. 최대 처리량(throughput) — 애플리케이션 실행 시간 대비 GC에 소요되는 시간 최소화 — 을 지향합니다. JVM에서 -XX:+UseParallelGC 플래그를 통해 활성화됩니다.
G1(Garbage-First) GC는 Java 9+의 기본 컬렉터입니다. 힙은 1–32MB의 영역으로 나뉩니다. G1은 일시 중지 시간을 예측하고 지정된 제한(기본 200ms) 내에 유지하려고 노력합니다. 우선 순위: 가비지가 가장 많은 영역이 먼저 정리됩니다(이름의 유래). G1은 예측 가능한 일시 중지와 함께 대규모 힙 서버(4–64GB)에 효과적입니다.
// 목표 일시 중지 100ms로 G1 GC 활성화
// java -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -jar app.jar
public class MemoryMonitor {
private static final long THRESHOLD = 512 * 1024 * 1024; // 512 MB
public void checkHeapUsage() {
Runtime rt = Runtime.getRuntime();
long used = rt.totalMemory() - rt.freeMemory();
if (used > THRESHOLD) {
System.out.println("Heap usage exceeded threshold: " + used);
System.out.println("Consider reducing allocations");
}
}
}
Runtime을 통한 힙 모니터링으로 메모리 누수를 초기 단계에서 감지할 수 있습니다. 안정적인 작동에서 used가 최대 힙의 80%를 초과하면 — 이는 누수 가능성 또는 애플리케이션의 과도한 메모리 소비 신호입니다.
현대적인 ART GC조차 모든 문제를 해결하지는 못합니다 — 부적절한 메모리 사용은 jank 및 ANR(Application Not Responding)의 주요 원인으로 남아 있습니다. 주요 시나리오와 최적화 방법을 살펴보겠습니다.
GC 일시 중지 — 컬렉션 중 애플리케이션 스레드 중단입니다. 화면에서 이는 두 프레임 사이의 시간이 16.6ms(60FPS)를 초과할 때 드롭된 프레임으로 나타납니다. GC가 30ms 지속되면 두 개 대신 하나의 프레임만 그려져 — 사용자는 인터페이스에서 끊김을 경험합니다.
긴 일시 중지의 주요 원인: Old Generation의 많은 수의 살아있는 객체, 힙 단편화, 빈번한 Full GC입니다. 진단에는 Android Studio Profiler 및 systrace가 사용됩니다.
GC 친화적 코드의 주요 규칙은 할당된 객체 수를 최소화하는 것입니다. 각 새 객체는 메모리 할당뿐만 아니라 이후 컬렉션도 필요로 합니다. GC가 빠르더라도 초당 1000개의 추가 할당은 컬렉터에 1000개의 검사를 생성합니다.
메모리 누수는 객체가 더 이상 필요하지 않음에도 도달 가능한 상태로 남아 있을 때 발생합니다. GC는 이러한 객체를 삭제할 수 없으며 메모리가 점차 고갈됩니다. 일반적인 원인: 등록 취소되지 않은 리스너, Activity에 대한 정적 참조, 외부 컨텍스트를 캡처하는 익명 클래스, 닫히지 않은 Cursor/InputStream입니다.
// 메모리 누수: 익명 클래스가 Activity 참조 보유
public void startTask() {
new Thread(new Runnable() { // 암시적으로 this(Activity) 보유
@Override
public void run() {
// 긴 작업...
System.out.println("Done");
}
}).start();
}
// 수정: 정적 중첩 클래스 + WeakReference
private static class TaskRunnable implements Runnable {
private WeakReference<Activity> activityRef;
TaskRunnable(Activity activity) {
this.activityRef = new WeakReference<>(activity);
}
@Override
public void run() {
Activity act = activityRef.get();
if (act != null) {
// Activity로 안전한 작업
}
}
}
이 예제에서 익명 Runnable은 Activity에 대한 암시적 참조를 캡처합니다. 스레드가 살아있는 한 — 사용자가 이미 화면을 닫았더라도 Activity는 GC에 의해 수집될 수 없습니다. WeakReference + 정적 클래스를 사용한 수정은 이 체인을 끊고 Activity가 해제되도록 합니다.
자주 묻는 질문
Android(ART)의 GC는 제한된 메모리를 가진 모바일 기기에 최적화된 동시 압축 기능이 있는 세대별 컬렉터입니다. Java GC(G1, ZGC)는 큰 힙과 예측 가능한 일시 중지를 가진 서버 측 컬렉터입니다. ART GC는 JVM 플래그를 사용하지 않습니다 — 모든 튜닝은 OS 수준에서 자동으로 수행됩니다.
Stop-The-World는 컬렉터가 객체 그래프를 안전하게 탐색하거나 메모리를 해제하기 위해 모든 애플리케이션 스레드를 일시 중지하는 순간입니다. STW가 길수록 jank가 더 눈에 띕니다. ART는 세대별 아키텍처 덕분에 일반적인 STW 시간을 2–4ms로 줄였습니다.
Android Studio Memory Profiler를 사용하세요 — 힙 증가, 할당 수를 표시하고 Heap Dump를 수행할 수 있습니다. 심층 분석을 위해 LeakCanary를 사용하세요 — 라이브러리가 누수를 자동으로 감지하고 GC 수집을 방해하는 참조 체인을 보여줍니다.
Full GC는 Old Generation을 포함한 모든 힙 세대의 완전한 컬렉션입니다. 모바일 애플리케이션에서 Full GC는 50–200ms 지속되어 눈에 띄는 jank 또는 ANR을 유발할 수 있습니다. 주요 원인: 힙 단편화, 메모리 누수, Old Generation 임계값 초과입니다.
Kotlin은 구조적 동시성(structured concurrency)을 가진 코루틴을 제공합니다 — 범위 취소는 모든 자식 코루틴을 자동으로 취소하여 누수를 방지합니다. Kotlin에는 지연 초기화를 위한 lazy 위임자와 임시 객체 수를 줄이는 범위 함수도 있습니다.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.