Execution engine은 JVM의 핵심 구성 요소 중 하나로, 바이트 코드를 운영체제에 맞게 해석해주는 역할을 수행합니다.
1. Interpreter
클래스 로더를 통해 메모리에 로드된 바이트 코드를 한줄 씩 실행시킵니다. 바이트코드를 하나씩 읽어와 즉시 실행하므로 여기서 속도 문제가 발생합니다. 바이트 코드 역시 기계어로 변환되어야하기 때문에 c, c++ 처럼 미리 컴파일을 통해 기계어로 변경되는 언어에 비해 속도가 느려집니다.
Q. 자바는 컴파일 언어로 알고 있어. 그런데 왜 interpreter를 갖고있지?
A. 자바는 사실 컴파일 언어와 인터프리터 언어의 특성을 모두 갖춘 언어입니다. 하지만 초기의 컴파일 과정과 바이트코드의 특성, 성능과 최적화 관련한 이유로 주로 컴파일 언어로 분류됩니다.
2. Just-In-Time compiler (JIT 컴파일러)
interpreter의 속도 문제를 해결하기 위해 디자인된 기능입니다. JIT 컴파일러는 실행 시점에서는 인터프리터와 같이 기계어 코드를 생성하면서 해당 코드가 컴파일 대상이 되면 컴파일하고 그 코드를 캐싱합니다. JIT 컴파일은 코드가 실행되는 과정에 실시간으로 일어나며(그래서 Just-In-Time이다), 전체 코드의 필요한 부분만 변환합니다. 기계어로 변환된 코드는 캐시에 저장되기 때문에 재사용 시 컴파일을 다시 할 필요가 없습니다.
- JIT 컴파일러가 컴파일하는 조건은 얼마나 자주 코드가 실행됐는가 입니다. 일정한 횟수만큼 실행되고 나면 컴파일 임계치에 도달하고 컴파일러는 컴파일하기에 충분한 정보가 쌓였다고 생각합니다.
- 임계치는 메서드가 호출된 횟수, 메서드의 루프를 빠져나오기까지 돈 횟수 두 개를 기반으로 한다. 이 두 수의 합계를 확인하고 메서드가 컴파일될 자격이 있는지 여부를 결정합니다. 자격이 있다면 메서드는 컴파일되기 위해 큐에서 대기하고 이후 메서드들은 컴파일 스레드에 의해 컴파일됩니다.
- 아주 오랫동안 돌아가는 루프 문의 카운터가 임계치를 넘어가면 해당 루프는 컴파일 대상이 됩니다. JVM은 루프를 위한 코드의 컴파일이 끝나면 루프가 다시 반복될 때는 코드를 컴파일된 코드로 교체하고 더 빠르게 실행됩니다. 이 교체 과정을 "스택 상의 교체(on-stack replacement, ORS)"라고 부릅니다.
3. Garbage collector (가비지 컬렉터)
Java의 메모리 관리 기법으로 어플리케이션이 동적으로 할당했던 메모리 영역 중 더이상 사용하지 않는 영역을 정리하는 기능입니다. GC는 heap 메모리에서 활동하며, JVM에서 GC의 스케줄링을 담당하며 개발자가 직접 관여하지 않아도 더 이상 사용하지 않는 점유된 메모리를 제거해주는 역할을 담당합니다.
가비지 컬렉션이 실행된다고 하면 다음의 2가지 공통적인 단계를 따르게 됩니다.
1. Stop The World
가비지 컬렉션을 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 작업입니다. GC가 실행될 때는 GC를 실행하는 쓰레드를 제외한 모든 쓰레드들의 작업이 중단되고, GC가 완료되면 작업이 재개됩니다. 당연히 모든 쓰레드들의 작업이 중단되면 애플리케이션이 멈추기 때문에, GC의 성능 개선을 위해 튜닝을 한다고 하면 보통 stop-the-world의 시간을 줄이는 작업을 합니다.
2. Mark and Sweep / Compact (필수는 아님)
Mark: 사용되는 메모리와 사용되지 않는 메모리를 식별하는 작업
Sweep: Mark 단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업
Compact: 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 존재하지 않는 부분으로 나누는 작업
# GC의 원리
GC가 가능한건 "약한 세대 가설" 덕분입니다.
1. 대부분의 객체는 금방 접근 불가능한 상태가 됩니다.
2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 발생합니다.
# GC 알고리즘 종류, 변화 과정
1. Serial
Serial GC의 Young 영역은 Mark Sweep이 수행됩니다. Old 영역에서는 Mark Sweep Compact 알고리즘을 수행합니다.
Serial GC는 서버의 CPU 코어가 1개일 때 사용하기 위해 개발되었기 때문에 STW가 너무 길어지는 문제가 있습니다.
2. Parallel
여러 개의 쓰레드를 통해 Parallel하게 GC를 수행합니다.
Java8까지 기본 가비지 컬렉터(Default Garbage Collector)로 사용되었지만 그럼에도 불구하고 Application이 멈추는 것은 피할 수 없었습니다.
3. Concurrent Mark Sweep (CMS)
초기 Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝냅니다. 그리고 Concurrent Mark 단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인합니다. 이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것입니다.
그 다음 Remark 단계에서는 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인하고 마지막으로 Concurrent Sweep 단계에서는 쓰레기를 정리하는 작업을 실행합니다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행됩니다.
애플리케이션이 구동중일 때 프로세서의 자원을 공유하여 이용가능해야 합니다. 하지만 이러한 CMS GC는 다른 GC 방식보다 메모리와 CPU를 더 많이 필요로 하며, Compaction 단계를 수행하지 않아 메모리 할당 및 해제를 어렵게 만들고, 메모리 사용 효율성을 저하시킬 수 있습니다.
Q. Compaction을 수행하면 무조건 좋은것같은데 왜 CMS는 수행하지 않는거지?
A. CMS GC는 빠른 응답성을 제공하기 위해 디자인되었습니다. Compact 단계가 없는 이유 중 하나는 메모리 조각화를 최소화하려는 노력입니다. Compact를 수행하려면 메모리 블록을 재배치해야 하며, 이 과정은 시간이 오래 걸릴 수 있습니다. CMS는 응답성이 중요한 시스템에서 사용되며, Compact를 생략함으로써 가비지 수집 과정을 더 빨리 완료하고 프로그램이 계속 실행될 수 있게 합니다.
4. Garbage First
G1 GC는 Eden 영역에 할당하고, Survivor로 카피하는 등의 과정을 사용하지만 물리적으로 메모리 공간을 나누지 않습니다. 대신 Region(지역)이라는 개념을 새로 도입하여 Heap을 균등하게 여러 개의 지역으로 나누고, 각 지역을 역할과 함께 논리적으로 구분하여(Eden 지역인지, Survivor 지역인지, Old 지역인지) 객체를 할당합니다.
Humonogous와 Availabe/Unused라는 2가지 역할을 추가되었습니다.
Humongous - region 크기의 50%를 초과하는 객체를 저장하는 region
Available/Unused - 아직 사용되지 않은 region
G1 GC의 기본 원리는 자바 heap의 메모리를 회수할때 최대한 살아 있는 객체가 적게 들어 있는 Region을 수집하는 것입니다. 살아 있는 객체가 적을수록 쓰레기란 의미고 따라서 이름도 쓰레기 우선 수집(Garbage First)이란 이름이 붙게되었습니다. heap 메모리 전체에 대해 compaction을 진행하지 않고 region 내에서 compaction이 이루어지므로 전체적으로 GC에 소요되는 시간이 감소합니다.
5. Z
가장 최근에 개발된 가비지 컬렉션 알고리즘이다. 관련 글들을 좀 읽어봤는데 이해를 잘 못하겠습니다... 핵심만 전달하자면 대부분의 가비지 컬렉션 단계를 병렬로 처리하고 응답성을 위해 STW 시간을 최소화하기 때문에 G1보다 훨씬 짧은 STW를 제공한다는 것입니다. 자세한 내용을 원하시면 다음 링크를 참고하세요.
https://d2.naver.com/helloworld/0128759
Q. 1~5까지의 알고리즘중 5번쨰가 가장 최신이고 좋으니까 이걸 써야하나?
A. 점점 짧은 STW를 제공하는건 사실이지만 모든 상황에 STW가 최우선이 되진 않습니다. 운영환경의 여러 이유로 CMS나 Parallel이 사용될 수 있습니다.
# References
https://velog.io/@tsi0521/Java%EB%8A%94-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EC%99%80-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%91%98-%EB%8B%A4-%EA%B0%80%EC%A7%84%EB%8B%A4
https://velog.io/@tsi0521/Java%EB%8A%94-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EC%99%80-%EC%9D%B8%ED%84%B0%ED%94%84%EB%A6%AC%ED%84%B0-%EB%91%98-%EB%8B%A4-%EA%B0%80%EC%A7%84%EB%8B%A4
https://hyeinisfree.tistory.com/26
https://joon11.tistory.com/17
https://mangkyu.tistory.com/118
https://beststar-1.tistory.com/16
https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EA%B0%80%EB%B9%84%EC%A7%80-%EC%BB%AC%EB%A0%89%EC%85%98GC-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%F0%9F%92%AF-%EC%B4%9D%EC%A0%95%EB%A6%AC
https://d2.naver.com/helloworld/1329
https://velog.io/@hanblueblue/GC-1.-G1GC
https://d2.naver.com/helloworld/0128759
https://incheol-jung.gitbook.io/docs/q-and-a/java/g1-gc-vs-z-gc
https://www.youtube.com/watch?v=jXF4qbZQnBc&ab_channel=%EC%96%B4%EB%9D%BC%EC%9A%B4%EB%93%9C%ED%97%88%EB%B8%8C%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4-AroundHubStudio
'Back-end' 카테고리의 다른 글
Java Serializable/Deserializable (1) | 2024.05.02 |
---|---|
Github actions pull_request_target (0) | 2024.01.10 |
[JVM 동작원리] 2. Runtime Data Area (0) | 2023.10.12 |
[JVM의 동작원리] 1. 클래스 로더 (0) | 2023.10.10 |
Java의 compile 과정 (0) | 2023.10.09 |