큰 규모의 안드로이드 프로젝트를 빌드하다보면 이러한 에러를 마주하게 될 때가 있습니다.
Caused by: java.lang.OutOfMemoryError:
Java heap space: failed reallocation of scalar replaced objects
e: java.lang.OutOfMemoryError: Java heap space
1: Task failed with an exception.
-----------
* What went wrong:
Execution failed for task ':app:compileFieldNoDexDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
> Not enough memory to run compilation. Try to increase it via 'gradle.properties':
kotlin.daemon.jvmargs=-Xmx<size>
gradle.properties
에서 자바 힙 영역을 늘려주세요
가장 유명하고 쉬운 해결책은 로그에서 나온 대로 gradle.properties
에서 힙 영역을 늘려주는 것입니다(Settings에서도 가능합니다).
// gradle.properites
org.gradle.jvmargs=-Xmx4096M (예시)
매번 이렇게 늘려주면 문제가 해결되었기 때문에 원인에 대해 진지하게 고민해본 적이 없는데요, 이번 기회를 계기로 정확하게 알고 넘어가고자 합니다. 왜 우리는 대규모 프로젝트를 빌드하다가 다음과 같은 에러를 마주하는 걸까요?
메모리 영역에 대해서 살펴보기 전, 잠시 Low level로 넘어가보겠습니다. 안드로이드 스튜디오는 Gradle이라는 빌드 시스템을 사용하여 프로젝트를 빌드합니다. 코틀린은 코틀린 컴파일러에 의해 바이트코드로 변환되며, 바이트코드는 JVM (Java Virtual Machine) 위에서 실행됩니다.
JVM에 대해서 짧게 설명하자면, 어느 OS든 JVM이 설치되어 있다면 다시 컴파일 할 필요 없이 자바 소스코드를 실행할 수 있습니다. 자바 코드는 javac라는 자바 컴파일러에 의해 바이트 코드로 컴파일되며, 이 바이트코드는 JVM이 OS에서 실행될 수 있도록 알맞게 변환해줍니다. 윈도우에서 컴파일된 실행파일을 리눅스에서 실행하려면 처음부터 다시 컴파일해야하는 C와 다른 점이죠. 그래서 C와 같은 언어를 플랫폼 종속적인 언어, 자바 또는 코틀린 같은 언어를 플랫폼 독립적인 언어라고도 합니다. (하지만 자바의 경우 바이트코드를 각 OS에서 실행될 수 있는 기계어로 변환하는 시간이 필요하기 때문에 수행 속도는 C보다 느립니다)
어쨌든 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받고, 그 메모리를 효율적으로 사용하기 위해 용도에 따라 여러 영역으로 나누어 관리합니다. JVM이 관리하는 메모리 영역은 크게 Static, Stack, Heap으로 나뉘며 데이터 타입에 따라 각 영역에 나눠서 할당되게 됩니다. 여기서는 Stack 영역과 Heap 영역만 보겠습니다.
메서드 내에서 정의하는 기본 자료형(Primitive Type)에 해당되는 지역변수의 데이터 값이 저장되는 메모리 영역입니다. 메서드가 호출될 때 스택 영역에 스택 프레임이 생기고 그 안에 메서드를 호출하며, 메서드가 호출될 때 메모리에 할당되고 종료되면 메모리에서 사라집니다.
JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역으로, 참조 자료형(Reference Type)에 해당되는 객체(인스턴스), 배열 등이 저장되는 메모리 영역입니다. 객체는 항상 힙 영역에 생성되며, 가비지 컬렉션(Garbage Collection, GC)이 더이상 참조되지 않는, 힙 영역에서의 사용되지 않는 불필요한 메모리를 주기적으로 해제해줍니다. c에서 free()를 사용하여 실제 메모리 영역을 해제해주는 역할을 가비지 컬렉션이 대신 해주고 있는 것입니다.
앞서 말했듯 자바나 코틀린으로 작성된 프로그램은 가비지 컬렉션이라는 뛰어난 도구가 주기적으로 불필요한 리소스를 청소해주기 때문에 개발자가 직접 메모리를 해제해줘야 하는 일이 거의 없습니다. 다만, 프로그램의 규모가 너무 커져서 가비지 컬렉션이 제 역할을 해줘도 남는 메모리가 없을 만큼 힙 영역이 다 차버리는 경우가 있습니다. 특히 안드로이드는 앱 내에서 사용할 수 있는 힙 메모리가 제한적이기에 더 그렇습니다.
따라서 위에서 나온 방식처럼 수동으로 힙 영역을 늘려주면 대부분의 경우 해결됩니다. 하지만 대규모 프로젝트라면, 그리고 지속적으로 OOM을 마주한다면 언제까지고 힙 영역을 수동으로 늘려줄 수는 없기 때문에 근본적인 원인을 찾아 해결하는 것이 가장 중요하지 않나 싶습니다.