본문 바로가기
📙/Study

[JVM 파헤치기] 02. 자바 메모리 영역과 메모리 오버플로(1)

by kiwi_wiki 2025. 3. 23.
728x90
반응형

자바 개발자는 가상 머신이 제공하는 자동 메모리 관리 메커니즘 덕에 메모리 할당과 해제를 코딩하지 않아도 메모리 누수나 오버플로 문제를 거의 겪지 않음

하지만 통제권을 위임했기 때문에 문제가 한 번 터지면 가상 머신의 메모리 관리 방식을 이해하지 못하는 한 해결하기가 상당히 어려움

런타임 데이터 영역

자바 가상 머신은 자바 프로그램을 실행하는 동안 메모리를 몇 개의 데이터 영역으로 나누어 관리하며 영역별로 목적과 생성/삭제 시점이 있음

프로그램 카운터

PC 레지스터는 작은 메모리 영역이지만, JVM의 멀티스레딩 구현과 프로그램 실행 흐름 제어에 핵심적인 역할을 함

  • 작은 메모리 영역으로 현재 실행 중인 스레드의 바이트 코드 줄 번호를 가르키는 표시기
  • 각 스레드마다 별도의 스택 보유(스레드 프라이빗 메모리)
  • 스레드와 동일한 라이프사이클(스레드가 생성될 때 함께 생성되고, 종료되면 함께 소멸)

주요 기능

  1. 실행 흐름 제어
    • 바이트코드 인터프리터는 PC의 값을 변경하여 다음에 실행할 명령어를 결정
    • 프로그램의 제어 흐름, 분기, 순환 점프 등을 표현
  2. 스레드 컨텍스트 관리
    • 멀티스레딩 환경에서 각 스레드의 실행 상태를 추적
    • 스레드 전환 후 이전에 실행하던 지점을 정확히 복원하는데 필요
  3. 예외 처리 지원
    • 예외 발생 시 어떤 코드에서 예외가 발생했는지 추적
    • 예외 처리기로 제어를 전달하기 위한 정보 제공

특징

  • 스레드가 자바 메서드를 실행 중일 때, 실행 중인 바이트코드 명령어의 주소가 기록됨
  • 스레드가 네이티브 메서드를 실행 중일 때, PC 값은 Undefined 상태가 됨
  • 각 스레드의 PC는 서로 독립적으로 동작하며 다른 스레드의 PC에 영향을 주지 않음

스레드 프라이빗 메모리

  • 멀티스레딩 환경에서 CPU 코어가 여러 스레드를 번갈아 실행할 때 컨텍스트 스위칭을 정확히 수행하기 위한 핵심 요소
  • 스레드 간 메모리 격리를 통해 안전한 멀티스레딩 실행 보장

자바 가상 머신 스택

  • 각 스레드마다 별도의 스택 보유(스레드 프라이빗 메모리)
  • 스레드와 동일한 라이프사이클(스레드가 생성될 때 함께 생성되고, 종료되면 함께 소멸)
  • 자바 메서드 실행을 위한 메모리 모델

동작 원리

  • 메서드 호출 시, 새로운 스택 프레임을 생성하여 스텍에 push
  • 메서드 종료 시, 해당 스택 프레임을 스택에서 pop
  • LIFO(Last-In-First-Out) 구조로 동작

스택 프레임 구성 요소

  1. 지역 변수 테이블(Local Variable Table)
    • 컴파일 타임에 결정되는 기본 데이터 타입, 객체 참조, 반환 주소 타입 저장
    • 지역 변수 슬롯으로 구성
      • 일반적으로 슬롯 하나의 크기는 32비트
      • double, long 같은 64비트 데이터는 슬롯 두 개 차지
    • 필요한 슬롯 개수는 컴파일 시점에 결정되며 실행 중에는 변하지 않음
    • 실제 메모리 크기는 JVM 구현에 따라 달라질 수 있음
  2. 피연산자 스택(Operand Stack)
    • 메서드 내 연산을 위한 작업 공간
    • 바이트코드 명령어가 작업을 수행하기 위한 데이터를 저장하고 결과를 보관
  3. 동적 링크(Dynamic Linking)
    • 현재 메서드에서 사용하는 클래스와 메서드에 대한 참조 정보
  4. 메서드 반환값(Return Value)
    • 메서드 실행 완료 후 호출자에게 전달할 값 저장

발생 가능한 오류

  • StackOverflowError:
    스레드가 요청한 스택 깂이가 JVM이 허용하는 깊이를 초과할 때 발생.
    주로 과도한 재귀 호출이나 메서드 호출 중첩에서 발생
  • OutOfMemoryError:
    스택을 동적으로 확장할 수 있는 JVM에서 스택 확장 시 메모리가 부족할 때 발생

주의사항

  • 자바 메모리 구조를 단순히 '힙과 스택'으로 구분하는 것은 정확하지 않음
  • JVM 메모리 구조는 더 복잡하며, 스택은 그중 한 부분임

네이티브 메서드 스택

