ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JVM] 자동 메모리 관리 - 자바 메모리 영역과 메모리 오버플로우
    DevBook/JVM 밑바닥까지 파헤치기 2025. 3. 15. 20:10

    1. 런타임 데이터 영역 -> 자바 프로그램 실행 동안 필요한 메모리 영역

    • JVM은 자바 프로그램을 실행하는 동안 필요한 메모리를 몇 개의 데이터 영역으로 나눠 관리
    • 이 영역들은 각각 목적과 생성/삭제 시점이 있음

     

    1.1 프로그램 카운터

    • 바이트코드 인터프리터는 카운터의 값을 바꿔 다음에 실행할 바이트코드 명령어를 선택하는 식으로 동작
      • 프로그램의 제어 흐름, 분기, 순환, 점프 등을 표현하는 것
    • JVM에서의 멀티스레딩은 CPU 코어를 여러 스레드가 교대로 사용하는 방식으로 구현되므로, 각 코어는 한 스레드의 명령어만 실행하게 됨
      • 스레드 전환 후 이전에 실행하다 멈춘 지점을 정확하게 복원하려면 스레드 각각에는 고유한 프로그램 카운터가 필요
      • 각 스레드의 카운터는 서로 영향을 주지 않는 독립된 영역에 저장
      • 이 메모리 영역을 '스레드 프라이빗 메모리'라고 함
    • 스레드가 자바 메서드를 실행 중일 때, 실행 중인 바이트코드 명령어의 주소가 프로그램 카운터에 기록됨
    • 스레드가 네이티브 메서드를 실행 중일 때, 프로그램 카운터 값은 Undefined임
    • 프로그램 카운터 메모리 영역은 OutOfMemoryError 조건이 명시되지 않은 유일한 영역이기도 함

     

    1.2 자바 가상 머신 스택 -> 자바 메서드 실행 시 필요한 정보 저장

    • 스레드 프라이빗하며, 연결된 스레드와 생명주기가 동일함(생성/삭제 시기 일치)
    • 자바 메서드를 실행하는 스레드의 메모리 모델을 설명
    • 활용
      • 각 메서드가 호출될 때마다 JVM은 스택 프레임을 만들어 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 반환값 등의 정보를 저장함
      • 그런 다음 스택 프레임을 가상 머신 스택에 push하고, 메서드가 끝나면 pop함
    • 발생 가능 오류
      • StackOverflowError : 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이보다 클 때
      • OutOfMemoryError : 스택을 확장하려는 시점에 여유 메모리가 충분하지 않을 때 (스택 용량을 동적으로 확장할 수 있는 JVM에서만)
    • 추가) 지역 변수 테이블
      • JVM이 컴파일타임에 알 수 있는 다양한 기본 데이터 타입, 객체 참조, 반환 주소 타입 저장
        • *기본 데이터 타입: boolean, byte, char, short, int, float, long, double
        • *반환 주소 타입: 바이트코드 명령어의 주소
      • 데이터 타입들을 저장하는 공간을 '지역 변수 슬롯'이라 함(일반적으로 슬롯 하나의 크기는 32bit)
      • 지역 변수 테이블을 구성하는 데 필요한 데이터 공간은 컴파일 과정에서 할당
      • 자바 메서드는 스택 프레임에서 지역 변수용으로 할당받아야 할 공간의 크기가 이미 완벽하게 결정됨(메서드 실행 중에는 절대 변하지 않음)
        • '크기'는 변수 슬롯 개수

     

    1.3 네이티브 메서드 스택

    • 가상 머신 스택과 동일한 역할
    • 가상 머신 스택 : 자바 메서드(바이트코드)를 실행할 때 사용, 네이티브 메서드 스택 : 네이티브 메서드를 실행할 때 용
    • <자바 가상 머신 명세>에 네이티브 메서드 스택에 대한 내용을 명시하지 않았기 때문에 가상 머신 구현자가 원하는 형태로 자유롭게 표현할 수 있음
    • 발생 가능 오류는 가상 머신 스택과 동일하게 스택 허용 깊이 초과 시 StackOverflowError, 스택 확장 실패 시 OutOfMemoryError

     

    1.4 자바 힙

    • 자바 어플리케이션이 사용할 수 있는 가장 큰 메모리
    • 모든 스레드가 공유하며 가상 머신이 구동될 때 만들어짐
    • <자바 가상 머신 명세>에는 '모든 객체 인스턴스와 배열은 힙에 할당된다'라고 적혀있음
    • 자바 힙은 가비지 컬렉터가 관리하는 메모리 영역이기 때문에 GC 힙이라고도 함
    • 메모리 할당 관점에서 자바 힙은 모든 스레드가 공유함. 객체 할당 효율을 높이고자 스레드 로컬 할당 버퍼 여러 개로 나뉨
    • 데이터가 자바 힙에 저장된다는 사실은 달라지지 않음. 자바 힙을 작은 영역들로 구분하는 목적은 오직 메모리 회수와 할당을 더 빠르게 하기 위함
    • <자바 가상 머신 명세>에 따르면 자바 힙은 물리적으로 떨어진 메모리에 위치해도 상관없으나 논리적으로는 연속되어야
      • 대다수 가상 머신이 큰 객체(주로 배열 객체)는 물리적으로도 연속된 메모리 공간을 사용하도록 구현
      • 저장 효율을 높이고 구현 로직을 단순하게 유지하기 위함
    • 확장 가능함 (-Xmx, -Xms 매개변수 사용)
      • 새로운 인스턴스에 할당해 줄 힙 공간이 부족하고 더는 확장할 수 없다면 OutOfMemoryError를 던짐

     

    1.5 메서드 영역

    • 모든 스레드가 공유
    • 가상 머신이 읽어 들인 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 컴파일 코드 캐시 등을 저장
    • 메서드 영역이 꽉 차서 필요한 만큼 메모리를 할당할 수 없다면 OutOfMemoryError 발생

     

    추가) JDK7, JDK 8부터 메서드 영역의 구현 방식 변화

    더보기

    Java 7 이하

    • 메서드 영역의 저장 방식 : PermGen (Permanent Generation, 영구 영역)
    • GC의 대상이 됨
    • 크기가 고정되어 있어 OutOfMemoryError(OOM) 다수 발생 가능
    • JDK8부터 핫스팟은 그 전까지 PermGen에서 관리하던 문자열 상수와 정적 변수 등의 정보를 자바 힙으로 옮김
      • 런타임 상수 풀에는 문자열 리터럴의 참조 정보만 남고, 실제 문자열은 힙의 String Pool로 이동

    Java 8 이상

    • 메서드 영역의 저장 방식 : Metaspace
    • Heap 외부의 네이티브 메모리 영역에서 관리
    • 기본적으로 크기가 제한되지 않으며, 필요하면 OS가 메모리를 동적으로 할당
      • -XX:MaxMetaspaceSize 옵션으로 제한 가능
    • PermGen보다 메모리 관리가 유연하고, OOM 위험이 줄어듦
    • JDK7까지 PermGen에 남아있던 모든 데이터(주로 타입 정보)를 메타스페이스로 옮김

     

    1.6 런타임 상수 풀

    • 메서드 영역의 일부
      • 메서드 영역을 넘어서까지 확장될 수 없음
      • 상수 풀 공간 부족 시 OutOfMemoryError 발생
    • 상수 풀 테이블에는 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보컴파일타임에 생성된 다양한 리터럴과 심벌 참조가 저장
    • 가상 머신이 클래스를 로드할 때 이러한 정보를 메서드 영역의 런타임 상수 풀에 저장
      • <자바 가상 머신 명세>에서 요구사항을 상세히 정의하지는 않았지만, 일반적으로 클래스 파일에 기술된 심벌 참조와 심벌 참조로부터 번역된 직접 참조 역시 저장됨
    • 주요 특징은 동적이라는 점
      • 상수 풀의 내용 전부가 클래스 파일에 미리 완벽하게 기술되어 있는 게 아님
      • 런타임에도 메서드 영역의 런타임 상수 풀에 새로운 상수가 추가될 수 있음
      • String 클래스의 intern() 메서드에 이러한 특성이 반영되어 있음

     

    추가) 다이렉트 메모리

    • 가상 머신 런타임에 속하지 않으며 <자바 가상 머신 명세>에 정의된 영역은 아님
    • JDK 1.4에서 NIO가 도입되면서 채널과 버퍼 기반 I/O 메서드가 등장함
    • NIO는 힙이 아닌 메모리를 직접 할당할 수 있는 네이티브 함수 라이브러리를 이용하고, 이 메모리에 저장되어 있는 DirectByteBuffer 객체를 통해 작업을 수행
      • 따라서 자바 힙와 네이티브 힙 사이에서 데이터를 복사해 주고받지 않아도 됨
    • 가상 머신의 메모리 크기 뿐 아니라, 다이렉트 메모리도 고려하여 모든 메모리 영역의 합이 물리 메모리 한계를 넘지 않도록 해야 함

     

    2. 핫스팟 가상 머신에서의 객체 들여다보기

    내용 : 핫스팟이 관리하는 자바 힙에서의 객체 생성(할당), 레이아웃, 접근 방법 등 전체 과정의 상세 내용

     

    2.1 객체 생성

    가상 머신 관점

    • JVM이 new 명령어에 해당하는 바이트코드를 만나면, 이 명령의 매개변수가 상수 풀 안의 클래스를 가리키는 심벌 참조인지 확인
    • 심벌 참조가 뜻하는 클래스가 로딩, 해석(resolve), 초기화(initialize)되었는지 확인
      • 준비되지 않은 클래스라면 로딩부터 해야 함
    • 로딩이 완료된 클래스라면 새 객체를 담을 메모리를 할당
      • 객체에 필요한 메모리 크기는 클래스를 로딩하고 나면 완벽하게 알 수 있음
      • 객체용 메모리 공간 할당은 자바 힙에서 특정 크기의 메모리 블록을 잘라 주는 것
    • Q. 가용 공간을 어떻게 나눌까?
      • 가상 머신은 가용 메모리 블록들을 목록으로 따로 관리하며, 객체 인스턴스를 담기에 충분한 공간을 찾아 할당한 후 목록을 갱신함
      • 이러한 할당 방식을 여유 목록(free list)이라 함
    • Q. 멀티스레드 환경에서 여러 스레드가 동시에 객체를 생성하려고 할 때 어떻게 해야할까?
      • 1) 메모리 할당을 동기화하는 방법
        • CAS(비교 및 교환)와 실패 시 재시도 방식의 가상 머신은 갱신을 원자적으로 수행함
      • 2) 스레드마다 다른 메모리 공간을 할당하는 방법
        • 스레드 각각이 자바 힙 내에 작은 크기의 전용 메모리를 미리 할당받아 놓는 것
        • 이런 메모리를 스레드 로컬 할당 버퍼(TLAB)라고 함
        • 각 스레드는 로컬 버퍼에서 메모리를 할당받아 사용하다가 버퍼가 부족해지면 그때 동기화를 해 새로운 버퍼를 할당받음
        • 가상 머신이 스레드 로컬 할당 버퍼를 사용할지는 -XX:+/-UseTLAB 매개변수로 설정함

    • 메모리 할당이 끝나면 가상 머신은 할당받은 공간을 0으로 초기화함 (객체 헤더 제외)
      • 스레드 로컬 할당 버퍼 사용할 때 초기화는 TLAB 할당 시 미리 수행
      • 자바 코드에서 객체의 인스턴스 필드를 초기화하지 않고도 사용할 수 있는 이유가 이 단계 덕분임
      • 모든 필드가 각 데이터 타입에 해당하는 0 값을 담고 있게 됨
    • JVM은 각 객체에 필요한 설정을 해줌
      • 아래 정보들이 각 객체의 객체 헤더에 저장
      • ex) 어느 클래스의 인스턴스인지, 클래스의 메타 정보는 어떻게 찾는지, 이 객체의 해시 코드는 무엇인지, GC 세대 나이는 얼마인지 등

    자바 프로그램 관점

    • 가상 머신 관점에서는 새로운 객체가 만들어졌지만, 생성자(클래스의 <init> 메서드)가 아직 실행되지 않았고, 모든 필드는 기본값인 0인 상태
    • new 명령어에 이어 <init>() 메서드까지 실행되어 객체를 의도대로 초기화해야 비로소 사용 가능한 진짜 객체 완성됨

    추가) new 키워드를 컴파일하면?

    더보기
    • 자바 컴파일러는 자바의 new 키워드를 발견하면 바이트코드 명령어인 new와 invokespecial로 변환한다.
    • new는 메모리 할당 단계를 수행하고, invokespecial은 <init>() 메서드 호출을 담당한다.
    • 주의) 자바 코드에서 new가 아닌 다른 방식으로 객체를 생성한 경우 invokespecial이 연이어 나오지 않을 수 있음

     

    2.2 객체의 메모리 레이아웃

    • 핫스팟 가상 머신은 객체를 세 부분으로 나눠 힙에 저장
      • 객체 헤더, 인스턴스 데이터, 길이 맞추기용 정렬 패딩(alignmnet padding)

     

    객체 헤더

    • 1) 마크 워드 - 필수
      • 객체 자체의 런타임 데이터
        • 해시 코드, GC 세대 나이, 락 상태 플래그, 스레드가 점유하고 있는 락들, 편향된 스레드의 아이디, 편향된 시각의 타임스탬프 등
          • *편향된 스레드의 아이디와 편향된 시각의 타임스탬프는 모두 편향 락(biased lock) 관련 사항으로 JDK 18 이후에는 해당 X
      • 차지하는 크기는 (참조 압축 기능을 켜지 않으면) 32비트 가상 머신에서는 32비트, 64비트 가상 머신에서는 64비트임
    • 2) 클래스 워드(klass word) - 필수
      • 객체의 클래스 관련 메타데이터를 가리키는 클래스 포인터 저장
        • 이 포인터를 통해 특정 객체가 어느 클래스의 인스턴스인지 런타임에 알 수 있음
        • 메모리 공간을 절약하기 위해 클래스 관련 메타데이터(klass)는 클래스당 하나씩만 생성한 후 클래스 포인터로 가리키게 함
    • 3) 배열 길이 - 선택
      • 자바 배열의 경우 배열 길이도 객체 헤더에 저장함

    인스턴스 데이터

    • 객체가 실제로 담고 있는 정보
      • 프로그램 코드에서 정의한 다양한 타입의 필드 관련 내용, 부모 클래스 유무, 부모 클래스에서 정의한 모든 필드
    • 저장 순서
      • 가상 머신의 할당 전략 매개변수(-XX:FieldsAllocationStyle)와 자바 소스 코드에서 필드를 정의한 순서에 따라 달라짐
      • 핫스팟 VM은 기본적으로 long/double, int, short/char, byte/boolean, 일반 객체 포인터 순으로 할당
      • 기본 할당 전략에서 필드 길이가 같다면 부모 클래스에서 정의된 필드가 자식 클래스의 필드보다 앞에 배치
      • +XX:CompactFields 매개변수를 true로 설정하면(기본 값 true), 하위 클래스의 필드 중 길이가 짧은 것들은 상위 클래스의 변수 사이사이에 끼워 넣어져 공간 절약 가능

    정렬 패딩

    • 존재하지 않을 수도 있으며, 특별한 의미 없이 자리를 확보하는 역할
    • 핫스파 VM의 자동 메모리 관리 시스템에서 객체의 시작 주소는 반드시 8바이트의 정수배여야 함 (즉, 모든 객체의 크기가 8바이트의 정수배여야 한다는 뜻)
    • 객체 헤더는 정확히 8바이트의 정수배가 되도록 설계되어 있으므로, 인스턴스 데이터가 조건을 충족하지 못하는 경우에만 패딩으로 채움

     

    2.3 객체 접근

    • 대다수 객체는 다른 객체 여러 개를 조합해 만들어짐. 자바 프로그램은 스택에 있는 참조 데이터를 통해 힙에 들어 있는 객체들에 접근
    • 객체에 접근하는 방식은 VM에서 구현하기 나름이며, 주로 핸들이나 다이렉트 포인터를 사용해 구현함
      • <자바 가상 머신 명세>에서 참조 타입을 단지 '객체를 가리키는 참조'라고만 정했을 뿐, 힙에서 객체의 정확한 위치를 알아내어 접근하는 구체적인 방법은 규정하지 않음

     

    1) 핸들 방식

    • 자바 힙에 핸들 저장용 풀이 별도로 존재함
    • 참조에는 객체의 핸들 주소가 저장되고, 핸들에는 다시 해당 객체의 인스턴스 데이터, 타입 데이터, 구조 등의 정확한 주소 정보가 담김
    • 장점
      • 참조에 '안정적인' 핸들 주소가 저장된다는 것
      • GC에 의해 객체가 이동하는 일은 흔함. 핸들을 이용하면 객체의 위치가 바뀌는 상황에서도 참조 자체를 손댈 필요 없음
      • 대신 핸들 내의 인스턴스 데이터 포인터만 변경하면 됨

    2) 다이렉트 포인터

    • 자바 힙에 위치한 객체에서 인스턴스 데이터뿐 아니라 객체 타입 데이터의 주소도 제공해야 함
    • 스택의 참조에는 객체의 실제 주소가 바로 저장됨
    • 장점
      • 속도가 빠름 (핸들을 경유하는 오버헤드 없기 때문)
    • 핫스팟은 주로 다이렉트 포인터 방식을 이용

    댓글

Designed by Tistory.