Java 메모리 구조와 컴파일, GC

유알·2024년 8월 28일

기술 면접 시 제대로 대답하지 못한 내용을 공부하며 정리한다.

Java의 메모리 구조와 GC에 대해 자세히 공부한다.

Java의 메모리 구조

컴퓨터 구조시간에 배운 메모리 구조는

  • code : 프로그램 코드
  • data : 전역변수, 정적 변수
  • heap : 동적할당
  • stack : 지역변수, 매개변수, 포인터

이렇게 구성되어 있다.

Java의 경우 JVM 상에서 실행이 되는데 이로 인해 JVM 내의 메모리 구조가 존재한다.

Java 메모리 구조

Method Area

  • 전역, static 뿐만 아니라, 클래스, 인터페이스, 메소드 등의 바이트 코드를 보관
  • 모든 쓰레드가 공유함
  • OS 메모리의 code, data 영역을 합친 느낌

PC Register

  • 쓰레드 마다 존재
  • Program Counter 관련 정보 저장
  • 이 또한 OS 메모리 구조와 다른 점

Native method stack

  • 자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역

Heap, Stack

  • OS 구조와 유사
  • Heap은 new 키워드로 동적 할당하는 메모리 영역
  • Heap은 GC의 대상이 된다.
  • Stack의 경우 메서드 호출 마다 스택 프레임이 생성된다.
  • Stack의 경우 메서드 수행이 끝나면, 프레임 별로 삭제

Java 컴파일, 실행 과정

  1. 개발자가 자바 소스 코드(.java) 작성
  2. 자바 컴파일러가 자바 소스파일 컴파일
    • java -> AST(Abstract Syntax Tree, 추상구문트리)
    • Annotation Processor 실행 (.java 생성 가능)
    • AST를 기반으로 바이트 코드(.class) 생성
  3. Class Loader가 바이트 코드 로딩
    • 로딩, 검증, 메모리 할당(메서드 영역), 클래스 변수 초기화(static)
  4. Execution Engine은 바이트 코드를 하나씩 실행
    • 인터프리터 : 바이트 코드 명렬어를 하나씩 읽어서 실행
    • JIT 컴파일러 : 인터프리터 단점을 보완하기 위해 도입, 실행횟수가 "기준"을 넘으면, 미리 컴파일 해놓고 이를 활용

Java GC

Java와 같은 Managed 언어의 경우 가비지 컬렉션(GC)가 존재하여 메모리를 자동으로 관리해준다.

가비지 컬렉션 대상

가비지 컬렉션은 어떤 Object를 Garbage로 판단해서 지울까?

바로 Reachable 여부이다. 다른 말로 말하면, Reference(참조)를 체크한다.

다른 영역에서 해당 객체를 참조하고 있다면 Reachable, 참조하고 있지 않다면 Unreachable로 판단한다.

참조하고 있지 않은 Object는 GC의 대상, 즉 Gabage로 인식한다.

Mark And Sweep

GC가 수행되는 방식이다.

  • Mark : Root Space부터 그래프 순회를 통해 연결된 객체를 찾아내어 어떤 객체를 참조하고 있는지 마킹
  • Sweep : Unreachable 객체 제거
  • Compact : 파편화 제거

Java Heap 영역

Java의 Heap 영역의 경우, 크게 Young 영역과 Old 영역으로 나뉜다.
객체가 처음 생성되면 Young 영역의 Eden 부분에 배정되고, '특정 조건'이 되면, Old 영역으로 배정된다.(추후 기술)

GC 수행 과정

Minor GC

Young 영역에서 발생하는 GC 를 Minor GC 라고 한다.
1. 객체 생성시 Eden 영역에 배정
2. Eden 꽉차면, Minor GC 실행
3. 살아 남은 객체들은 한쪽 Survivor 영역으로 이동
4. 다시 Eden 꽉차면, Survivor영역을 포함해서 GC 실행
5. 여기서 살아남은 객체들을 반대쪽 Survivor 영역에 저장
6. 4 ~ 5 반복