JVM이 자바 외의 언어로 작성된 코드와 상호작용할 수 있게 해주는 메모리 영역

  • 네이티브 메서드(JNI: Java Native Interface를 통해 호출되는 C/C++ 등의 코드)를 실행하기 위한 스택
  • 자바 가상 머신 스택과 유사한 역할을 수행하지만 다른 대상을 위해 사용됨
  • 스레드 프라이빗 메모리

구현의 자유도

  • JVM 명세는 네이티브 메서드 스택의 내부 구조에 대해 특별히 명시하지 않음
  • 구현자가 원하는 방식으로 자유롭게 구현 가능
  • 일부 JVM 구현체(핫스팟 포함)는 가상 머신 스택과 네이티브 메서드 스택을 하나로 통합

발생 가능한 오류

  • StackOverflowError: 허용 깊이를 초과할 때 발생
  • OutOfMemoryError: 스택을 동적으로 확장하려 할 때 메모리가 부족한 경우 발생

네이티브 메서드 사용 사례

  • 하드웨어 접근이 필요한 경우
  • 플랫폼 종속적인 기능을 사용해야 할 때
  • 이미 C/C++로 작성된 라이브러리를 재사용할 때
  • 성능이 중요한 특정 연산을 최적화할 때

자바 힙

  • 자바 애플리케이션이 사용할 수 있는 가장 큰 메모리 영역
  • 모드 스레드가 공유하는 공통 메모리 영역
  • JVM이 시작될 때 생성됨
  • 객체 인스턴스의 저장소 역할 수행. 가비지 컬렉션의 주요 대상

주요 특징

  1. 목적과 용도
    • 객체 인스턴스를 저장하는 것이 주요 목적
    • 거의 모든 객체 인스턴스가 이 영역에 할당됨 (자바 언어 발전에 따라 모든 객체가 할당된다고 볼 수 없음)
  2. 가비지 컬렉션(GC)
    • 가비지 컬렉터가 관리하는 메모리 영역으로 GC 힙 이라고도 불림
    • 전통적으로 세대별 컬렉션 이론(Generational Collection Theory)에 기반하여 설계됨
      • 신세대, 구세대, 에덴 공간, 생존자 공간.. 이런 개념
    • 최신 GC 알고리즘은 전통적인 세대 구분을 따르지 않을 수 있음
  3. 메모리 할당
    • 모든 스레드가 공유하지만, 할당 효율을 높이기 위해 스레드 로컬 할당 버퍼(TLAB, Thread-Local Allocation Buffer)로 나뉨
    • 어떻게 세부 영역이 나뉘든 모든 객체 인스턴스는 결국 자바 힙에 저장됨
    • 세부 영역 구분은 메모리 회수와 할당 속도 향상이 주목적
  4. 메모리 구조
    • 논리적으로는 연속된 메모리로 취급, 물리적으로는 떨어진 메모리에 위치해도 상관없음
    • 단, 대부분 JVM 구현체는 큰 객체(특히 배열)의 저장 효율과 구현 로직 단순화를 위해 물리적으로도 연속된 메모리 공간을 사용
  5. 크기 설정
    • 고정 크기 또는 가변 크기로 설정 가능
    • JVM 실행 옵션으로 초기 크기(`-Xms`)와 최대 크기(`-Xmx`) 지정 가능

발생 가능한 오류

  • OutOfMemoryError: 새 인스턴스를 할당할 힙 공간이 부족하고 더 이상 확장할 수 없을 때 발생

메서드 영역

  • 모든 스레드가 공유하는 메모리 영역
  • JVM이 시작될 때 생성, 종료될 때까지 유지
  • 클래스 로더가 로드한 클래스와 인터페이스의 메타데이터를 저장하는 물리적 메모리 영역

저장 정보

  • 클래스와 인터페이스의 타입 정보
  • 상수 풀(Constant Pool) 데이터
  • 정적 변수(Static Variables)
  • JIT 컴파일러가 최적화된 코드 캐시
  • 메서드와 생성자 코드 등

메서드 영역과 힙의 관계

  • 논리적으로 힙의 일부로 기술되지만 구분을 위해 '논힙(non-heap)'이라고도 불림
  • 메서드 영역은 자바 힙과 구조적으로나 목적상으로 다름

구현 변천사 (HotSpot VM 기준)

  • JDK 7 이전
    • 메서드 영역을 '영구 세대(PermGen)'에 구현
    • 가비지 컬렉터의 수집 범위를 메서드 영역까지 확장
    • 영구 세대에서 자바 힙처럼 메서드 영역 관리
    • 메모리 오버플로 가능성 높음
  • JDK 7
    • 문자열 상수와 정적 변수 등 일부 정보를 자바 힙으로 이동
    • 네이티브 메모리에 구현하는 계획 시작
  • JDK 8 이후
    • 영구 세대 개념 제거
    • 메서드 영역을 네이티브 메모리에 '메타스페이스(Metaspace)'로 구현
    • 시스템 메모리를 활용하여 자동으로 크기 조정 가능

