시작하기에 앞서 자바에서 일반적으로 구성되는 메모리의 구조는 다음과 같습니다.
일반적으로 메모리는 크게 stack과 heap 두 파트로 나누어집니다.
이 둘에 대해서 자세히 알아보도록 하겠습니다.
Stack
스택 프레임(혹은 Activation Record)을 저장하는 메모리 공간입니다.
스택 프레임은 메서드가 호출되기 이전의 상태를 기록하는 역할을 수행하며, 다음과 같은 내용들을 포함합니다.
- 매개 변수(Parameter) : 호출 메서드가 전달한 인자 값
- 지역변수(Local Variables) : 메서드내에서 선언한 변수
- 리턴 주소(Return Address) : 메서드실행을 마친 다음 실행할 명령문의 주소
- 리턴 값(Return Value) : 호출 메서드에게 돌려줄 값
즉 메서드 내에서 정의하는 기본 자료형( primative type) (int, double, byte, long, boolean 등)에 해당되는 지역변수와 매개변수의 데이터 값은 스택 프레임에 포함되어 Stack Area(스택 메모리 영역)에 저장됩니다.
스택 프레임은 메서드가 호출될 때 메모리에 할당(Push)되고, 종료되면 메모리에서 제거(Pop)됩니다.
위에서 확인한 것 처럼, 기본형 타입 변수의 값들은 Stack 영역에 저장됩니다.
그러나 참조형 타입 변수(쉽게 말해서 객체들은)는 참조값만 저장됩니다. 그리고 실제 객체들은 heap 영역에 저장됩니다. (즉 Stack메모리 영역에서는 heap 영역에 존재하는 객체들에 대한 참조를 가지고 있습니다.)
스택 메모리의 top 에 존재하는 하나의 스택 프레임만 활성화되며, 그 이전에 존재하는 스택 프레임은 모두 비활성화되며, 비활성화된 스택 프레임의 지역변수에는 접근이 불가능합니다.
위의 사진을 다시 본다면, stack 메모리가 여러겹 중첩되어 있다는 것을 알 수 있는데, 이는 Stack 메모리 영역이 스레드별로 할당되기 때문입니다.
따라서 스레드가 생성되고 시작될 때마다 각각의 stack 메모리를 가지게 되며, 다른 스레드의 스텍 메모리에 액세스할 수 없습니다.
Heap
Heap 메모리 영역에는 실제 객체가 저장됩니다. Heap 영역에 존재하는 객체들은 Stack 영역의 변수들에 의해 참조됩니다.
예를 들어 다음 코드를 보고 어떠한 일이 일어나는지 분석해 보도록 하겠습니다.
StringBuilder builder = new StringBuilder();
new 라는 키워드는 heap 영역에 StringBuilder라는 객체를 생성하기에 충분한 빈 공간이 있는지 확인해야 하고, Stack에 있는 builder라는 변수를 통해 참조되도록 보장합니다.
실행 중인 각 JVM 프로세스에 대해서 Stack과 달리 Heap 메모리 영역은 단 하나만 존재합니다.
따라서 실행 중인 스레드의 수에 관계없이 Heap 영역에 존재하는 메모리는 공유됩니다.
위 사진에서 힙 구조를 보기 편하게 그렸지만, 사실 힙 자체는 가비지 콜렉터 프로세스를 용이하게 하는 몇 개의 부분으로 나뉩니다.
Reference Types
메모리 구조 그림을 자세히 보면, 힙에서 객체에 대한 참조를 나타내는 화살표가 다르게 생겼다는 것을 알 수 있습니다.
자바에서는 Strong Reference, Weak Reference, Soft Reference, Phantom Reference와 같은 다양한 참조 유형이 있기 때문입니다.
참조되는 Heap의 객체들은 가비지 컬렉팅의 기준에 따라 여러 참조 타입으로 분류됩니다.
1. Strong Reference
우리에게 가장 익숙한 참조 유형입니다. 위의 StringBuilder 예제에서, 우리는 힙 영역에 존재하는 객체에 대한 Strong Reference(강력한 참조)를 가지고 있습니다.
그리고 힙 영역에 존재하는 객체들은 이러한 강력한 참조가 있는 경우 혹은 강력함 참조와 연결되어 있는 경우, 가비지 컬렉터에 의해 제거되지 않습니다.
2. Weak Reference
힙 영역에 존재하는 객체에 대한 Weak Reference(약한 참조)는 가비지 컬렉터가 수행된다면 이에 의해 제거될 수 있습니다.(무조건 제거되는 것이 아니라, 참조되는 객체가 null이 되고, 해당 객체를 가리키는 참조가 Weak Reference뿐인 경우)
java.lang 패키지의 WeakReference 클래스를 사용하면 약한 참조로 만들 수 있습니다.
다음은 사용 예시입니다.
Integer prime = 1;
WeakReference<Integer> weak = new WeakReference<Integer>(prime);
3. Soft Reference
Soft Reference(부드러운 참조라 해야할까요...?)는 응용 프로그램의 메모리가 부족한 경우에만 가비지 콜렉터에 의해 제거되므로 메모리에 무척 민감한 상황에서 사용됩니다.
따라서 일부 (메모리)공간을 확보할 필요가 없는 한, 가비지 컬렉터는 Soft Reference 유형의 객체를 제거하지 않습니다.
자바는 모든 Sofr Reference 유형의 객체가 OutOfMemoryError를 발생시키기 전에 제거되도록 보장합니다.
Soft Reference는 다음과 같이 생성됩니다.
SoftReference<StringBuilder> reference = new SoftReference<>(new StringBuilder());
4. Phantom Reference
유령 참조는 객체가 더이상 살아있지 않다는 것을 확실히 알고 있기 때문에, 사후 정리 작업을 예약하는 데 사용됩니다.
유령 참조의 get 메서드는 항상 null을 반환하기 때문에, 참조 Queue에서만 사용됩니다.
즉 요약하자면 사용이 아닌, 올바르게 삭제하고, 삭제 이후의 작업을 조작하기 위해서 사용되는 것입니다.
자바에는 finalize()라는 메서드가 존재하며, 이 메서드는 가비지 컬렉터에 의해 호출되지만, 이는 몇가지 문제점을 가지고 있습니다.
finalize()메서드를 잘못 사용하면 가비지 컬렉터에 의해 제거된 객체가 resurrect(부활)할 수도 있습니다. (finalized() 메서드에서 Strong Reference를 갖도록 코드를 작성한다면 부활합니다)
그러나 유령 참조는 메모리에서 해제된 후 enqueue되기 때문에 객체가 부활하는 문제가 없습니다.
Garbage Collection Process
앞에서 설명한 것 처럼, Stack의 변수가 Heap의 객체에 대해서 보유한 참조 유형(Reference Type)에 따라 가비지 컬렉터(이제부터는 GC라 부르겠습니다)에 의해 수집되는 시점이 다릅니다.
아래 그림을 보면서 설명을 이어가겠습니다.
위 그림에서 빨간색으로 표시된 Heap 영역의 객체들은 GC에 의해서 수거됩니다.
오른쪽 상단에 보면, 힙 영역의 객체가 다른 두 객체에 대한 강력한 참조가 있는 것을 알 수 있습니다. 그러나 해당 객체는 스택에서 참조되지 않는 객체이기에 이는 더이상 도달할 수 없는 객체이며, 따라서 GC에 의해 수거됩니다.
GC에 대해 좀 더 자세히 알아보기 전에 먼저 몇 가지 GC 프로세스의 특징을 언급하고 넘어가겠습니다.
- GC 프로세스는 JAVA에 의해 자동적으로 실행되며, 언제 GC가 작동할지는 JAVA가 결정합니다. (쉽게 말해서 우리는 언제 GC가 작동할 지 알 수 없다는 소리입니다.)
- GC는 사실 비용이 많이 드는 과정입니다. GC가 실행된다면 현재 실행중인 애플리케이션의 모든 스레드가 일시중지됩니다.
- GC 프로세스는 가비지 수집과 메모리 해제보다 훨씬 더 복잡한 프로세스입니다.
GC의 실행 여부는 JAVA가 결정하지만, 사실 저희는 System.gc() 메서드를 통해 GC를 호출할 수 있습니다.
그런데 주의할 점이 있습니다.
System.gc()를 호출했다고 해서 무조건 작동하는 것이 아닙니다. 단지 GC를 수행해 달라고 JAVA에 요청을 하는 것이지, 실제로 호출할 지 말지에 대한 여부는 JAVA가 결정합니다.
어쨌든 System.gc()를 호출하는 것은 권장되지 않습니다.
GC 프로세스는 매우 복잡하며, 성능에 영향을 줄 수 있기 때문에, 조금 똑똑한 방식으로 구현됩니다.
소위 "Mark and Sweep" 프로세스라 불리는 방식이 사용됩니다.
Java는 Stack 영역의 변수들을 분석하고 살려놔야 하는(표현이 조금 웃긴데.. 활성상태로 유지해야 하는..? 아무튼 그런 의미입니다!) 모든 객체에 대해 Mark(표시)를 남깁니다.
그리고 Mark(표시)되지 않는 객체들을 모두 정리합니다.
쓰레기가 더 많아지고, 활성 상태로 Mark되는 객체들이 적을수록 프로세스는 더 빨라집니다.
이 과정을 더욱 최적화시키기 위해서 Heap 메모리는 여러 부분으로 구성됩니다.
JVisualVM을 사용하여 메모리 사용량 및 기타 유용한 것들을 시각화할 수 있습니다.
Visual GC라는 플러그인을 설치하기만 하면 사용할 수 있으며, 지금은 이미 분석된 그림을 보고 설명을 이어가겠습니다.
객체가 생성되었을 때, 해당 객체는 1번 영역인 Eden 영역에 할당됩니다.
Eden 영역은 크지 않기 때문에, 빠른 속도로 가득 차게됩니다.
GC는 Eden 공간에서 개체를 활성 상태로 Mark합니다.
만약 객체가 GC 프로세스에서 살아남는다면 "survivor space(생존 공간)"이라 불리는 S0영역(2번)으로 이동하게 됩니다.
GC가 Eden 공간에서 또다시 수행되었을 때, 살아남은 모든 객체들은 S1영역(3번)으로 이동합니다. 또한 S0영역에 존재하는 모든 객체들도 S1공간으로 이동됩니다.
객체가 X번째의 GC 프로세스 동안 생존하는 경우, 해당 객체는 영원히 생존할 가능성이 가장 높으며, 이는 Old영역(4번)으로 넘어갑니다.
(여기서 X번째는 JVM의 구현에 따라 다르며, 위 예시의 경우에는 8번동안 살아남으면 Old 영역으로 이동합니다.)
GC 그래프(6번)를 보면 GC를 실행할 때마다 객체가 생존 공간으로 이동하고, Eden 공간이 확보되는 것을 볼 수 있습니다.
Old 영역에서도 가비지 수집이 가능하지만, Eden 공간에 비해 메모리의 큰 부분을 차지하므로 자주 발생하지는 앟습니다.
Metaspace 영역(5번)은 클래스 로더에 의해 로드된 클래스에 대한 메타데이터를 JVM에 저장하는 데 사용됩니다.
정리
- Young 영역(Young Generation)
- Eden 영역과 survivor영역이 존재
- 새롭게 생성된 객체가 할당(Allocation)되는 영역
- 대부분의 객체가 금방 Unreachable 상태가 되기 때문에, 많은 객체가 Young 영역에 생성되었다가 사라진다.
- Young 영역에 대한 가비지 컬렉션(Garbage Collection)을 Minor GC라고 부른다.
- Old 영역(Old Generation)
- Young영역에서 Reachable 상태를 유지하여 살아남은 객체가 복사되는 영역
- 복사되는 과정에서 대부분 Young 영역보다 크게 할당되며, 크기가 큰 만큼 가비지는 적게 발생한다.
- Old 영역에 대한 가비지 컬렉션(Garbage Collection)을 Major GC 또는 Full GC라고 부른다.
Garbage Collector Types
JVM에는 세 가지 유형의 GC가 있으며, 프로그래머는 이들 중 어떤 것을 사용할 지 선택할 수 있습니다.
기본적으로 Java는 기본 하드웨어를 기반으로 사용할 GC를 선택합니다.
- Serial GC (직렬 GC) - 단일 스레드 수집기(single thread collector)입니다. 주로 데이터 사용량이 적은 소규모 애플리케이션에 적용됩니다.
- Parallel GC (병렬 GC) - 여러 스레드를 사용하여 GC 프로세스를 수행합니다. 해당 유형의 GC를 throughput(스루풋) collector라고도 부릅니다.
- Mostly concurrent GC (주로 동시성 GC..?) - 이전에 GC 프로세스가 비싸며, 실행될 때 모든 스레드가 일시중지 된다고 했었던 것이 기억나시나요? 해당 유형의 GC는 애플리케이션과 동시에 작동한다고 명시되어 있습니다. 그러나 대부분(Mostly) 동시성인 데이는 이유가 있습니다. 응용 프로그램과 100% 동시에 작동하지는 않습니다. 스레드가 일시 중지되는 기간이 있지만, 그래도 최고의 GC 성능을 달성하기 위해 가능한 짧게 유지됩니다. 실제 Mostly concurrent GC는 다음 두 가지 유형이 있습니다.
- Garbage First - 합리적인 어플리케이션의 일시 중지 시간으로 높은 스루풋을 가집니다.
- Concurrent Mart Sweep - 어플리케이션의 일시 중지 시간이 최소로 유지됩니다. 그러나 JDK 9부터 이 GC 유형은 더이상 사용되지 않습니다.
📔 Reference
'☕️ Java > 기본' 카테고리의 다른 글
[Java] static 파헤치기 (feat. Hiding (static Method의 Overriding)) (0) | 2022.01.24 |
---|---|
자바는 어떻게 작동하는가? (JVM, Class Loader, JVM Memory 등) (0) | 2022.01.24 |
[Java] ThreadLocal에 대해 (0) | 2022.01.22 |
[Java] 애너테이션에 대한 기초 (0) | 2022.01.22 |
[Java] HashSet, LinkedHashSet, TreeSet (0) | 2022.01.21 |