[Java] JVM과 GC

sion·2024년 11월 28일
0

자바 스터디

목록 보기
6/7

JVM

JVM은 자바 프로그램을 실행하기 위한 가상 머신입니다. javac에 의해 변환된 바이트코드를 읽어 실행하는 일종의 머신입니다.
그러면 어떻게 읽고, 어떻게 실행하는 건지 좀더 자세히 내부 동작에 대해 알아보겠습니다.

JVM 구조

Class Loader - Execution Engine - Runtime Data Area

JVM은 크게 세가지 영역으로 나뉩니다.

Class Loader

바이트코드를(.class)를 JVM의 메모리 영역인 Runtime Data Area에 배치시키는 작업을 수행합니다. 클래스 로더는 다음과 같이 3단계로 구성됩니다.

  1. Loading
    바이트코드를 JVM 메모리에 "동적"으로 로드
  2. Linking: 검증
    • Verifying: 유효한 클래스파일인지 검사. 중간에 조작된 경우 에러 발생.
    • Preparing: 기본값을 위한 메모리 할당
    • Resolving: 참조변수가 Heap에 저장된 인스턴스를 가리키도록 연결
  3. Initializing: 클래스 변수 초기화

Execution Engine

Execution Engine은 클래스 로더를 통해 Runtime Data Area에 배치된 바이트 코드를 명령어 단위로 읽어서 동적으로 실행합니다. 중간레벨인 바이트코드를 기계어로 변경해주는 역할을 합니다.

이때, 인터프리터와 JIT 컴파일러 두 가지 방식을 혼합하여 바이트 코드를 실행합니다.

  1. Interpreter
    명령어를 하나씩 읽어서 해석하여 실행합니다. JVM에서는 기본적으로 인터프리터 방식으로 동작합니다. 컴파일러에 비해 한줄한줄 읽는 것이다 보니 속도가 느립니다.
  2. JIT Compiler (Just-In-Time)
    인터프리터의 단점을 보완하기 위해 반복되는 코드를 컴파일하여 Native Code로 캐싱해두었다가, 바로 실행됩니다. (코드캐시라는 영역에 저장된다.)

[참고] 코드캐시 영역이 꽉 찬다면?
-> 코드캐시 영역은 JVM이 시작될 때 설정된 고정 크기이므로 확장이 불가능합니다. 따라서 코드캐시가 꽉 차면 더이상 JIT 컴파일은 이루어지지 않고 새로운 코드는 모두 인터프리터 모드로만 동작합니다.

Runtime Data Area

JVM의 메모리 영역을 Runtime Data Area라고 합니다. Method, Heap, Stack, PC Register, Native Method Stack 으로 구성되어 있습니다.

  • Method(Static) Area: 클래스에 대한 메타데이터 영역으로, 모든 스레드가 공유합니다.
  • Heap Area: new 키워드를 통해 동적으로 생성된 인스턴스 객체가 저장되는 영역으로, 모든 스레드가 공유합니다. GC의 대상 영역이기도 합니다.
    • 새로운 인스턴스를 만들 Heap 영역이 부족해지면, OutOfMemoryError 발생
  • Stack Area: 지역변수, 매개변수, 리턴값이 저장되는 여역으로, 스레드마다 고유한 영역을 가집니다.
    • 메소드가 호출되면, Stack 영역에 Stack Frame을 push하고 Frame 내에 데이터들을 저장한다. 메소드 종료시 Frame이 pop되면서 메모리를 바로 반납한다.
    • 메소드가 매우 많이(무한정) 호출되어 스택 프레임을 더이상 추가할 수 없다면 StackOverFlowError 발생
    • 스레드 생성을 위한 스택 메모리가 부족하다면 OutOfMemoryError 발생
  • PC Register: 현재 실행중인 명령어 주소를 기록하는 영역으로, 스레드마다 고유한 영역을 가집니다.
  • Native Method Stack: Native 코드를 실행하기 위한 영역으로, 스레드마다 고유한 영역을 가집니다.
    • Native 언어(C/C++) 메소드를 실행하면 이곳에 저장됩니다.
    • JNI(Java Native Interface) 규약을 통해 호출이 가능합니다.

참고: https://velog.io/@impala/JAVA-JVM-Runtime-Data-Area

GC

Runtime Data Area 영역에서 Stack, PC Register, Native Method Stack은 생명주기가 비교적 짧기 때문에 메모리 걱정을 덜 해도 됩니다.

  • 스레드와 함께 생성되고 소멸된다.
  • 특히, Stack은 메소드 호출마다 스택 프레임이 생성되고, 종료 시 즉시 제거된다.

JVM 메모리에서 Heap 영역에 저장된 인스턴스는 Stack 영역의 참조변수에 의해 가리키고 있습니다. 참조되지 않는 인스턴스는 GC를 통해 메모리에서 제거됩니다.

