ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 책 스터디 <JVM 밑바닥까지 파헤치기> - 2장 자동 메모리 관리
    JAVA/Java 2025. 1. 22. 23:47

     

     

    자바 메모리 영역과 메모리 오버플로

    • 자바 개발자는 가상 머신이 제공하는 자동 메모리 관리 메커니즘 덕에 메모리 할당과 해제를 짝지어 코딩하지 않아도 메모리 누수나 오버 플로 문제를 거의 겪지 않음
      • 요즘은 C++만큼 성능을 낼 수 있고 메모리를 안전하게 관리할 수 있는 Rust도 있음

     

     

    2.2 런타임 데이터 영역

    1) 프로그램 카운터

    • 프로그램 카운터
      • 작은 메모리 영역
      • 현재 실행중인 스레드의 바이트코드 줄 번호 표시기
        • 바이트코드 인터프리터
          • 카운터의 값을 바꿔 다음에 실행할 바이트코드 명령어를 선택
        • 프로그램의 제어 흐름, 분기, 순환, 점프 등을 표현
        • 표시기를 통해 예외 처리나 복원 같은 모든 기본 기능 동작
      • 스레드 각각 고유한 프로그램 카운터를 가짐
        • = 스레드 프라이빗 영역에 저장됨
      • 이를 통해 멀티스레드 환경에서 스레드 전환 후 이전에 실행하다 멈춘 지점을 정확하게 복원
      • 스레드가 자바 메서드를 실행 중에 실행중인 바이트코드 명령어의 주소가 프로그램 카운터에 기록됨
        • 스레드가 네이티브 메서드를 실행 중일 때 프로그램 카운터 값은 Undefined
      • 자바 가상 머신 명세에서 OutOfMemoryError 조건이 명시되지 않은 유일한 영역

    2) 자바 가상 머신 스택 (스택) (JVM 랭귀지 스택)

    • 자바 가상 머신 스택
      • 스레드 프라이빗
      • 연결된 스레드와 생성 / 삭제 시기가 일치
      • 자바 메서드를 실행하는 스레드의 메모리 모델을 설명
        • 각 메서드가 호출될 때마다 스택 프레임을 만들어 지역 변수 테이블(우리가 흔히 이야기 하는 스택), 피연산자 스택(계산을 수행하기 위한 값 저장), 동적 링크(실행 중인 메서드의 상위 프레임 정보), 메서드 반환값 등의 정보를 전달
        • 스택 프레임을 가상 머신 스택에 push하고 메서드가 끝나면 pop하는 일을 반복
      • 지역 변수 테이블을 가리킬 때가 많음
        • 지역 변수 테이블에는 자바 가상 머신이 컴파일 타임에 알 수 있는 다양한 기본 데이터 타입, 객체 참조, 반환 주소 타입을 저장
        • 지역 변수 슬롯
          • 지역 변수 테이블에서 데이터 타입을 저장하는 공간
          • 슬롯 하나의 크기는 32비트
            • double의 경우 64비트로 슬롯 2개를 차지, 그외의 경우 모두 슬롯 하나에 저장
        • 지역 변수 테이블 구성에 필요한 데이터 공간(변수 슬롯 개수)은 컴파일 과정에서 할당
          • 자바 메서드는 스택 프레임에서 지역 변수용으로 할당 받아야 할 공간의 크기가 이미 완벽하게 결정됨
          • 메서드 실행중에는 절대 변하지 않음
          • 메모리 실제 크기는 가상 머신의 구현 방법에 따라 달라짐 (슬롯 하나당의 크기가 다를 수 있음)
      • 발생 가능한 예외
        • StackOverflowError
          • 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이보다 큼
        • OutOfMemoryError
          • 스택 용량을 동적으로 확장할 수 있는 자바 가상 머신에서 스택을 확장하려는 시점에 여유 메모리가 충분하지 않음

    3) 네이티브 메서드 스택

    • 네이티브 메서드 스택
      • 가상 머신 스택과 매우 비슷한 역할
        • 가상 머신 스택
          • 자바 메서드(바이트코드)를 실행할 때 사용됨
        • 네이티브 메서드 스택
          • 네이티브 메서드를 실행할 때 사용됨
      • 가상 머신 구현자가 원하는 형태로 자유롭게 표현 가능
        • ex) 핫스팟 가상 머신 등
          • 네이티브 메서드 스택과 가상 머신 스택을 하나로 합쳐놓은 가상 머신
      • 발생 가능한 예외
        • StackOverflowError
          • 스레드가 요청한 스택 깊이가 가상 머신이 허용하는 깊이보다 큼
        • OutOfMemoryError
          • 스택을 확장하려는 시점에 여유 메모리가 충분하지 않음

    4) 자바 힙 (Heap)

    • 자바 힙
      • 자바 애플리케이션이 사용할 수 있는 가장 큰 메모리
      • 모든 스레드가 공유
        • 객체 할당 효율(메모리 회수와 할당)을 높이고자 스레드 로컬 할당 버퍼 여러개로 나뉨
        • 객체의 인스턴스만 저장 가능
      • 가상 머신이 구동될 때 생성
      • 객체 인스턴스를 저장하는 역할
        • 자바 세계의 거의 모든 객체 인스턴스가 이 영역에 할당됨
          • 자바 가상 머신 명세에는 ‘모든 객체 인스턴스와 배열은 힙에 할당한다.’고 되어있음
          • 자바 언어가 계속 발전하면서 앞으로는 값타입도 지원할 것으로 보임
            • ex) JIT 컴파일러의 발전으로 스택 할당과 스칼라 치환 최적화 방식이 달라짐
              • 인스턴스로 올렸으나, 로컬에서만 사용하면 컴파일러가 스택으로 변경해줌
      • 가비지 컬렉터가 관리하는 메모리 영역
        • = GC 힙
          • new GC, old GC, 영구 세대, eden space, from survivor space, to survivor space
        • 명세에 힙의 세부 영역 구분에 관한 이야기는 없음
          • ex) 핫스팟 가비지 컬렉터 (G1 컬렉터)
            • 두 신세대 컬렉터와 구세대 컬렉터를 조합해 동작하는 전통적인 세대 구분을 따름
      • 명세에 따르면 자바 힙은 물리적으로 떨어진 메모리에 위치해도 상관 없으나 논리적으로는 연속되어야 함
        • 파일을 저장할 때 디스크 공간을 활용하는 방식과 동일
        • 대다수 가상 머신이 큰 객체(주로 배역)는 물리적으로도 연속된 메모리 공간을 사용하도록 구현
          • 저장 효율을 높이고 구현 로직을 단순하게 유지
      • 자바 힙의 크기는 고정하거나 확장할 수 있게 구현 가능
        • 최근의 주류 가상 머신은 모두 확장 가능한 형태로 구현
          • ex) -Xmx 사용
      • 발생 가능한 예외
        • OutOfMemoryError
          • 새로운 인스턴스에 할당해 줄 공간이 부족하고 힙을 더는 확장할 수 없는 경우

    5) 메서드 영역

    • 메서드 영역
      • 모든 스레드가 공유
      • 가상 머신이 읽어들인 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 코드 캐시 등을 저장
      • non heap 영역
        • 논리적으로는 힙의 한 부분으로 기술될 수 있음
      • 가상 머신에 따라 힙처럼 관리되거나 관리되지 않을 수 있음
        • ex) JDK 7까지
          • 핫스팟 가상 머신은 가비지 컬렉터의 수집 범위를 메서드 영역까지 확장
          • 메서드 영역을 영구세대에 구현
            • ⇒ 자바 애플리케이션이 메모리 오버플로를 겪을 가능성이 커짐
              • 영구 세대의 최대 크기는 -XX:MaxPermSize로 제한되며 이 값을 설정하지 않더라도 기본값이 정해져 있음
          • 자바 힙처럼 관리됨
            • 메서드 영역 관리 코드가 따로 필요 없음
            • JDK 7에서 문자열 상수, 정적 변수등의 정보를 자바 힙으로 옮김
        • ex) BEA Jrockit, IBM J9 등
          • 영구 세대라는 개념이 없음
          • 프로세스가 쓸 수 있는 메모리 최댓값에 손을 대지 않음
        • ex) JDK 8
          • 영구 세대에 남아있던 모든 데이터(ex) 타입 정보)를 네이티브 메모리에 메타 스페이스를 구현
        • 영구 세대 때문에 가상 머신에 따라 성능이 달라지는 메서드가 있음
          • ex) String::intern()
      • 자바 힙과 마찬가지로 연속될 필요가 없음
      • 크기를 고정하거나 확장 가능
      • 가비지 컬렉션을 하지 않아도 됨
        • 쓰레기를 회수할 일이 거의 없음
          • 회수할 대상이 거의 대부분 상수 풀과 타입이라서 회수 효과가 작음
          • 타입의 경우 회수 조건이 상당히 까다로움
        • 가끔 메모리 누수가 있는 케이스의 경우 필요함
      • 발생 가능한 예외
        • OutOfMemoryError
          • 메서드 영역이 꽉 차서 필요한 만큼 메모리 할당이 불가능 한 경우

    6) 런타임 상수 풀

    • 런타임 상수 풀
      • 메서드 영역의 일부
      • 상수 풀 테이블
        • 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보에 더해 컴파일 타임에 생성된 다양한 리터럴과 심벌 참조가 저장
      • 가상 머신이 클래스 로드시 메서드 영역의 런타임 영역의 상수 풀에 저장
      • 명세에는 클래스 파일 각 영역별 엄격한 규칙이 있지만, 런타임 상수 풀에 대해서는 상세 요구 사항이 없음
        • 필요에 따라 구현 가능
        • 그러나, 클래스 파일에 기술된 심벌 참조, 심벌 참조로부터 번역된 직접 참조는 런타임 풀에 저장되는 것이 일반적
      • 클래스 상수 풀에 비해 동적임
        • 자바에서 상수가 꼭 컴파일 타임에 생성되어야 하는 규칙이 없기 때문에 런타임에도 메서드 영역의 런타임 상수 풀에 새로운 상수가 추가될 수 있음
          • ex) String::intern()
      • 발생 가능한 예외
        • OutOfMemoryError
          • 메서드 영역에 속하므로 메서드 영역을 넘어서까지 확장 불가능 하기 때문에 상수 풀의 공간이 부족하면 OutOfMemoryError를 던짐

    7) 다이렉트 메모리

    • 다이렉트 메모리
      • 가상 머신 런타임에 속하지 않으며 자바 가상 머신 명세에 정의된 영역이 아님
      • 자주 쓰이는 메모리
        • OutOfMemory의 원인이 될 수 있음
        • JDK 1.4에서 NIO도입으로 채널과 버퍼 기반 I/O 메서드가 소개됨
          • 힙이 아닌 메모리를 직접 할당할 수 있는 네이티브 함수 라이브러리를 이용
          • 이 메모리에 저장되어있는 DirectByteBuffer 객체를 통해 작업을 수행할 수 있음
            • 자바 힘과 네이티브 힙 사이에서 데이터를 복사해 주고받지 않아도 되어 일부 시나리오에서 성능을 크게 개선
        • 물리 메모리는 직접 할당하여 자바 힙 크기의 제약과는 무관
        • 메모리이기 때문에 하부 기기의 총 메모리 용량(물리 메모리, 스와프 파티션, 페이징 파일 포함)과 프로세서가 다룰 수 있는 주소 공간을 넘어설 수는 없음
          • 가상머신의 메모리 뿐 아니라 다이렉트 메모리도 고려해야함
      • 발생 가능한 예외
        • OutOfMemoryError
          • 사용되는 메모리 영역의 합이 물리 메모리 한계(물리적 제약과 운영 체제 수준의 제약 포함)를 넘어서면 동적 확장을 시도하면서 예외 발생

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

    • 핫스팟
      • 가장 보편적인 가상 머신
      • 핫코드 감지 (page.21 ~ 참고)
    • 자바 힙
      • 가장 보편적인 메모리 영역

    1) 객체 생성

    포인터 밀치기 (bump the point)
    여유 목록
    스레드 로컬 할당 버퍼 (TLAB : Thread Local Allocation Buffer )

    • 자바
      • 객체 지향 프로그래밍 언어
      • 프로그램이 동작하는 동안 수시로 객체 생성
      • 순서
        • new 명령에 해당하는 바이트코드를 만나면 명령의 매개 변수가 상수 풀안의 클래스를 가리키는 심벌 참조인지 확인
        • 심벌 참조가 뜻하는 클래스가 로딩, 해석(resolve), 초기화(initialize)되었는지 확인
          • 준비되지 않은 클래스라면 로딩부터 실시
          • 로딩이 완료된 클래스라면 새 객체를 담을 메모리를 할당
            • 객체에 필요한 메모리 크기는 로딩하고 나면 완벽하게 알 수 있음
            • 객체용 메모리 공간 할당은 자바 힙에서 특정 크기의 메모리 블록을 잘라 주는것
              • 포인터 밀치기 (bump the point)
                • https://os.phil-opp.com/allocator-designs/
                • 메모리 할당 시 포인터를 여유공간 쪽으로 객체 크기만큼 이동시킴
                • 모으기가 가능한 컬렉터를 사용하는 시스템
                  • ex) 시리얼과 파뉴(ParNew)
                 
            • 사용중인 메모리와 여유 메모리가 뒤섞여 있다면 밀치기가 쉽지 않음
              • 여유 목록
                • 가상 머신은 가용 메모리 블록들을 목록으로 따로 관리하여 객체 인스턴스를 담기에 충분한 공간을 찾아 할당 후 목록 갱신
                • 이론상의 스윕(sweep: 쓸기) 알고리즘을 적용한 컬렉터를 쓰는 시스템
                  • ex) CMS
            • ⇒ 자바힙의 규칙여부에 따라 달라지며 자바 힙의 규칙 여부는 가비지 컬렉터의 컴팩트(모으기)가능 여부에 달림
      • 멀티 스레딩 환경에서의 객체 생성
        • 여유 메모리의 시작 포인터 위치를 수정하는 일도 스레드 세이프 하지 않음
        1. 메모리 할당
          • 방법 1) 메모리 할당 동기화
            • 비교 및 교환(CAS)과 실패 시 재시도 방식의 가상 머신은 갱신을 원자적으로 수행
              • 락 등을 활용
          • 방법 2) 스레드마다 다른 메모리 공간을 할당
            • 스레드 로컬 할당 버퍼 (TLAB : Thread Local Allocation Buffer )
            https://www.programmersought.com/article/23116594948/#google_vignette
            • 스레드 각각이 자바 힙 내에 작은 크기의 전용 메모리를 미리 할당
            • 각 스레드는 로컬 버퍼에서 메모리를 할당 받아 사용하다가 버퍼 부족시 동기화해 새로운 버퍼를 할당 받음
            • -XX:+/-UseTLAB를 통해 가상 머신이 스레드 로컬 할당 버퍼 사용 결정
        2. 메모리 할당이 끝나면 가상 머신은 객체 헤더를 제외하고 할당받은 공간을 0으로 초기화
          • 스레드 로컬 할당 버퍼를 사용한다면 초기화는 TLAB 할당 시 미리 수행
          • ⇒ 자바 코드에서 객체의 인스턴스 필드를 초기화하지 않고도 사용할 수 있는 이유
        3. 객체 헤더에 각 객체에 필요한 설정을 해줌
          • ex) 어느 클래스의 인스턴스 인지, 클래스 메타정보 찾는 방법, 객체의 해시코드, GC 세대 나이 등
          • 할당 방법은 가상 머신의 현재 구동 모드에 따라 달라짐 (편향 락 활성화 등)
      • init 메서드 실행
        • 자바 프로그램 관점에서는 생성자가 실행되지 않았기 때문에 모든 필드가 기본값인 0인 상태
        • 사용가능한 진짜 객체 상태로 완성

    2) 객체의 메모리 레이아웃

    • 객체
      • 객체 헤더
        • 유형
          • 객체 자체의 런타임 데이터
            • 마크워드(mark word)
              • 해시 코드, GC 세대 나이, 락 상태 플래그, 스레드 점유 락, 편향 스레드 아이디, 편향된 시각의 타임 스탬프 등
            • 32 비트 가상머신에서는 32 비트, 64 비트 가상 머신에서는 64 비트
            • 객체 헤더에는 객체 자체가 정의한 데이터와 관련없는 데이터까지 담아야 해서 한정된 메모리를 최대한 효율적으로 써야 함
              • 마크워드의 데이터 구조는 동적으로 의미가 달라짐
              • 적은 공간에 가능한 많은 정보를 담고 객체 상태에 따라 공간을 재활용 할 수 있도록 함
          • 객체의 클래스 관련 메타데이터를 가리키는 클래스 포인터
            • 클래스 워드 (klass word)
              • 특정 객체가 어느 클래스의 인스턴스인지 런타임에 알 수 있음
              • 메타 데이터 정보는 반드시 객체 자체에서 찾아야 하는 데이터가 아니기 때문에 클래스 포인터를 객체 헤더에 저장하지 않을 수도 있음
          • 자바 배열의 경우 배열 길이도 객체 헤더에 저장
            • 객체 헤더에 저장되는 객체 타입은 배열에 담긴 원소의 타입
            • 배열 길이(원소 개수)까지 알아야 배열 객체가 차지하는 메모리 크기를 제대로 계산 가능
      • 인스턴스 데이터
        • 객체가 실제로 담고 있는 정보
          • ex) 타입의 필드 관련 내용, 부모 클래스 유무, 부모 클래스에서 정의한 모든 필드
        • 가상 머신의 할당 전략 매개변수 (-XX:FieldsAllocationStyle)와 자바 소스 코드에서 필드를 정의한 순서에 따라 달라짐
          • ex) 핫스팟
            • long, double, int, short, char, byte, boolean, 일반 객체 포인터 순
          • 기본 할당 전략에서는 길이가 같은 필드들은 항상 같이 할당되고 저장
          • 필드 길이가 같다면 부모 클래스에서 정의된 필드가 자식 클래스의 필드보다 앞에 배치
        • -XX:CompactFields 매개 변수를 true(default)로 설정하면 하위 클래스의 필드 중 길이가 짧은 것들은 상위 클래스의 변수 사이사이에 끼워 넣어져서 공간이 조금이나마 절약됨
      • 정렬 패딩 (길이 맞추기용, alignment padding)
        • 자리를 확보하는 역할
        • 핫스팟 가상 머신의 자동 메모리 관리 시스템에서 객체의 시작 주소는 반드시 8바이트의 정수배여야 함
          • 모든 객체의 크기가 8바이트의 정수배여야 함
            • IO의 효율을 높이기 위함일까?
        • 객체 헤더의 경우 정확히 8바이트의 정수배가 되도록 설정되어있음
          • 인스턴스 데이터가 조건을 충족하지 못하는 경우 패딩으로 채움
          • 인코딩할때도 패딩 붙여서 함 ⇒ IO를 최적화 하기 위함일까?

    3) 객체에 접근하기

    • 대다수의 객체는 다른 객체 여러개를 조합해 만들어짐
    • 자바 프로그램은 스택에 있는 참조 데이터를 통해 힙에 들어있는 객체들에 접근해 이를 조작
    • 객체에 접근하는 방식은 가상 머신에서 구현하기 나름
      • 주로 핸들이나 다이렉트 포인터를 사용해 구현
        • 핸들
          • 자바 힙에 핸들 저장용 풀이 별도로 존재
            • 참조에는 객체의 핸들 주소, 핸들에는 다시 해당 객체의 인스턴스 데이터, 타입 데이터, 구조 등 정확한 주소 정보
          • 참조에 안정적인 핸들의 주소가 저장됨
            • 핸들을 이용하면 가비지 컬렉션 과정에서 객체가 이동하는 일에도 참조 자체는 손댈 필요 없음
              • 핸들 내의 인스턴스 데이터 포인터만 변경하면 됨
        • 다이렉트 포인터
          • 자바 힙에 위치한 객체에서 인스턴스 데이터 뿐 아니라 타입 데이터에 접근하는 길도 제공해야 함
          • 스택의 참조에는 객체의 실제 주소가 바로 저장
            • 핸들을 경유하는 오버헤드가 없기 때문에 속도가 빠름
            • 핫스팟의 경우 다이렉트 포인터 방식을 이용
              • 예외적으로 셰넌도어 컬렉터를 이용하면 추가적인 포워드를 거침

    2.3 OutOfMemoryError 예외

    • 프로그램 카운터 외에도 가상 머신 메모리의 여러 런타임 영역에서 OutOfMemoryError가 발생할 수 있음

    1) 자바 힙 오버플로

    • 자바 힙 오버플로
      • 객체 인스턴스를 저장하는 공간
        • 객체를 계속 생성하고 객체들에 접근할 경로가 살아있다면 언제가는 힙의 최대 용량을 넘어섬
        • 최소 크기는 -Xms, 최대 크기는 -Xmx
          • -XX:+HeapDumpOnOutOfMemoryError 매개 변수를 설정하면 메모리가 오버플로됐을 때 가상 머신이 예외 발생 지점의 힙 메모리 스냅샷을 파일로 저장(dump)
      • 자바 애플리케이션에서 OutOfMemoryError가 가장 많이 발생하는 영역
      • 메모리 이미지 분석 도구로 힙덤프 스냅샷을 분석해볼 수 있음
        • 메모리 누수인지, 오버플로인지 확인해야 함
          • 필요없는 객체가 원인이라면 메모리 누수
            • 메모리 누수라면 도구를 이용해 누수된 객체로부터 GC 루트까지의 참조 사슬(reference chain)을 살펴봐야 함
              • 해당 객체가 만들어진 위치를 더 정확하게 파악
          • 메모리 누수가 아니라면(오버플로) 모든 객체가 다 살아있어야 함
            • 힙 매개 변수 설정을 가용 메모리와 비교해 가상 머신에 메모리를 더 많이 할당할 수 있는지 파악
            • 코드에서 수명 주기가 너무 길거나 상태를 너무 오래 유지하는 객체가 없는지, 공간 낭비가 심한 데이터 구조를 쓰고 있지 않은지 살펴본 수 런타임에 소비하는 메모리를 최소로 낮추기
              • ⇒ 스프링의 경우 빈들은 생명주기가 길 것 같은데, 어떻게 판단해서 관리하고 있을까?
              • ⇒ 공간 낭비가 심한 데이터 구조는? (맵 아닐까? 참조만 시켜놓으면 GC가 해소하지 못하니까 ⇒ 메모리가 부족하면 GC가 해소할 수 있는 자료구조도 있음(weak reference) )
                • 처음에는 최소한의 필요한 클래스만 메모리에 올리고 힙에 두고 가져오는게 아닐까?
                • 클래스는 다 띄워놓고 정보를 가져오는건 다를까?
                • 웜업을 고려하는 것은 다띄워놓고 있지 않기 때문 아닐까?

    2) 가성 머신 스택과 네이티브 메서드 스택 오버플로

    • 핫스팟 가상 머신은 가상 머신 스택과 네이티브 메서드 스택을 구분하지 않음
      • 네이티브 메서드 스택의 크기를 설정(-Xoss)하더라도 효과가 없음
      • 스택의 크기는 오직 -Xss 로만 변경 가능
    • 핫스팟 가상 머신은 스택 확장을 지원하지 않음
      • 스레드 생성시 메모리 부족으로 인한 OutOfMemoryError 를 제외하고 스레드 실행중에 가상 머신 스택이 넘치는 일이 없음
        • = 스택 용량이 부족하여 새로운 스택 프레임을 담을 수 없을 때만 StackOverflowError 발생
      • StackOverflowError 테스트 케이스
          1. -Xss 로 스택 메모리 용량을 줄이는 경우
          1. 지역 변수를 많이 선언해 메서드 프레임의 지역 변수 테이블 크기를 키우는 경우
          • 예외 발생 시 출력된 스택 깊이는 의도대로 축소되어 있음
          • 다른 가상 머신에서는 스택 크기를 동적으로 확장할 수 있다면 OutOfMemoryError를 낼수 있음
    • 스레드를 계속 만들어 낸다면, 핫스팟에서도 메모리 오버플로가 발생할 수 있음
      • 운영체제 자체의 메모리 사용 상태에 영향을 크게 받을 뿐 스택 공간이 충분한 지와는 아무런 관련이 없음
      • 스레드별 스택을 크게 잡을 수록 메모리 오버플로를 쉽게 발생 가능
        • 운영체제가 각 프로세스에 할당하는 메모리 크기가 제한적이기 때문
      • 핫스팟 가상머신에서는 자바 힙과 메서드 영역의 최댓값을 매개변수로 설정 가능
        • 남은 메모리 영역 = 운영체제 한계 (2GB) - 힙 최대크기 - 메서드 영역 최대 크기
          • 프로그램 카운터 메모리는 아주 작음
        • 다이렉트 메모리와 자바 가상 머신 프로세스가 자체적으로 소비하는 메모리를 제외한 나머지가 가상 머신 스택과 네이티브 메서드 스택에 할당됨
      • 각 스레드에 스택 메모리를 많이 할당하면 생성할 수 있는 스레드 수가 작아짐
        • 새로운 스레드를 생성할 때 메모리가 고갈될 가능성이 커짐
      • ⇒ 많은 스레드가 필요한 경우에는 힙 크기와 스택 용량을 줄여야 함 (메세지에 unable to create native thread 인 경우)
        • 힙크기와 스택 용량을 줄여서 중간단계의 용량을 늘려야 함
          • 스택은 위에서 내려오고 힙은 아래서 올라가는 구조
      • 💡 윈도우에서 구동되는 가상머신은 기본적으로 자바 스레드를 운영체제의 커널 스레드와 매핑시켜 스레드를 무한정 만들면 운영 체제에 엄청난 압박이 가해져 운영체제가 멈출 수 있음
    • StackOverflowError의 경우 예외를 일으킨 스택 정보가 분석용을 제공되어 문제 위치를 찾기 쉬움

    3) 메서드 영역과 런타임 상수 풀 오버플로

    • 런타임 상수 풀은 메서드 영역에 속하므로 두 영역의 오버플로 테스트는 함께 수행 가능
    • String::intern()
      • 네이티브 메서드
      • 똑같은 문자열이 이미 존재한다면 풀에 있던 기존 문자열의 참조를 반환
      • 같은 문자열이 없다면 현재 String 객체에 담긴 문자열이 상수 풀에 추가되고 이 String의 참조가 반환됨
        • StringBuilder로 생성한 일반 문자열 값과 String::intern()을 사용한 값을 ==비교를 하면
        • JDK 6에서는 false
          • 처음 만나는 문자열 인스턴스를 영구 세대의 문자열 상수 풀에 복사한 다음 영구 세대에서 저장한 문자열 인스턴스의 참조를 반환하고 StringBuilder로 생성한 문자열 객체의 인스턴스는 자바 힙에 존재하기 때문
        • JDK 7에서는 true
          • 문자열 인스턴스 풀 위치가 힙이므로 풀에 있는 첫번째 인스턴스의 참조로 바꾸면 됨
          • 반환 값들이 모두 힙에 존재하기 때문에 StringBuilder가 생성한 문자열 인스턴스와 같음
    • 세대별
      • JDK 6 까지는 영구 세대에 저장
        • -XX:PermSize, -XX:MaxPermSize 매개변수로 조절 가능
        • 상수 풀 용량에도 간접적으로 영향
          • ex) OutOfMemoryError : PermGen space
      • JDK 7 이상에서는 자바 힙으로 옮겨짐
        • -XX:PermSize, -XX:MaxPermSize 으로는 아무런 예외 발생하지 않음
        • -Xmx으로 힙 영역 변경 시 예외 발생
          • ex) OutOfMemoryError : Java heap space
    • 메서드 영역
      • 타입 관련 정보 저장의 역할
      • 런타임에 메서드 영역이 가득찰 때 까지 계속해서 클래스를 생성
        • 클래스를 동적으로 생성하는 방법을 통해 테스트 가능
      • 빈번히 실행되는 코드가 동적 클래스를 많이 생성한다면, 이 클래스들을 회수하는 시점에 특별이 신경써야 함
        • ex) 동적 JSP 파일 생성하는 웹 사이트 또는 애플리케이션
          • JSP가 실행되려면 먼저 자바 클래스로 컴파일 해야함
        • ex) OSGi 애플리케이션(같은 클래스 파일이라도 다른 로더가 읽어 들였다면 다른 클래스로 간주)
      • JDK 8 부터는 메타스페이스를 이용
        • -XX:MaxMetaspaceSize
          • 메타스페이스의 최대 크기를 설정
        • -XX:MetaspaceSize
          • 메타스페이스의 초기 크기를 바이트 단위로 지정
          • 가득차면 가비지 컬렉터가 언로딩을 시도하고 크기를 조정
          • 공간 확보되면 값을줄이고, 확보되지 않았다면 적절한 값으로 증가
          • 단, -XX:MaxMetaspaceSize로 최댓값을 설정했다면 그 값을 초과 불가
        • -XX:MinMetaspaceFreeRatio
          • 가비지 컬렉션 후 가장 작은 메타스페이스 여유 공간의 비율
          • 메타스페이스 공간이 부족해 발생하는 컬렉션 빈도를 줄일 수 있음
        • -XX:MaxMetaspaceFreeRatio
          • 가장 큰 메타스페이스의 남은 공간 비율을 설정

    4) 네이티브 다이렉트 메모리 오버플로

    • -XX:MaxDirectMemorySize
      • 다이렉트 메모리 용량 설정
      • 따로 설정하지 않았다면 -Xms 로 설정된 자바 힙의 최댓값과 동일
    • 다이렉트 메모리에서 발생한 메모리 오버플로는 힙덤프 파일에서는 이상한 점을 찾을 수 없음
      • 메모리 오버플로로 생성된 덤프 파일이 매우 작고 프로그램에서 DirectMemory를 직접 또는 간접적으로(보통 NIO를 통해) 사용했다면, 다이렉트 메모리에서 원인을 찾는데에 집중해야 함

    JPA 등을 사용하면 쿼리 플랜을 사용하게 되고 이때 기본값은 2G로 설정되어 있어 디폴트 값이 크다.

    해당 기본 값을 항상 사용중으로 되고, 나머지로 메모리를 사용해야 해서 OutOfMemoryError 발생율이 많을 수 있음.

     

     

     


    + 💡 chatGPT한테 물어본 내용 

     

    새로운 스레드가 생성될 때 가상 머신 스택, 네이티브 메서드 스택, 그리고 프로그램 카운터(PC) 레지스터가 할당됩니다. 특히 가상 머신 스택과 프로그램 카운터는 모든 스레드에서 필수적으로 사용되는 핵심 메모리 공간입니다.

     

    Q : 각 스레드에 스택 메모리를 많이 할당하면 생성할 수 있는 스레드 수가 작아짐 - 새로운 스레드를 생성할 때 메모리가 고갈될 가능성이 커져서 많은 스레드가 필요한 경우에는 힙 크기와 스택 용량을 줄여야 하는 이유는? (메세지에 unable to create native thread 인 경우)

    A : 스레드 수 제한 문제해결 방법의 관계는 JVM 메모리 구조와 OS가 스레드 메모리를 관리하는 방식에 있습니다. 이를 이해하기 위해 각각의 개념과 해결 방법이 어떻게 작동하는지 설명하겠습니다.

    1. 스레드 생성과 메모리의 관계

    • JVM에서 새로운 스레드가 생성될 때, 스레드별 데이터 영역(가상 머신 스택, 네이티브 메서드 스택, 프로그램 카운터 등)이 할당됩니다.
    • 각 스레드가 할당받는 스택 메모리 크기는 JVM 옵션으로 설정된 Xss 값에 따라 달라집니다.
      • 예: Xss1m로 설정하면 스레드 하나당 1MB의 스택 메모리가 할당됩니다.
    • 동시에 JVM은 **힙 영역(Heap)**에도 메모리를 사용하며, 이는 모든 스레드가 공유합니다.

    2. 스레드 생성 실패 원인

    1. 스레드당 메모리 소비가 너무 큼
      • 스레드 하나가 차지하는 스택 메모리 크기가 크면, 생성 가능한 스레드의 총 개수가 줄어듭니다.
      • 이는 OS에서 제공하는 프로세스 메모리 한도를 초과할 가능성을 높입니다.
    2. 힙 메모리가 너무 큼
      • JVM 힙 크기(Xmx)가 지나치게 크면, 남은 프로세스 메모리에서 스레드 스택 메모리를 할당할 공간이 부족해집니다.
      • 결과적으로 "unable to create native thread" 오류가 발생합니다.

    3. 해결 방법: 힙 크기와 스택 용량 줄이기

    • 힙 크기와 스택 용량을 조정하면, 한 프로세스에서 사용할 수 있는 전체 메모리를 보다 효율적으로 분배할 수 있습니다.

    (1) 스택 크기 줄이기 (Xss)

    • 스레드 하나당 할당되는 스택 메모리 크기를 줄이면, 같은 메모리 내에서 더 많은 스레드를 생성할 수 있습니다.
      • 예: Xss512k로 설정하면 스레드 하나가 512KB만 사용하므로, 동일한 메모리에서 더 많은 스레드를 생성 가능.

    (2) 힙 크기 줄이기 (Xmx)

    • 힙 영역을 작게 설정하면, 나머지 메모리를 스레드 스택 용도로 활용할 수 있습니다.
      • 예: Xmx1g에서 Xmx512m로 줄이면, 남는 공간이 늘어나 스레드 생성 여유가 생김.

    4. 해결 원리

    1. 메모리 여유 확보
    2. 힙 크기와 스택 크기를 줄이면 JVM이 사용하는 전체 메모리가 감소하고, 스레드 생성 시 OS에서 추가로 메모리를 할당받을 여유가 생깁니다.
    3. 스레드당 메모리 감소
    4. 스택 크기를 줄이면 각 스레드가 차지하는 메모리 양이 줄어들어, 같은 메모리에서 더 많은 스레드를 생성할 수 있습니다.
    5. OS 한계 완화
    6. OS는 각 프로세스에 할당할 수 있는 메모리와 스레드 개수를 제한합니다. 힙과 스택 크기를 최적화하면 OS의 메모리 한도를 효율적으로 사용할 수 있습니다.

    5. 설정 예시

    • 힙 크기와 스택 크기를 줄이는 명령:
    • bash 코드 복사 java -Xmx512m -Xss256k -jar your-application.jar
    • 이 설정은:
      • 힙 크기를 512MB로 제한하여 여유 메모리를 확보.
      • 스레드당 스택 크기를 256KB로 설정하여 더 많은 스레드를 생성 가능.

    결론

    많은 스레드가 필요한 상황에서는 힙 크기와 스택 크기를 줄여 프로세스 메모리의 사용 효율을 높이는 방식으로 스레드 생성 실패 문제를 해결할 수 있습니다. 스택 크기를 줄이면 개별 스레드가 사용하는 메모리가 감소하고, 힙 크기를 줄이면 스레드 생성에 필요한 추가 메모리를 확보할 수 있기 때문입니다.

     

Designed by Tistory.