Be-Developer

JVM 밑바닥까지 파헤치기 : 2 자바 메모리 영역과 메모리 오버플로

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

2.2 런타임 데이터 영역

1. 프로그램 카운터 레지스터 PC

  • 현재 실행중인 스레드의 바이트 코드 줄 번호 표시기
  • 스레드 프라이빗 영역
  • OOM 조건이 명시되지 않은 유일한 영역.
  • CPU 코어가 여러 스레드를 동시에 실행할때, 시분할로 실행되므로, 스레드 전환 후 이전에 실행하다 멈춘 지점을 정확하게 복원하려면 스레드 각각의 pc 가 필요하다.
  • 스레드가 네이티브 메서드를 실행중일때 프로그램 카운터 값은 Undefined 이다.

2. 자바 가상 머신 스택

  • 가상머신 스택 : stack frame 을 stack으로 관리한다.
  • stack frame : 메서드가 실행될때마다 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 반환값 등 정보를 저장한다.
  • 스레드 프라이빗 영역
  • StackOverFlow, OOM 발생가능.

지역변수 테이블

  • 컴파일 타임에 알 수 있는 데이터 타입, 객체 참조, 반환 주소 타입 저장.
  • 지역 변수 슬롯 : 일반적으로 슬롯 하나당 32 bit 이며, double 타입은 두개의 슬롯을 차지한다.
    • 컴파일 과정에서 지역변수테이블의 크기는(슬롯 갯수는) 할당된다.

3. 네이티브 메서드 스택

  • 가상머신 스택과의 차이는 네이티브 메서드를 실행할 때 사용한다는 것.
  • 스레드 프라이빗 영역
  • StackOverFlow, OOM 발생가능.

4. 자바 Heap (= GC Heap)

  • 모든 스레드가 공유하는 영역
  • 객체 인스턴스를 저장하는 곳
  • GC가 관리하는 메모리영역이므로, GC Heap 이라고도 불린다.
  • 자바 힙을 다시 작게 구분하는 목적은 메모리 회수와 할당을 더 빠르게 하기 위함.
    • new generation, old generation, 영구세대, eden space,.. 다음 장에서 더 상세히 다룸.
  • Xmx, Xms 로 크기 조정 가능.
  • OOM 발생 가능

5. 메서드 영역 (= non Heap)

  • 모든 스레드가 공유한다.
  • VM이 읽어들인 타입정보, 상수, 정적변수, JIT 컴파일러가 컴파일한 코드 캐시 등 저장.
  • JVM7 까지 메서드 영역까지 GC 대상으로 두었던 시절엔 힙메모리의 영구세대에 구현되어있었다.
  • JVM8 부터 MetaSpace 영역으로, 힙메모리에서 분리되어 OS가 관리하는 네이티브 메모리에 올라간다. (2.4.3 참고)
  • OOM 발생 가능.

5.1 런타임 상수풀

  • 상수 풀 테이블에는 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 설명 정보 + 컴파일 타미에 생성된 다양한 리터럴과 심벌 참조.
  • 클래스를 로드할때 올림.
  • 클래스파일의 상수풀과 비교하여 동적이다. 런타임에도 메서드 영역의 런타임 상수풀에 새로운 상수가 추가될 수 있다.
  • JVM7 부터 힙메모리에 위치한다. (2.4.3 참고)
  • ex) String.intern()

7. 다이렉트 메모리

  • 가상머신 런타임에 속하지 않음.
  • NIO (NotI/O) 는 힙이 아닌 메모리를 직접 할당할 수 있는 네이티브 함수 라이브러리를 사용함.
  • 물리 메모리 한계를 넘으면 OOM 발생 가능.

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

new 객체 생성시

바이트코드 단계

  1. new : 메모리 할당, JVM 관점에서 객체 생성
  2. invokespecial : 생성자 실행 , 자바 프로그램 관점에서 객체 생성

    메모리 할당 방식

    • 포인터 밀치기 할당방식
    • 그림 2.2 참고
    • 사용중인 메모리 뒤에 객체를 할당하여 포인터가 증가되는 방식
    • 자바 힙이 규칙적이다 = GC가 compact(모으기)를 할 수 없다. - 여유 목록 (free list)
    • 객체 인스턴스를 담기에 충분한 공간을 찾아 할당 후 목록을 갱신.
    • 자바 힙이 불규칙적이다 = GC가 compact(모으기)를 할 수 있다.

메모리 할당 동기화 방식

  • 멀티 스레딩 환경에서는 메모리 포인터 수정이 위험할 수 있다.
  • 갱신을 원자적으로 수행. (단일 스레드가 메모리 할당을 처리)
  • 스레드 로컬 할당 버퍼 TLAB
    • XX:+/-UseTLAB
    • 스레드마다 다른 메모리 공간을 미리 할당받아놓는방식.

