티스토리 뷰

 

자바 애플리케이션을 개발하다 보면 한 번쯤 OutOfMemoryError나 StackOverflowError라는 거대한 벽에 부딪히곤 합니다. 현대의 하드웨어 성능이 비약적으로 발전했음에도 불구하고, JVM(Java Virtual Machine)의 메모리 구조를 이해하는 것이 왜 여전히 중요할까요?

그 이유는 자바의 자동 메모리 관리 시스템인 **Garbage Collector(GC)**가 만능이 아니기 때문입니다. 효율적인 리소스 관리와 고성능 애플리케이션 구축을 위해서는 데이터가 메모리의 어디에, 어떻게 저장되는지 아는 것이 필수적입니다. 오늘은 JVM 메모리 구조의 두 기둥인 StackHeap을 깊이 있게 파헤쳐 보겠습니다.


1. JVM 메모리 구조의 Deep Dive

JVM 메모리는 크게 여러 영역으로 나뉘지만, 개발자가 가장 밀접하게 제어하고 이해해야 할 부분은 StackHeap입니다.

Stack: 나만을 위한 프라이빗한 실행 공간

Stack 영역은 스레드(Thread)당 하나씩 생성되는 독립적인 공간입니다. 메서드가 호출될 때마다 '스택 프레임(Stack Frame)'이 쌓이고, 메서드 종료와 함께 메모리에서 즉시 사라집니다.

  • 비유: Stack은 요리사가 요리할 때 사용하는 **'개인 도구함'**과 같습니다. 지금 당장 필요한 칼, 도마를 꺼내 쓰고 요리가 끝나면 바로 정리하는 매우 빠르고 효율적인 공간이죠.

Heap: 모두가 공유하는 거대한 창고

Heap 영역은 모든 스레드가 공유하는 공간으로, JVM이 시작될 때 생성됩니다. 주로 new 키워드로 생성된 **객체(Object)**와 배열이 이곳에 저장됩니다.

  • 비유: Heap은 식당의 **'대형 냉장고'**입니다. 모든 요리사가 재료를 넣고 뺄 수 있으며, 누군가 재료를 다 썼더라도(참조가 끊겼더라도) 청소부(Garbage Collector)가 와서 치우기 전까지는 공간을 차지합니다.

2. Hands-on: 실전 비즈니스 로직으로 보는 메모리 변화

이커머스 시스템의 주문 처리 로직을 예로 들어 메모리가 어떻게 할당되고 해제되는지 살펴보겠습니다.

Java
 
public class OrderService {
    public void processOrder(int orderId) {
        // 1. 기본 타입 변수는 Stack에 직접 저장됨
        int priority = 10; 
        
        // 2. 객체는 Heap에 생성되고, 그 주소값(Reference)만 Stack에 저장됨
        Order currentOrder = new Order(orderId, "PENDING");
        
        updateStatus(currentOrder);
    } // 메서드 종료 시 Stack의 변수들은 즉시 제거됨

    private void updateStatus(Order order) {
        // order 변수는 Stack에 새로 생성되어 Heap의 동일한 객체를 가리킴
        order.setStatus("SHIPPED");
    }
}

코드 실행 단계별 메모리 시뮬레이션

  1. processOrder 호출: Stack에 processOrder를 위한 프레임이 생깁니다. orderId와 priority라는 값이 직접 저장됩니다.
  2. new Order(...) 실행: Heap 영역의 빈 공간에 Order 객체 데이터가 기록됩니다. Stack에 있는 currentOrder 변수는 이 Heap의 주소값(예: 0x100)을 담습니다.
  3. updateStatus 호출: 새로운 스택 프레임이 생성됩니다. 매개변수로 전달된 order는 currentOrder와 동일한 주소값(0x100)을 가집니다. (Call by Reference 같은 효과)
  4. 메서드 종료: updateStatus가 끝나면 해당 프레임은 소멸하지만, Heap에 있는 객체는 여전히 남습니다. 이후 processOrder까지 끝나면 Heap의 객체를 가리키는 변수가 사라지므로, 비로소 GC의 청소 대상이 됩니다.

3. 트러블슈팅: 흔히 발생하는 메모리 오류와 대책

StackOverflowError (SOE)

  • 원인: 재귀 호출이 너무 깊거나 무한 루프에 빠졌을 때 발생합니다.
  • 해결책: 재귀 함수의 탈출 조건(Base case)을 점검하거나, -Xss 옵션으로 스택 사이즈를 조정합니다.

OutOfMemoryError (OOME)

  • 원인: Heap 영역이 가득 찼을 때 발생합니다. 객체 참조를 끊지 않아 GC가 수거하지 못하는 'Memory Leak'이 주범입니다.
  • 해결책: * static 컬렉션에 무분별하게 객체를 쌓지 않는지 확인하세요.
    • Large Object(큰 이미지, 대용량 리스트) 사용 후 반드시 null 처리를 하거나 범위를 제한하세요.
    • -Xmx 옵션으로 최대 힙 크기를 늘릴 수 있습니다.

4. Trade-offs: Stack vs Heap

비교 항목 Stack Heap
관리 주체 OS/JVM (자동 할당/해제) Garbage Collector
속도 매우 빠름 (LIFO 구조) 상대적으로 느림 (탐색 필요)
공유 여부 스레드별 독립 운영 (안전) 모든 스레드 공유 (동기화 필요)
생명 주기 메서드 실행 동안만 유효 참조가 살아있는 한 유지

5. 결론 및 제언

JVM 메모리 구조를 이해하는 것은 단순히 시험 문제를 풀기 위함이 아닙니다. Stack은 프로그램의 실행 흐름을 제어하는 날카로운 칼날과 같고, Heap은 데이터를 풍부하게 담아내는 그릇과 같습니다.

효율적인 자바 개발자가 되기 위해서는 다음 질문을 스스로에게 던져보아야 합니다.

"지금 내가 만드는 이 객체가 얼마나 오랫동안 메모리에 머물러야 하는가? 그리고 이 객체의 참조를 적절한 시점에 끊어주고 있는가?"

혹시 여러분의 프로젝트에서 원인 모를 성능 저하가 발생하고 있다면, Heap 덤프를 떠서 불필요하게 살아남은 객체들이 공간을 독점하고 있지는 않은지 확인해 보시는 건 어떨까요?

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함