1. JVM
JVM은 자바 바이트코드를 실행할 수 있는 프로그램이다.
플랫폼에 독립적이므로 운영체제에 무관하게 프로그램을 실행할 수 있다는 점이 특징이다.
2. 메모리 구조
위와 같이, JVM메모리는 크게 Heap과 Thread stack으로 이루어진다. JVM메모리 사용를 표현한 자료를 첨부한다.
2.1. Heap메모리
JVM은 new와 같은 키워드를 통해 오브젝트를 생성하면 이를 heap에 할당한다. 때문에 가장 큰 메모리 영역이며 일반적으로 가비지컬렉션이라고 부르는 작업이 여기서 일어난다.
JVM은 메모리를 재사용하기 위해 가비지컬렉션을 실행하는데, 이런 작업에도 불구하고 메모리가 부족하면 흔히 말하는 Out of Memory Error가 발생하게 된다.
이 때 사용하는 heap메모리는 java프로그램 기동 시, Xmx, Xms변수를 통해 제한 가능하다.
java -jar -Xms2m -Xmx64m //힙을 2MB ~ 64MB까지만 사용함
2.1.1. Young영역
해당 영역의 객체가 사라질 때 Minor GC가 발생했다고 부른다.
또한, 해당 영역은 또다시 두가지 영역으로 나뉜다.
- eden: 객체 처음 생성됐을 때 할당된다. eden이 일정 비율 이상 차면 설정에 따라 가비지 컬렉션이 발생한다.
- survivor: eden에서 살아남은 객체들은 해당 영역으로 이동한다. 이 때 survivor영역의 상태를 검사해 다른 한쪽으로 이동시키므로 둘 중 하나는 항상 비어있게 된다. (S0이나 S1에만 데이터가 있어야 정상)
2.1.2. Old영역
Minor gc가 계속 일어났음에도 살아남은 객체들이 저장된다.
대부분 Young영역보다 크게 할당하며, 크기가 큰 만큼 GC는 적게 발생한다. 해당 영역의 GC를 full gc(major gc)라고 부르며, 영역이 크기 때문에 시간은 더 오래 소요된다.
2.1.3. 가비지컬렉터 통계 확인
$ jstat -gcutil 50267
S0 S1 E O M CCS YGC YGCT FGC FGCT GCT
0.00 75.33 24.31 22.04 98.02 95.57 169 1.732 4 0.777 2.509
java설치 시 내장되어있는 jstat명령어를 통해 가비지컬렉터의 상태를 확인할 수 있다.
gcutil옵션을 통해 GC통계 개요를 확인할 수 있는데, 상위의 설명처럼 Survivor0(S0)과 Survivor1(S1) 중 하나에만 메모리가 75.33% 쌓여있고, Eden(E)에는 24.31%가 쌓여있으며, Old영역(O)은 22.04% 쌓여있다는 것을 확인할 수 있다.
또한 Young영역의 GC(YGC)는 169번 수행되었고, Old영역의 Full GC(FGC)는 4번 수행되었으며, YGC 소요시간(YGCT)는 1.732초, FGC 소요시간(FGCT)는 0.777초 수행되었다.
GCT는 YGC와 FGC의 총합 시간을 의미한다.
2.2. 스레드 스택
스레드 스택에는 객체가 저장되는 힙 영역과 달리, 프로그램에서 실행되는 메소드나 primitive변수들이 저장된다. 또한, heap영역의 어떤 주소를 참조하는지를 저장한다.
메소드 호출이 끝났다거나, for while 내부에서만 사용되던 변수가 해당 스코프가 종료된 경우라면 해당 메모리는 제거된다.
2.3. Metaspace(=PermGen)영역
JVM에서 실행되는데 필요한 클래스정의, String상수. 클래스로더는 class파일을 가져와 메모리에 저장
JDK8 이전에는 Metaspace가 아니라 Permanent영역이라고 불렀는데, 이전에는 JVM이 크기를 강제했다가 OS가 자동으로 크기를 조절하도록 하면서 메모리 사용 성능을 증대시켰다고 한다.
3. 가비지 컬렉션
위에서 언급한 것과 같이, 가비지 컬렉션은 기존의 메모리를 재사용하는 메커니즘이다.
더 이상 참조되지 않는 메모리를 제거하는 것인데, C나 C++는 수동으로 메모리를 관리했으나, java가 이를 자동으로 관리할 수 있었던 것은 이러한 가비지컬렉션이 이루어졌기 때문이다.
3.1. 작동과정
- mark-and-sweep: 가비지 컬렉션은 실행 중인 코드에서 참조하는 객체를 확인해 live로 표시한다.
live가 아닌 메모리 위치에 메모리를 재할당할 수 있도록 하는 것이다.
Mark : 사용 중인 메모리와 그렇지 않은 메모리를 식별
Sweep : mark단계에서 식별된 오브젝트를 삭제하는 단계 - stop-the-world: GC가 수행되면, 순간적으로 JVM의 모든 스레드가 정지된다. Minor gc는 속도가 빨라 크게 영향을 미치지 않지만, major gc는 시간이 오래 걸린다. 따라서 gc횟수가 많다면 튜닝이 필요하다.
3.2. 장점
- 수동으로 메모리 할당/해제하지 않아도 됨
- 자동 memory leak관리
3.3. 단점
- jvm이 object참조의 생성, 삭제를 트래킹하므로, 기존 어플리케이션보다 더 많은 서버 리소스를 사용하게 됨
- 필요하지 않은 object를 해제하는데 사용되는 cpu시간을 제어할 수 없음
- 일부 GC구현은 어플리케이션을 예상치 못하게 중단시킬 수 있음
- 적절한 수동 메모리 할당/해제보단 효율적이지 않음
3.4. 가비지컬렉터 종류
3.4.1. Serial Garbage Collector
- 단일 스레드에서 작동하는 가장 간단한 구현
- 실행 시 전체 어플리케이션의 스레드를 프리징하므로, 멀티 스레드 환경에서는 좋은 방법이 아님
- 적은 중단 시간을 필요로 하지 않는 클라이언트 스타일의 머신에서 선택됨
- mark-sweep-compact: old영역의 live한 객체를 mark후, heap의 앞부분부터 확인해 살아있는 것만 남김(sweep), 그리고 각 객체들이 연속되게 쌓이도록 힙의 앞부분부터 채워 객체가 존재하는 부분과 없는 부분으로 나눔(compaction)
java -XX:+UseSerialGC -jar Application.java
3.4.2. Paralled Garbage Collector
- JVM의 기본 GC. 다중 스레드를 사용해 힙 메모리를 관리
- GC수행하는 동안 다른 어플리케이션 스레드도 프리징됨(stop the world)
- 최대 가비지 컬렉션 스레드와 일시 중지시간, 처리량 및 힙 크기 지정 가능
- Serial garbage collector와 알고리즘은 동일
java -XX:+UseParallelGC -jar Application.java
3.4.3. CMS Garbage Collector
- Concurrent mark sweep이라는 의미.
- 다수의 가비지 컬렉션 스레드를 사용. 따라서 다른 gc방식보다 메모리와 cpu를 많이 사용
- 짧은 가비지 컬렉션 중단을 위해 디자인되었고, 어플리케이션을 사용하는 동안 가비지 컬렉터와 프로세서 리소스가 공유됨
- 응답이 느리지만 가비지 컬렉션을 위해 응답을 중지하지 않음
- GC가 concurrent하게 동작하므로, System.gc와 같은 명시적인 가비지 컬렉션 요청을 하는 경우 Concurrent Mode Failure / Interruption발생
- Java9부터 더이상 사용X, Java14부터 미지원
- compaction단계가 기본적으로 제공되지 않아, 자주 실행해야 하는 경우 다른 gc방식보다 stop-the-world시간이 길 수 있음
java -XX:+UseParNewGC -jar Application.java
3.4.4. G1 Garbage Collector
- 대용량 메모리 공간이 있는 다중 프로세서 시스템에서 실행되는 어플리케이션을 위해 설계됨
- Java7부터 지원하며, 효율성이 높아 CMS 가비지 컬렉터를 대체
- 힙을 각 가상메모리의 인접한 범위로 구성된 동일한 사이즈의 힙 집합으로 분할.
- 가비지 컬렉션이 수행될 때, G1은 동시에 사용 중인 메모리를 marking함
- marking이 완료되면 g1은 빈 공간을 인식하고, 빈 공간을 생성함(sweeping)
java -XX:+UseG1GC -jar Application.java
3.4.5. Z Garbage Collector
- java11에서는 linux용 실험용으로, java14에서 읜도우 및 맥OS에서 동작할 수 있도록 도입
- java15부터 production상태
- 스레드를 10ms이상 중지하지 않음
- G1과 유사하게 힙을 분할하여 처리. 8mb~16tb크기의 힙을 처리함
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC Application.java --java15이전
java -XX:+UseZGC Application.java --java15이후
3.5. GC튜닝의 목적
GC튜닝은 Old영역으로 넘어가는 객체의 수를 최소화하는데 목적이 있다. full gc가 minor gc보다 오래걸리므로, 객체의 수를 줄여 full gc의 빈도 및 실행시간을 최소화해야하기 때문이다.
그런데 full gc의 실행시간을 줄이기 위해 old영역을 줄이면 full gc 횟수가 늘어나고, old영역을 늘리면 full gc의 횟수는 줄어들지만 실행시간이 늘어나므로 적절한 설정이 필요하다.
일반적으로 GC상황을 모니터링한 후, 수행시간이 1~3초가 넘어가면 튜닝을 진행해야 한다. 해당 모니터링의 방법에는 다양한 방법이 있을 수 있는데, -Xverbosegclog옵션을 통해 GC로그를 실시간으로 확인할 수 있다.
메모리를 충분하게 잡았는데도 OOM이 발생한다면, 힙덤프로 원인 파악을 해야하며, 24시간 데이터를 수집해 GC방식 및 메모리 크기를 변경하며 최적의 옵션을 찾아야 한다.