hongcheol / CS-study

cs지식을 정리하는 공간
MIT License
248 stars 30 forks source link

[면접 질문] JVM, 가비지 컬렉션 #138

Open nayoon-kim opened 3 years ago

nayoon-kim commented 3 years ago

JVM에 대해 설명하고 Java 애플리케이션이 실행되는 과정을 설명하세요.

(질문 받은 곳 - Spring Boot 기반 결제 서비스 회사)

JVM

JRE의 구성 요소 중 하나로 자바 바이트코드를 해석하고 실행한다.

JVM의 역할은 자바 애플리케이션을 클래스 로더를 통해 읽어 들여서 자바 API와 함께 실행하는 것이다.

특징

바이트 오더

어떤 값을 메모리에 저장할 때의 순서

방식에 따라 빅 엔디안과 리틀 엔디안으로 나눠지게 된다.

(컴퓨터에 저장되는 데이터의 최소 단위 = 1 바이트(Byte), 데이터 저장 순서 (Order))

JVM 메모리 구조

image

- Class Loader

런타임 시에 동적으로 처음 참조하는 클래스를 로드하고 링크한다.

- Execution Engine

Class Loader를 통해 JVM 내의 Runtime Data Area에 배치된 바이트 코드들을 명령어 단위로 읽어서 실행한다.

자바 바이트코드는 기계가 바로 수행할 수 있는 언어이기보다는 인간이 보기 편한 형태로 기술되었다. 그렇기 때문에 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 다음 두 가지가 있다.

1. 인터프리터

바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다.

하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다.(흔히 이야기하는 인터프리터 언어의 단점을 그대로 가진다.)

즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.

2. JIT(Just-In-Time) 컴파일러

인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다.

인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일해서 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다.

네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.

그렇다면 인터프리터 대신 JIT 컴파일러만 사용하면 안되나?

JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리기 때문에, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다.

따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

- Garbage Collector

힙 메모리에 생성된 객체들 중에서 참조되지 않은 객체들을 탐색 후 제거하는 역할을 한다.

GC 실행 시점을 알 수 없다.

System.gc()를 호출해서 실행을 유도할 수는 있으나 바로 수행된다는 보장은 없다. 또한, 시스템의 성능에 큰 영향을 미치기 때문에 호출을 지양해야 한다.

JVM에 따라 다양하게 구현, 실행되며 성능에 영향을 주기 때문에 Java version에 따라 더 좋은 성능의 Garbage Collection 기법을 사용한다.

[참고]

가비지 컬렉션 알고리즘 기법에 대해 설명하세요.

(질문 받은 곳 - Spring Boot 기반 결제 서비스 회사)

가비지 컬렉션(Garbage Collection, GC)

참조되지 않는 객체 = Garbage

Garbage를 청소함으로써 메모리 누수를 막고 메모리를 효율적으로 관리한다.

Garbage Collector는 Garbage Collection을 수행합니다.

힙 구조

image

가비지 컬렉션의 동작 방식

Young 영역과 Old 영역은 서로 다른 메모리 구조로 되어 있기 때문에 세부적인 동작 방식은 다르지만 가비지 컬렉션이 실행된다고 하면 공통적으로 2가지 단계를 따른다.

  1. Stop The World
  2. Mark and Sweep

Stop The World

가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업이다.

GC가 실행될 때는 GC를 실행하는 스레드를 제외한 모든 스레드들의 작업이 중단되고, GC가 완료되면 작업이 재개된다.

모든 스레드의 작업이 중단되면 애플리케이션이 멈추기 때문에 성능 문제가 있다.

Mark and Sweep

Mark: 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업 Sweep: Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업

Stop The World를 통해 모든 작업을 중단시키면, GC는 스택의 모든 변수 또는 Reachable 객체를 스캔하면서 어떤 객체를 참고하고 있는지 탐색한다.

그리고 사용되고 있는 메모리를 식별하는데, 이러한 과정을 Mark라고 한다.

이후에 Mark가 되지 않은 객체들을 메모리에서 제거하는데, 이 과정을 Sweep이라고 한다.

Minor GC가 일어나는 과정

  1. 새롭게 생성된 객체는 Eden 영역에 할당된다.

  2. 객체가 계속 새로 생성되어 Eden 영역이 꽉 차게 되면 Minor GC가 실행된다. 참조되지 않는 객체의 메모리 할당을 해제하고 참조되고 있는 객체는 Survivor 영역으로 이동한다.

  3. 1~2번 과정이 반복되다가 Survivor 영역이 꽉차게 되면 Survivor 영역의 살아남은 객체를 다른 Survivor 영역으로 이동시킨다. (1개의 Survivor 영역은 반드시 빈 상태여야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 정상적인 상황이 아니다.)

  4. 이러한 과정을 반복해서 계속 살아남은 객체는 Old 영역으로 이동(Promotion)한다.

Object Aging

Survivor 영역에서 다른 Survivor 영역으로 이동할 때마다 객체의 살아남은 횟수를 나타내는 age의 값을 증가시킨다. Object Header에 기록하며 age를 보고 Minor GC 때 Old로 이동(Promotion)시킬지를 결정한다.

Promotion

age 값이 특정 이상이 되면 Old Generation으로 옮겨지는 것을 말한다.

Major GC가 일어나는 과정

  1. Promotion 작업이 계속되다보면 Old Generation이 가득 차게 되면서 Major GC가 발생하게 된다.

  2. 참조되지 않는 객체는 메모리 할당을 해제한다.

Minor GC와 Major GC의 차이점

Young Generation은 Old Generation보다 크기가 작기 때문에 Minor GC가 일어나는 속도가 빠르다. (대략 0.5초에서 1초) 그렇기 때문에 애플리케이션에 큰 영향을 주지 않는다.

하지만 Old Generation에서의 Major GC는 Minor GC에 비해 10배 이상의 시간이 소요된다.

[참고]