JVM의 Heap 영역은 아래 두 가지를 전제로 설계되었습니다.

  1. 대부분의 객체는 금방 Unreachable 해진다.
  2. 오래된 객체가 새로운 객체를 참조하는 일은 매우 드물다.

즉, 객체는 대부분 일회성이고 메모리에 오래 남아있는 경우는 적다는 것입니다. 이런 전제를 통해 Heap 영역을 Young/Old 으로 나누었습니다.

  • Young Generation: new를 통해 생성된 객체가 저장되는 곳으로, 여기서 발생한 GC를 Minor GC라고 합니다.
    • Eden 영역이 가득차면 Minor GC가 발생하고, reachable한 객체는 Survivor 영역으로 이동합니다. (Mark)
    • unreachable한 객체는 메모리에서 해제합니다. (sweep)
  • Old Generation: Minor GC에서도 여러 번 살아남은 객체가 저장되는 곳으로, 여기서 발생한 GC를 Full GC(Major GC)라고 합니다. Young 영역보다 공간이 크기 때문에 Full GC는 Minor GC보다 오래 걸립니다. 이때, 애플리케이션이 전체로 멈추기 때문에 stop-the-world라고 불립니다.

GC 모니터링

GC 모니터링이란 JVM이 어떻게 GC를 수행하고 있는지 알아내는 과정입니다. JVM이 효율적으로 GC를 수행하는지 파악하고, stop-the-world가 언제 일어나고 얼마동안 지속되는지 등의 정보를 알 수 있습니다. GC 모니터링에 문제가 있다면, GC 알고리즘을 변경하거나 GC 튜닝을 진행할 수 있습니다.

OutOfMemoryError 가 발생했다면?

JVM 프로그램이 OutOfMemoryError 에러가 발생했다면, 원인을 파악하기 위해 Heap Dump를 분석해야 합니다. -XX:+HeapDumpOnOutOfMemoryError JVM 옵션을 설정하면 힙덤프 .hprof 파일을 자동으로 생성됩니다.
실시간으로 스냅샷을 생성할 수도 있습니다.

  1. 프로세스 id 확인
    • ps -ef | grep java 로 확인해도 되지만,
    • jps 간편한 명령어도 있다.
  2. 힙덤프 파일 생성
    • pid를 확인했다면, 파일을 생성하자.
    • jmap -dump:format=b,file=/my/path/test-heapdump.hprof ${pid}

힙덤프 툴을 이용해 분석합니다. (대표적으로 Memory Analyzer, Visual Vm 이 있다.)
메모리를 가장 많이 점유하는 객체를 파악할 수 있고 의심되는 객체 등을 확인할 수 있습니다.

OOM을 예방하는 방법

무분별하게 객체를 생성하거나, reachable 상태를 유지할 경우가 대부분입니다.
따라서 코드레벨에서 주의하고, 적절한 메모리 사이즈를 지정해줍니다.

코드레벨

  1. 객체 선언 범위를 최소화하자
    예를 들어, 1000줄 메소드 초반에 객체 선언을 하고 끝까지 살아있다면 필요한 상태보다 훨씬 더 오래 살아남을 것이다. 선언 범위 크기를 최소화하자.
  2. FileInputStream을 사용해라
    내부 버퍼를 사용해서 일정한 크기만큼 데이터를 조회하여 위험을 줄인다.
  3. Stream API를 사용해라
    지연 연산으로 필요할 때만 데이터를 처리하여 메모리 사용을 최소화한다.

애플리케이션 메모리

애플리케이션에 맞는 적절한 메모리 사이즈를 지정해주는 것이 좋다.
-Xmx: 최대 메모리 사이즈
-Xms: 초기 메모리 사이즈

Java의 버전에 따른 디폴트 GC

  1. Java8: Parallel GC
  2. Java11: G1 GC
  3. Java17: G1 GC
  4. Java21: G1 GC

G1 GC (Garbage-First GC)

G1 GC는 대용량 메모리가 있는 멀티 프로세서 시스템을 위해 제작되었습니다. Stop-the-world를 최소화하도록 설계되었습니다.

Heap 영역을 동일한 크기(기본 2MB)의 Region으로 나누어 논리적으로 구분합니다. 각 Region은 Eden, Survivor, Old 등의 역할을 동적으로 부여받습니다. 새로운 영역도 존재합니다.

  • Humongous: Region 크기의 50% 초과하는 큰 객체를 저장하기 위한 공간
  • Available/Unused: 아직 사용되지 않은 Region

이전 GC와 마찬가지로 Eden -> Survivor -> Old 이동의 생명주기를 가집니다.
가비지 비중이 높은 Region을 우선적으로 수집하기 때문에 "Garbage First"라고 합니다.

참고

0개의 댓글

관련 채용 정보