JVM의 구조는 클래스로더, 메모리 영역(Runtime Data Area), 실행 엔진, 가비지 컬렉션, 네이티브 메소드 인터페이스(JNI)+네이티브 메소드 라이브러리 로 구성되어있습니다.
그리고 Runtime Data Area 에는 Method Area, Heap, PC Registers, Stack, Native Method Stack 로 구성되어있습니다. (참고)
간단히,
스택과 힙
스택
컴파일 타임에 할당되는 메모리 영역입니다.
원시 타입의 데이터가 값과 함께 할당되고, Heap 영역에 생성된 Object 타입 데이터의 참조 변수(주소값)가 할당됩니다.
프로세스가 메모리에 로드 될 때 스택 사이즈가 고정되어 있어, 런타임 시에 스택 사이즈를 바꿀 수는 없습니다.
힙
런타임에 할당되는 메모리 영역입니다.
모든 자바 클래스 인스턴스, 배열이 할당되고, Heap 영역의 Object를 가리키는 참조 변수가 Stack에 할당됩니다.
힙은 JVM이 실행되면서 생성됩니다.
그리고 애플리케이션이 실행 중인동안 힙의 크기가 줄어들고 늘어납니다. (heap의 사이즈는 -Xms VM 옵션으로 설정할 수 있습니다.)
일반적으로 GC 전략에 따라 힙을 정적 크기 혹은 동적 크기로 지정할 수 있습니다.
최대 힙사이즈는 -Xmx 옵션으로 설정할 수 있고, 디폴트는 64MB입니다.
가비지 컬렉션
동적으로 할당한 메모리 영역(Heap) 중 사용하지 않는 영역을 탐지하여 해제하는 기능입니다.
가비지 컬렉션 과정
1. Marking : Stack의 모든 변수를 스캔하면서 각각 힙 영역의 어떤 Object(객체)를 참조하고 있는지 찾아서 마킹합니다.
2. Marking : 힙 영역의 Reachable Object가 참조하고 있는 객체도 찾아서 마킹합니다.
3. Normal Deletion : 마킹되지 않은(사용하지 않는) 객체를 Heap에서 제거하고 다른 개체에 할당할 여유 공간을 회수합니다.
+ Deletion with compacting : 더 좋은 성능을 위해, 사용하지 않는 개체를 제거 후, 살아남은 모든 개체들을 함께 이동시켜 새로운 객체에 대한 메모리 할당의 성능을 향상시킵니다.
위 과정을 예로 보면,
1. Stack에 있는 names라는 참조 변수를 스캔하면서 힙 영영의 참조하고 있는 List 객체를 마킹
2. 힙 영역의 List 객체가 참조하고 있는 인덱스 0,1,2의 String 객체들도 마킹
3. 마킹되지 않은 String 객체 해제
1,2번 과정이 Mark 과정이고
3번 과정을 Sweep 과정이라 하여
가비지 컬렉션 과정을 Mark and Sweep 과정이라고도 합니다.
가비지 컬렉션이 언제 일어날까?
(1) Heap 구조
Heap 구조는 New/Young Generation과 Old Generation 영역으로 이루어져있습니다.
New Generation은 새로 생성된 객체가 저장되는 영역으로, 1차 GC라고 불리는 Minor GC가 발생하는 영역입니다.
너무 커서 이 영역에 들어갈 수 없는 객체는 Old Generation으로 들어갑니다.
GC의 비용은 살아있는 객체의 수에 비례하므로, young gen의 GC는 효율적입니다.
Old Generation은 오래 살아남은 객체가 저장되는 영역으로, New 영역보다 크게 할당되기 때문에 GC가 더 적게 발생합니다. 해당 영역이 꽉 차면 Major GC가 발생합니다.
단, New/Young Generation 영역을 거치지 않고 Old 영역으로 넘어가는 객체도 존재합니다. 객체의 크기가 Survivor 영역의 크기보다 클 경우에 해당합니다.
다시 New/Young Generation은 Eden, Survivor 0, Survivor 1 영역으로 이루어져 있습니다.
Survivor 영역은 한번의 Minor GC를 경험한 객체들이 저장되는 곳이며 서로 교대하면서 살아남은 객체가 옮겨가는 대상 영역이 됩니다. 따라서 Survivor 둘 중 하나는 반드시 깨끗하게 비워져 있게 됩니다.
Survivor 0과 1 간의 우선순위는 없고 단지 영역을 구분하기 위한 명칭이라 생각하시면 됩니다.
Minor GC가 발생하는 상황은 아래와 같습니다.
- Eden 영역에서 GC가 발생 후, 살아남은 객체가 Survivor 영역 중 하나로 이동됩니다.
- Eden 영역에서 GC가 발생할 때 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓입니다.
- 하나의 Survivor 영역이 가득 차게 되면 그 중에서 살아남은 객체를 다른 Survivor 영역으로 이동합니다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 Sweep됩니다.
전체적인 흐름을 보겠습니다.
A. 객체 생성
1. 생성된 객체들이 Eden 영역에 할당됩니다.
B. Eden 영역 Full
2. Eden 영역의 메모리가 모두 할당되면, Minor GC가 발생합니다.
이때 Young Gen(Eden 영역과 Survivor) 영역에 한하여 GC가 발생하여 다른 Survivor 영역으로 옮겨지는데, 이것을 노화(aging)라 부릅니다.
2-1. GC가 발생함과 동시에 JVM은 Suspend 상태(*Stop-The-World)로 들어갑니다.
2-2. unreachable한 객체와 reachable한 객체를 구분해야하므로 Mark 작업을 시작합니다.
2-3. 살아남은 객체는 비워져있는 Survivor 영역으로 복사됩니다. (이때 age 값이 증가합니다.)
2-4. Eden 영역과 기존의 Survivor 영역은 깨끗하게 비웁니다. (Sweep)
2-5. JVM의 상태가 해제됩니다.
* 이미 한 Survivor 영역에 객체들이 할당되어있다면, 해당 영역으로 옮겨집니다.
* Stop-The-World
GC를 실행하기 위해 가비지 컬렉션을 실행하는 스레드를 제외한 모든 스레드의 작업을 중지하는 것입니다.
GC 작업을 완료한 이후에 중단한 작업을 다시 시작합니다.
3. 계속해서 Eden 영역에는 새로 생성된 객체들이 할당됩니다.
Survivor 영역을 옮겨가면서 age값이 증가한다고 언급했는데, 그렇기 때문에 같은 영역에 있는 객체들끼리도 age값의 차이가 생깁니다. (사진 속에서 빗금 색상 차이로 나타냈습니다.)
이 age값은 Old Gen 영역으로 옮겨지는 기준이 됩니다.
4. 2,3번 과정이 계속 반복됩니다.
C. Age값이 설정값 초과
9. Minor GC 사이클을 많이 반복한, 즉 Age값이 MaxTenuringThreshold라는 설정 값을 초과한 객체들은 Old Generation 영역으로 옮겨집니다. (Promotion 과정)
(또는 Survivor 영역의 메모리가 부족할 경우 미리 Young -> Old 영역으로 객체가 옮겨질 수도 있습니다.)
- Promotion의 기준이 되는 Age는 -XX:MaxTenuringThreshold 옵션으로 설정할 수 있습니다.
- Java SE 8 에서의 default 값은 15 이다. 설정 가능한 범위는 0 ~ 15
E. Old Generation 영역 Full
Old Generation 영역에서 메모리가 모두 할당되면 Old Generation 영역에서 garbage를 수집하는 과정인 MajorGC가 발생합니다.
이때 GC를 진행하는 Thread를 제외한 모든 Thread를 멈춘 상태로 GC가 진행됩니다. (*Stop-The-World)
Major GC는 시간이 더 오래 걸립니다.
GC가 동작하는 시간은 GC의 전략에 따라 다릅니다. 그래서 Stop the World를 발생시키는 횟수를 최소화하기 해서는 GC를 모니터링 하여 적절하게 튜닝하는 습관이 필요합니다.
GC 튜닝은 더 알아봐야할 것 같습니다 ㅎㅎ
결국 Minor GC가 발생하는 경우를 정리하면 다음과 같습니다.
- Eden 영역이 객체들로 꽉 찼을 때, MinorGC가 발생하면서 survivor 영역들 중 한 곳으로 살아남은 객체들이 이동된다.
- Survivor 영역의 객체들을 체크하며 다른 Survivor 영역으로 객체들을 이동시킵니다. (full인 경우)
Heap 메모리 영역의 흐름을 표현한 그림
위 과정이 반복되면서 메모리를 관리하기 때문에 저희는 (C언어를 사용할 때와 달리) 메모리와 관련된 고려를 크게 하지 않고도 객체들을 사용할 수 있습니다.
추가로 사진 속 Permanent Generation영역에 대해 간단히 정리하겠습니다.
Permanent Generation (Java8부터는 "Metaspace" 로 대체)
Permanent Generation영역은 애플리케이션에서 사용되는 클래스 및 메서드를 설명하는 메타 데이터를 포함하는 영역으로, JVM이 필요로 하는 영역입니다.
- Perm Gen 영역은 힙 영역이 아닙니다.
- Perm Gen 영역은 애플리케이션이 사용하는 클래스에 기반하여 JVM이 런타임 시에 생성합니다.
- Perm Gen에는 Java SE 라이브러리 클래스와 메소드도 포함되어 있습니다.
- 항상 최대 크기가 고정되었었지만, Metaspace로 바뀌면서 (OS가 제공하는 최대 값까지는) 자동적으로 크기가 증가합니다.
- Metaspace는 “-XX:MetaspaceSize”와 “-XX:MaxMetaspaceSize” 플래그를 이용해서 크기를 지정할 수 있습니다.
- 클래스 및 해당 메타데이터의 수명이 클래스 로더의 수명과 일치하기 때문에, 클래스로더가 살아있는 한 메타데이터는 Metaspace에 남아있고 해제되지 않습니다.
Java SE(Java Platform, Standard Edition)?
자바의 표준안입니다. 자바라는 언어가 어떠한 문법적인 구성을 가졌는지와 같은 것들을 정의하고 있습니다. 이것은 구체적인 소프트웨어가 아닌, 그 소프트웨어의 명세서(spec, specification)라고 할 수 있습니다. 이 명세서에 따라서 Java가 만들어지게 됩니다. ex) Java SE 8은 버전 8에 대한 명세서
출처: https://devbox.tistory.com/entry/실행
Java Garbage Collection Types
Serial GC (-XX:+UseSerialGC)
Mark-Sweep-Compact 알고리즘을 사용합니다.
1. Old 영역에서 Reachable한 객체를 식별(Mark)합니다.
2. 살아있는 객체만 남기고 Unreachable한 객체는 제거(Sweep)합니다.
3. 객체들을 앞부터 채워나가서 객체가 존재하는 부분과 존재하지 않는 부분을 나눕니다. 컴팩트하게!(Compaction)
Parallel GC (-XX:+UseParallelGC)
기본적인 알고리즘은 Seriacl GC와 같지만, 여러 스레드를 병렬적(Parallel)으로 이용하여 GC를 진행합니다.
Concurrent Mark Sweep (CMS) Collector (-XX:+UseConcMarkSweepGC)
G1 Garbage Collector (-XX:+UseG1GC)
참고
- https://mangkyu.tistory.com/94
- https://d2.naver.com/helloworld/1329
- https://donghyeon.dev/java/2020/03/31/%EC%9E%90%EB%B0%94%EC%9D%98-JVM-%EA%B5%AC%EC%A1%B0%EC%99%80-Garbage-Collection/
- https://www.betsol.com/blog/java-memory-management-for-java-virtual-machine-jvm/#Java_JVM_Memory_Structure
- https://www.youtube.com/watch?v=vZRmCbl871I
- https://johngrib.github.io/wiki/java-gc-eden-to-survivor/
- https://1-7171771.tistory.com/140
'언어 > Java' 카테고리의 다른 글
익명 클래스(익명 객체) (0) | 2022.03.15 |
---|---|
JVM(Java Virtual Machine)의 구조 - 메모리 (0) | 2022.03.02 |
JDK, JVM, JRE (0) | 2022.02.17 |
Exception과 Exception handler (+테스트 코드) (0) | 2022.01.05 |
참조 타입과 메모리 사용 영역 (+Java 코드 실행 과정) (0) | 2021.12.29 |