구현  특성

  • 물리적으로 연속될 필요 없음
  • 고정 크기로 설정하거나 확장 가능하게 구현 가능
  • 가비지 컬렉션을 수행하지 않아도 됨(필수 아님)
    • 회수 대상은 주로 상수 풀과 타입 정보
    • 회수 효과가 상대적으로 작고 조건이 까다로움
    • 그러나 특정 상황에서는 회수가 필요할 수 있음

발생할 수 있는 오류

  • OutOfMemoryError: 메서드 영역이 가득 차서 필요한 메모리를 할당할 수 없을 때 발생
    JDK 8 이후에는 `-XX:MaxMetaspaceSize` 옵션으로 메타스페이스 최대 크기 제한 가능

런타임 상수 풀

  • 메서드 영역의 일부
  • 클래스나 인터페이스가 JVM에 로드될 때 생성됨
  • 클래스 파일의 상수 풀 테이블에 있는 정보를 런타임에 사용하기 위한 형태로 저장
  • JVM이 프로그램을 실행하는 데 필요한 다양한 상수 정보를 관리하는 중요한 메모리 영역으로, 자바의 동적 특성을 지원하는 핵심 요소

저장 정보

  • 클래스 메타데이터(버전, 필드, 메서드, 인터페이스 정보 등)
  • 컴파일 타임에 생성된 리터럴(숫자 상수, 문자열 리터럴 등)
  • 심벌 참조(Symbolic References)
    • 클래스와 인터페이스 이름
    • 필드 이름과 설명자
    • 메서드 이름과 설명자
  • 심벌 참조에서 번역된 직접 참조(Direct References)

특징

  1. 동적 특성
    • 클래스 파일의 상수 풀과 달리 런타임에도 동적으로 내용이 변경될 수 있음
    • 런타임 중에 새로운 상수가 추가될 수 있음
      • String 클래스의 intern() 메서드를 통한 문자열 상수 풀 추가
  2. 구현의 자유도
    • JVM 명세는 런타임 상수 풀에 대해 상세한 구현 요구사항을 정의하지 않음. 자유롭게 구현 가능
    • 일반적으로 심벌 참조와 직접 참조 모두 런타임 상수 풀에 저장됨
  3. 메모리 제약
    • 메서드 영역에 속하므로 메서드 영역의 공간 제약을 받음
    • OutOfMemoryError: 상수 풀 공간이 부족하면 발생 가능

역할과 중요성

  • 클래스 로딩과 링킹 과정에서 중요한 역할 수행
  • 컴파일 타임에는 알 수 없는 참조를 런타임에 해석하는 데 필요
  • 다양한 타입의 사수를 통합 관리하여 효율적인 메모리 사용 지원
  • 동적 바인딩(Dynamic Binding)을 가능하게 함

다이렉트 메모리

  • JVM 런타임 데이터 영역에 공식적으로 포함되지 않는 메모리 영역
  • 운영체제의 네이티브 메모리를 직접 사용하는 영역
  • JDK 1.4부터 도입된 NIO(New I/O) 라이브러리와 함께 사용되기 시작
  • 대용량 데이터 처리나 고성능 I/O 작업에 유용하지만, 명시적인 메모리 관리와 시스템 전체 메모리 사용량에 대한 이해가 필요한 영역

NIO와 다이렉트 메모리

  • NIO는 채널(Channel)과 버퍼(Buffer) 기반의 I/O 메서드 제공
  • `DirectByteBuffer` 객체를 통해 네이티브 메모리에 직접 접근
  • 자바 힙과 네이티브 힙 사이의 데이터 복사 과정을 생략하여 성능 향상
  • 물리 메모리를 직접 할당하기 때문에 I/O 작업 성능 개선

특성

  1. 메모리 제약
    • 자바 힙 크기의 제약과 무관하게 사용 가능
    • 하지만 시스템 물리 메모리 총량과 프로세서가 다룰 수 있는 주소 공간 제약을 받음(물리 메모리, 스왑 파티션, 페이징 파일)
  2. 메모리 관리
    • 자바 GC의 직접적인 관리 대상이 아님
    • `DirectByteBuffer` 객체는 힙에 할당되지만, 실제 버퍼 데이터는 네이티브 메모리에 저장
    • `DirectByteBuffer` 객체가 GC에 의해 수집될 때 네이티브 메모리도 함께 해제
  3. 성능 특성
    • 네이티브 I/O 작업에 최적화됨
    • 자바 힙과 네이티브 메모리 간 데이터 복사 비용 절감
    • 큰 데이터 처리 시 성능 이점이 있음

메모리 관리 주의사항

  • 서버 관리자들이 JVM 힙 메모리만 고려하고 다이렉트 메모리를 간과하는 경우가 많음
  • OutOfMemoryError: 다이렉트 메모리 사용량이 제한을 초과하면 발생 가능
  • `-XX:MaxDirectMemorySize` 옵션으로 최대 다이렉트 메모리 크기 설정 가능
  • 전체 시스템 메모리 사용량(JVM 힙 + 다이렉트 메모리 + 기타 네이티브 메모리)이 물리적 제약을 초과하지 않도록 주의해야 함
728x90
반응형