자바의 메모리 구조와 코틀린의 Pair 클래스 성능 (JVM + Algorithm)
서론
알고리즘 스터디를 진행하면서 골드 5이상의 문제를 처음 풀어봤는데 메모리 초과가 굉장히 많이 발생했습니다. 보통 실버문제들은 시간 제한이나 메모리 제한이 굉장히 널널하기 때문에 지금까지 이런 예외들은 생각하지 못했는데, 이번 기회에 정말 많이 배운 것 같습니다.
가장 인상깊었던 것은 코틀린에서의 Pair 객체 사용이였는데, 이 객체는 실버문제를 풀 때는 아무런 문제가 발생하지 않았습니다. 하지만 골드 문제들을 풀 때 Pair 객체를 사용하니까 매번 실패하고 IntArray 라는 자료구조로 풀어야만 문제를 통과했습니다. Pair 자료구조가 왜 메모리를 많이 잡아먹는지와 IntArray의 장점에 대해서 살펴보고 JVM 레벨에서 어떻게 메모리를 관리하는지 알아보겠습니다.
Pair 자료구조의 메모리 사용률과 IntArray 비교
Pair data class의 코드는 아래와 같습니다.
public data class Pair<out A, out B>(
public val first: A,
public val second: B
)
Pair는 정말 간단하게 두 개의 제네릭 타입을 받고, 그 타입에 맞는 원소 값을 저장하는 간단한 래퍼 클래스입니다. 이 데이터 클래스는 코틀린으로 알고리즘 문제를 풀 떄 first를 X좌표, second를 Y좌표로 표시하면 굉장히 편하기 때문에 자주 사용했습니다. 특히 다익스트라 알고리즘을 사용할 때 많이 사용했는데, 이번에 성능 분석 툴인 visualVM 통해 메모리 사용량을 분석해보니 굉장히 많은 메모리를 사용한다는 것을 알았습니다.
분석한 자료를 보기에 앞서, 먼저 자바의 원시타입과 래퍼런스 타입의 저장 공간 차이에 대해서 알아야 하는데 원시 타입은 stack 영역에 저장되고 래퍼런스 타입은 heap 영역에 저장됩니다. 래퍼런스 타입은 heap 영역에 저장된 주소 값을 stack 영역의 변수 값을 저장하는 것이고 원시 타입은 실제 값이 들어있는데 여기서부터 메모리 사용량의 차이가 발생합니다.
배열은 heap 영역에 저장되게 되는데 위 개념을 가지고 다시한번 배열을 생각해보면 아래의 사진과 같습니다.

원시 타입의 배열은 heap 영역에 연속적인 메모리를 할당받고 “값”이 저장되지만 래퍼런스 타입은 heap 영역에 연속적인 메모리를 할당받지만 “값”이 아니라 다른 인스턴스가 heap 영역 어딘가에 저장된 “주소 값”을 저장하고 있습니다. 이 그림을 보고 생각할 수 있는 것은 레퍼런스 타입의 배열은 힙 영역에 인스턴스가 추가로 생성된다는 것과 배열 안의 “값”에 접근하기 위해서는 한번 더 참조를 해야 함으로 시간이 더 오래 걸린다는 정도입니다.
지금까지 알아본 개념으로 Array


