TECH/웹개발

[Java] 자바 성능의 키, JVM 메모리 구조와 가비지 컬렉션(GC) 완벽 이해

런코리치 2026. 2. 23. 18:00

[Java] 자바 성능의 핵심, JVM 메모리 구조와 가비지 컬렉션(GC) 원리 총정리

자바는 C/C++와 달리 개발자가 직접 메모리를 해제(free, delete)하지 않습니다. 바로 **JVM(Java Virtual Machine)**이 그 역할을 대신해주기 때문이죠. 하지만 이 편리함 뒤에는 '메모리 관리의 원리'를 모르면 해결할 수 없는 성능 저하와 OutOfMemoryError라는 함정이 숨어 있습니다.

오늘은 JVM이 코드를 어떻게 실행하고, 메모리를 어떻게 관리하며, 불필요한 데이터를 어떻게 청소하는지 깊이 있게 파헤쳐 보겠습니다.


1. JVM의 전체 구조: 자바가 실행되는 과정

우리가 짠 .java 파일은 컴파일러에 의해 .class 파일(바이트코드)이 됩니다. 이를 JVM이 읽어 들여 운영체제 위에서 실행하는데, 그 내부는 크게 세 부분으로 나뉩니다.

  1. 클래스 로더(Class Loader): 실행 시점에 클래스 파일들을 로딩하고 링크하여 메모리에 배치합니다.
  2. 실행 엔진(Execution Engine): 메모리에 배치된 바이트코드를 명령어 단위로 실행합니다. (Interpreter 방식 + JIT 컴파일러 방식)
  3. 런타임 데이터 영역(Runtime Data Area): JVM이 프로그램을 수행하기 위해 OS로부터 할당받은 메모리 공간입니다.

2. 런타임 데이터 영역: 자바 메모리의 5대 구역

가장 중요한 곳은 데이터를 저장하는 '런타임 데이터 영역'입니다. 여기서 우리가 주목해야 할 곳은 StackHeap입니다.

  • PC Register / Native Method Stack: 스레드마다 생성되며, 현재 수행 중인 주소와 네이티브 코드(C/C++) 정보를 저장합니다.
  • Stack (스레드별 독립): 메서드 호출 시마다 프레임이 생성됩니다. 지역 변수, 매개변수 등이 저장되며 메서드가 종료되면 즉시 사라집니다.
  • Heap (모든 스레드 공유): new 연산자로 생성된 모든 객체가 저장되는 곳입니다. GC의 주요 관리 대상입니다.
  • Method Area (모든 스레드 공유): 클래스 정보, 상숫값(Runtime Constant Pool), static 변수 등이 저장됩니다.

3. 가비지 컬렉션(GC): 메모리 청소의 원리

JVM은 더 이상 참조되지 않는 객체(Garbage)를 찾아 메모리에서 제거합니다. 이를 위해 힙(Heap) 영역은 객체의 수명에 따라 크게 두 구역으로 나뉩니다.

① Young Generation (젊은 객체들의 공간)

대부분의 객체는 생성되자마자 금방 접근 불가능한 상태가 됩니다.

  • Eden: 새로 생성된 객체가 위치하는 곳.
  • Survivor 0, 1: Eden에서 살아남은 객체가 이동하는 곳.
  • 이 영역에서 발생하는 GC를 Minor GC라고 부르며, 속도가 매우 빠릅니다.

② Old Generation (장수 객체들의 공간)

Young 영역에서 끝까지 살아남은(기준 횟수 이상 참조된) 객체들이 이동합니다.

  • 객체 크기가 커서 Young 영역에 담기 힘든 경우 바로 여기로 오기도 합니다.
  • 이 영역에서 발생하는 GC를 **Major GC(Full GC)**라고 하며, 속도가 느리고 실행 시 Stop-the-world가 발생합니다.

⚠️ Stop-the-world란? GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 현상입니다. GC 스레드를 제외한 모든 스레드가 중단되며, 이 시간이 길어지면 서비스가 먹통이 된 것처럼 느껴집니다. GC 튜닝의 핵심은 이 시간을 최소화하는 것입니다.


4. 실무 예시: 메모리 누수(Memory Leak) 찾기

자바에서 메모리 관리를 자동으로 해준다고 해서 안심하면 안 됩니다. '참조'가 끊기지 않으면 GC는 데이터를 지우지 못합니다.

  • 나쁜 예시: 무한히 커지는 Static 컬렉션
  • Java
    public class MemoryLeak {
        private static final List<Object> list = new ArrayList<>();
    
        public void addToCache(Object obj) {
            list.add(obj); // static 리스트에 계속 담기만 하고 비우지 않음
            // 프로그램이 종료될 때까지 list가 객체들을 붙잡고 있어 GC가 회수 불가!
        }
    }
    
  • 해결책: 사용이 끝난 객체는 null 처리를 하거나, 컬렉션의 경우 명시적으로 clear()를 해주어야 합니다. 또는 유효 기간이 있는 WeakHashMap 등을 고려해야 합니다.

5. GC 알고리즘의 발전 (G1 GC, ZGC)

자바 버전이 올라가면서 GC의 성능도 비약적으로 발전했습니다.

  1. G1 GC (Java 9+ 기본): 힙을 바둑판 같은 영역(Region)으로 나누어, 효율적으로 쓰레기가 많은 영역부터 청소합니다. 대용량 메모리에서 빠릅니다.
  2. ZGC (Java 15+ 정식): Stop-the-world 시간이 10ms 이하를 목표로 설계된 최신 알고리즘입니다. 테라바이트급 메모리에서도 성능을 보장합니다.

🏁 마무리하며: "로그를 보고 튜닝하세요"

자바 개발자로서 성능 문제를 해결하고 싶다면, jstat, VisualVM, jconsole 같은 도구를 활용해 힙 메모리의 변화 그래프를 직접 관찰해 보세요.

  1. Minor GC가 너무 자주 일어나면? -> Young 영역의 크기를 키우기
  2. Major GC(Full GC)가 빈번해 성능이 떨어진다면? -> 객체 생성을 줄이거나 최신 GC 알고리즘(G1, ZGC) 도입 검토

메모리 구조를 이해하고 코딩하는 개발자와 그렇지 않은 개발자의 차이는, 서비스가 장애 상황에 처했을 때 비로소 드러납니다.