포인트는 한쪽 Survivor 영역은 항상 비어 있다는 것이다.

Major GC

  • Minor GC에서 객체가 Survivor 영역을 옮겨 다닐 때마다, 나이가 1씩 증가한다
  • {설정한 나이}를 넘어가게 되면, Old 영역에 배정된다.
  • Old 영역이 꽉 차게 되면, Major GC가 실행된다.
  • Major GC는 전체 객체를 대상으로 GC를 실행하므로, 오래걸리고 어플리케이션 성능에 치명적이다.

왜 두가지 GC로 나누었는가?

객체의 특징 때문이다. 객체들은 대부분 빠른시간 내에 생성되었다가 소멸되며, 일부 객체만 아주 오랜시간 살아남는다.

Old영역에 들어갔다는 말은 오랫동안 GC를 실행시키지 않겠다는 의미이며, Minor GC에서는 Old 영역을 대상에서 제외하여 성능을 높인다.

GC의 종류

  • Serial GC : CPU 성능 낮을 때 사용, Major/Minor GC에서 하나의 쓰레드만 사용
  • Parallel GC : Minor GC에서 코어수만큼 쓰레드 사용
  • Parallel Old GC : Major/Minor GC에서 코어 수만큼 쓰레드 사용
  • Concurrent Mark And Sweep Collector
    - Major GC 시 어플리케이션 쓰레드를 멈추지 않고 동시에 실행
    • 어플리케이션이 절대 멈추면 안될 때 실행
  • G1 GC
    - Young, Old 영역을 나누지 않음
    • 동일한 크기의 Heap 공간으로 나눔(페이징 처럼)
    • 가장 liveness가 적은 곳을 GC

STW를 줄이기 위한 시도

CMS (Concurrent Mark and Sweep)

이는 기존 old, young 영역 방식을 유지하면서도 STW를 줄이기 위해 노력했다.

  • 초기 마킹(GC루트로 부터 바로 연결된 애들만 마크, STW발생)
  • 순회 탐색(애플리케이션 쓰레드와 함께)
  • 재 지정(그동안 바뀐 참조 반영, STW)
  • 지우기(STW)

이러한 방법을 통해 어플리케이션 쓰레드가 정지하는 시간을 최소화 했다.

G1 GC

여기서 가장 큰 변화점은 old, young 영역으로 나누지 않고 리전이라는 단위로 일정하게 나눠두는 것이다. (물론 거대 리전이라는게 존재한다)

그래서 liveness가 가장 적은(효율이 좋은) 부분을 gc 대상으로 판단한다.

물론 eden, survivor, old 영역등은 여전히 존재하며, 하나의 리전이 하나의 상태를 갖는다.

ZGC

이 알고리즘의 목적은 매우 큰 메모리 크기에서도 아주 작은 GC타임을 유지하는 것이다.
따라서 64bit에서만 사용가능하다.

Colored pointers 와 Load barriers 라는 특이한 방법을 쓰고, 리전의 경우도 SMALL, MEDIUM, LARGE(제한없음) 이라는 세가지 타입이 존재한다.

내용이 어려워 완전히 이해는 못하였지만, 메모리 주소 공간의 특정 비트를 특정한 의미로 쓰고(Colored Pointer) 이를 활용해 메모리 컴팩션을 STW없이 진행하는게 큰 특징 인것 같다.

GC 튜닝

자세한 내용은 이 글의 범위를 벗어나므로, 생략한다.

  • Java의 실행 인자로 JVM 을 튜닝할 수 있으며
  • 대표적으로 Young 영역, Old 영역 크기
  • 전체 Heap 사이즈
  • 몇살이 넘으면 Old로 이동시킬 것인지
  • 와 같은 옵션을 통해 GC의 실행 빈도를 튜닝하여 어플리케이션 속도를 끌어 올릴 수 있다.
profile
더 좋은 구조를 고민하는 개발자 입니다

0개의 댓글