위의 첫번째 사진에서 볼 수 있듯이 IntArray로 선언한 코드는 원시 타입의 배열이 생성되었고 총 461,546개가 생성되었고, 메모리 사용량은 11,114,864 Byte가 사용되었습니다. 두 번째 사진은 Pair<Int, Int> 배열로 선언한 코드의 사진인데, Pair 객체가 280,747개가 생성되었고 메모리는 6,737,928 Byte가 사용되었습니다. 또, 추가적으로 Integer 인스턴스가 총 561,501개가 생성되었고 메모리는 8,984,016이 사용되었습니다.
이 수치를 가지고 GPT에게 물어보니 같은 데이터 수를 저장한다고 가정했을 때 길이가 2인 IntArray는 약 24Byte, Pair + 2 * Integer 객체는 약 56Byte를 사용하게 되므로 약 2.3배 정도의 메모리 차이가 난다고 합니다. 이러한 이유 때문에 코틀린으로 푼 알고리즘 문제에서 계속해서 메모리 초과가 발생하였고 IntArray를 사용하면 무사히 통과할 수 있었습니다.
자바의 메모리 구조
모든 궁금증은 아래의 코드 한 줄에서 시작되었습니다. new 연사자를 통해서 인스턴스를 생성하는 것은 아는데, 구체적으로 어떤 과정으로 인스턴스를 만들고 메모리에 저장되는지 궁금했습니다.
Test test = new Test();
아래부터는 JVM 책에 적힌 내용을 기술할텐데 다 외울수는 없고, 이 정도의 영역이 있고 나중에 메모리 관련된 이슈나 동시성 이슈가 발생한다면 책을 피고 찾아보면 좋을 것 같습니다.
프로그램 카운터 레지스터(PC)
PC는 작은 메모리 영역인데, 스레드 독립적입니다. 현재 실행 중인 스레드가 몇 번 째 라인의 바이트코드를 읽고 있는지에 관한 정보를 담고 있다고 생각하면 쉽습니다.
Stack
이 영역또한 스레드 독립적입니다. 지역 변수나 매개변수, 함수 리턴 주도 등의 정보를 담고 있습니다. 또, 원시 타입과 객체 잠조 등의 정보도 저장하고 있습니다. 이는 위에 그림에서 확인했습니다.
네이티브 메서드 스택 (Native Method Stack)
네이티브 메서드를 위한 스택 공간입니다. 자바는 플랫폼에 종속되어 있지 않지만 그 밑단의 커널 자원을 사용하기 위해서는 어쩔 수 없이 네이티브 언어의 메서드를 사용해야 합니다.
Heap
객체의 인스턴스와 배열이 저장되어 있는 공간인데 모든 스레드가 공유하는 공간입니다. 따라서 멀티 스레드 프로그래밍을 할 때 상당히 주의를 해야 하는 영역이고 위의 메모리 초과같은 문제가 발생하지 않게 관리를 해 줘야 합니다.
메서드 영역
이 영역은 타입 정보, 상수, 정적인 변수 등을 저장하는데 사용됩니다. 이 영역또한 모든 스레드가 공유를 합니다. 이 안에 런타임 상수 풀이라는 공간도 있는데 클래스의 버전, 필드, 메서드, 인터페이스, 심벌 참조 등의 정보가 저장됩니다.
객체 생성 과정
메모리 구조에 대한 개념을 알았으니 실제로 JVM 레벨에서의 객체 생성 과정을 살펴보겠습니다. new 생성자에 해당하는 바이트 코드 명령을 만들면 클래스가 로딩 되었는지에 대한 정보를 메서드 영역 안에 있는 심벌 참조를 통해 알아냅니다. 클래스가 로딩, 초기화가 되었는지 확인을 하고 안됐으면 클래스 로더를 통해서 클래스를 로딩해야 합니다.
클래스를 로딩하고 나면 인스턴스의 메모리 사이즈를 정확하게 측정할 수 있는데 이 사이즈를 토대로 메모리 영역을 할당 받습니다. 메모리 영역을 탐색하는 알고리즘은 가비지 컬렉터에 따라 달라지므로 암기할 필요는 없을 것 같은데 핫 스팟 정도만 뒤에서 알아보도록 하겠습니다.
메모리를 할당받는 과정에서 조금 생각을 해야 하는 부분이 있는데 멀티 스레드 환경이라면 스레드가 동시에 메모리를 할당해달라고 요청할 수 있습니다. 이를 해결하기 위한 동기화 방식이 두 개가 있는데, 하나는 락을 걸어 동기화를 하는 것인데 이는 다른 스레드가 멈추게 되는 문제가 있습니다. 그래서 로컬 할당 버퍼(TLAB) 이라고 하는 영역에 메모리를 미리 할당 받아 두고, 각 스레드 마다 TLAB에 할당하는 방법입니다.
과거에는 자바의 new 연산자를 쓰면 JVM의 네이티브 메서드 중 malloc 같은 메서드를 통해서 메모리를 할당 받는 줄 알았는데, 미리 메모리를 받는다고 해서 조금 찾아봤습니다. 하지만 책에 내용이 없고 구글링을 해도 자료가 안 나와서 Chat GPT o1 pro 에게 물어봤는데 아래와 같은 답변을 해 줬습니다.
핫스팟을 기준으로 malloc 같은 메서드를 사용하는게 아니라 mmap이나 VirtualAlloc 등의 시스템 콜을 통해서 충분히 메모리를 가져오고 TLAB의 구조를 사용해서 각 스레드마다 메모리를 할당받고 0값으로 초기화 해준다고 합니다. 네이티브 코드는 아래의 링크에서 확인하실 수 있습니다.
https://github.com/openjdk/jdk/blob/master/src/hotspot/share/memory/allocation.cpp
지금까지가 메모리를 “할당”받는 것이었고 다음으로는 invokespecial 이라는 바이트코드를 통해 생성자를 실행하게 됩니다. 이를 통해서 또 알 수 있듯이 생성자를 통해서 필드 값을 초기화 안 해도 값이 들어 있는 이유가 메모리를 할당받고 0으로 초기화를 해 주기 때문입니다.
결론
코틀린의 Pair 인스턴스는 원시 타입이 아닌 래퍼런스 타입을 사용합니다. 따라서 Array<Pair<Int, Int»가 IntArray에 비해 2배 이상의 메모리를 차지하게 되므로 가급적 IntArray를 사용해 메모리를 줄이고 접근 속도를 높입시다. 또, new 연산자를 통한 객체 생성 과정을 JVM 레벨에서 살펴봤습니다.
수정사항 및 참고
JVM 밑바닥까지 파헤치기 운영체제 아주 쉬운 세 가지 이야기
2025.02.04 : 서론, Pair 자료구조의 메모리 사용률과 IntArray 작성 2025.02.05 : 자바의 메모리 구조 작성