2.3.2 객체의 메모리 레이아웃

  • 그림 2-4 참고

객체 헤더

  1. mark word
    • 32bit or 64bit, 8byte 단위로 고정됨.
    • 객체 해시코드, GC 세대 나이, 락 상태 플래그, 스레드가 점유하고있는 락들, 편향된스레드 아이디, 편향된 시각의 타임스탬프 등..

      편향되었다는게 무엇을 의미하는지?

  2. Klass word
    • 객체의 클래스 관련 메타데이터를 가리키는 클래스 포인터.
    • 객체가 배열인경우 원소의 타입이 저장된다.
  3. array length
    • 배열인경우 배열 길이

인스턴스 데이터

: 객체가 실제로 담고있는 정보.

  • 필드 순서 할당 전략
    • -XX:FieldsAllocationStyle 기본 : 길이가 같은 필드들은 항상 같이 할당되고 저장된다.
    • -XX:CompactFields 길이가 짧은 필드를 상위 클래스 변수 사이에 배치하여 공간절약.

      왜 기본전략인건지? CompactFields 의 단점은?

정렬 패딩

: 인스턴스데이터가 8byte단위로 끊기지 않을 수 있으므로, 8btye를 맞춰주기위한 패딩

2.3.3 객체에 접근하기

  • 그림 2.5, 2.6 참고

    핸들방식

  • heap 에 핸들풀과 인스턴스풀이 존재하며, 핸들풀에는 객체의 참조 포인터만 존재(인스턴스 데이터 포인터, 타입 데이터 포인터)
  • 장점 : GC 과정에서 객체 인스턴스의 위치이동이 빈번한데, 위치이동이 되더라도 핸들풀의 참조는 변경하지 않아도 된다.

    다이렉트 포인터

  • 핫스팟 JVM 의 기본 전략
  • heap 에 풀이 존재하지 않으며, 참조포인터와 데이터가 한묶음.(인스턴스 데이터, 타입 데이터 포인터)
  • 장점 : 속도

2.4 실전:OutOfMemoryError 예외

2.4.1 자바 힙 오버플로우

  • 힙덤프 로 메모리 분석하여 메모리 누수를 확인한다.
  • 메모리 누수가 아니라면
    • 수명주기가 너무 긴 객체
    • 공간낭비가 심한 데이터 구조인지
    • Xmx, Xms 값이 적절한지

      2.4.2 가상머신 스택과 네이티브 메서드 스택 오버플로우

  • 스레드가 요구하는 스택의 깊이 > VM이 허용하는 최대 깊이 : StackOverFlow
  • VM이 스택 메모리를 동적으로 확장하는 기능을 지원하지만, 가용 메모리가 부족한 경우 : OutOfMemoryError
    • HotspotVM 은 확장을 지원하지 않는다.
    • 스레드에 할당가능한 메모리 예
      • 32bit window 의 프로세스당 최대 메모리는 2GB
      • 가상 머신 스택 + 네이티브 메서드 스택 메모리 = 프로세스당 최대 매모리 - Xmx 최대 힙메모리 - XX:metaSpace 최대 메서드영역 메모리
      • 더 많은 스레드를 할당하고싶다면, 최대 힙크기를 줄이거나, 스레드당 스택크기를 줄이는 방법이 있다.

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

  • String::intern()
    • 문자열 상수풀에 같은 값이 있다면 참조를 반환, 없다면 상수풀에 추가 후 반환.
      • 런타임에 상수풀에 새로운값을 추가할 수 있는 방식.
  • ~JVM6 : 상수풀이 영구세대에 있었음 (XX:PermSize)
  • JVM7~ : 상수풀이 힙메모리에 있음.
  • JVM8~ : 영구세대가 사라지고, 메타스페이스가 대체.

Image

출처 : https://www.programmersought.com/article/4905216600/

  • JVM별 실험 1
          while(true){
              collection.add(String.valueOf(i++).intern());
          }
    
    • ~JVM6 : OOM : PermGen Size 에러 반환
    • JVM7~ : OOM : Java heap Size 에러 반환
  • JVM별 실험 2
          String str1 = new StringBuilder("aa").toString()
          System.out.println(str1.intern() == str1)
    
    • ~JVM6 : false, 다른 영역에 있으므로.
    • JVM7~ : true, 같은 영역에 있으므로.

2.4.4 : 네이티브 다이렉트 메모리 오버플로우

  • default : XX:MaxDirectMemorySize = Xmx value
  • 다이렉트 메모리에서 발생한 오버플로우는 힙덤프 파일에서는 이상한 점을 찾을 수 없다.