👉 [Java] JVM 메모리 관리는 어떤 원리로 돌아갈까?
JVM 이 메모리를 관리한다는 것은 알겠는데, 실제로 GC가 어떻게? 동작하는 것인지 궁금해졌다.
Garbage ?
= 유효하지 않은 메모리 주소를 말한다. Unreachable Object 라고도 부른다.
아래 예시를 보자.
//Java
String talk = "Hello";
talk += " World";
좌측 그림처럼 String 객체인 talk는 Heap 내 "Hello" 라는 주소값을 가리키고 있다.
그러나 += 연산을 통해 새로운 객체를 할당하여 "Hello World" 라는 주소값을 가리키도록 변경된다.
해당 부분은 String의 불변성 특성 때문에 차이가 발생한다. 주제에서 살짝 벗어나니 해당 부분은 생략한다.
우측과 같이 talk 가 가리키는 주소값이 변경되었다면, 기존의 0x10 주소 내 Hello는 어떻게 될까?
만약 메모리 직접 할당/해제가 가능한 C 였다면 아래와 같이 작성되었을 것 이다.
(C 를 다룬지 너무 오래되었지만 기억을 더듬어 써본다...)
// C
char *talk;
talk = (char*)malloc(256);
strcpy(talk, "Hello");
..(중략)..
free(talk);
중요한 차이점은 malloc&free 부분이다.
메모리 할당/해제를 개발자가 처리하는 C 계열에서는 위와 같은 가비지(Hello) 값들을 하나하나 지울 수 있다.
malloc() = memory + allocation
free() = memory + free
그러나 Java 진영에서는 해당 부분을 JVM에게 위임한다.
malloc&free 를 프로그램을 통해 처리하는 것.
그 녀석이 바로 JVM의 Garbage Collector 이다.
Garbage Collector
= 가비지 객체의 메모리를 해제시키는 SW
Garbage 자체가 Heap에서 발생하므로, Heap의 구조를 알아보았다.
JVM의 Heap은 위와 같이 YG / OG / PG 영역으로 구분된다.
다시 YG는 eden / survivor 로 나뉘는데, 이 구조를 통해 더 효율적인 GC가 가능케한다.
아래에서 필요한 개념이니 한줄 정리만 넣고 가자.
"Young Generation"은 새로 생성된 객체가 할당되는 곳
"Old Generation"은 오래된 객체들이 저장되는 곳
"Permanent Generation"은 사용하는 클래스와 메서드에게 필요한 메타데이터를 포함
GC 동작 원리
JVM 내의 GC는 Mark&Swap 이라는 알고리즘을 채택했다.
Python은 Reference Counting 알고리즘을 활용한다.
1. First, any new objects are allocated to the eden space. Both survivor spaces start out empty.
= Eden 영역에 객체들이 할당되어 쌓인다 (위 예시의 Hello, Hello world 등)
2. When the eden space fills up, a minor garbage collection is triggered.
= Eden이 가득차면 Minor GC가 실행된다.
3. Referenced objects are moved to the first survivor space. Unreferenced objects are deleted when the eden space is cleared.
= 참조되어지는 쓰레기가 아닌 객체들은 Survivor 영역으로 보내진다. 반대로 쓰레기들은 Eden 에서 해제된다.
4. At the next minor GC, the same thing happens for the eden space. Unreferenced objects are deleted and referenced objects are moved to a survivor space. However, in this case, they are moved to the second survivor space (S1). In addition, objects from the last minor GC on the first survivor space (S0) have their age incremented and get moved to S1. Once all surviving objects have been moved to S1, both S0 and eden are cleared. Notice we now have differently aged object in the survivor space.
= 다음 Minor GC 실행시, Eden에서 같은 동작을 실행한다. 그런데 Survivor0가 아니라 Survivor1 영역으로 Eden + Survivor0 에 있는 객체들이 함께 이동한다. 더불어 이동하면서 age-bit += 1 처리 한다.
5. At the next minor GC, the same process repeats. However this time the survivor spaces switch. Referenced objects are moved to S0. Surviving objects are aged. Eden and S1 are cleared.
= 다음 Minor GC 에서 똑같이 반복한다. 이번에는 살아남은 S1 + Eden 객체들이 S0로 향한다.
6. This slide demonstrates promotion. After a minor GC, when aged objects reach a certain age threshold (8 in this example) they are promoted from young generation to old generation.
= 좌측 슬라이드는 Promotion 이라는 개념이다. YG에서 OG로 특정 age-bit 이상 살아남은 객체들을 승격시켜주는 과정이다.
GC 최적화할때, age-bit의 threshold 값을 변경할 수 있다고 한다.
7. So that pretty much covers the entire process with the young generation. Eventually, a major GC will be performed on the old generation which cleans up and compacts that space.
= YG에서 이 과정이 매우 많이 처리된다. 결국엔 Major GC가 OG 영역에서 발생되며 참조되지 않는 객체를 해제한다.
위와 같은 Mark&Swap 동작 방식으로 가득찬 메모리를 처리하면서 JVM은 GC를 동작 시킨다. 최근에는 G1 GC 같은 방식도 있다는데, 결국 "메모리 해제해야하는 부분을 인지하고 + 조금씩 지워주는 것" 이라는 논리는 같다.
마지막으로 문득 궁금했던 점.
왜 Survivor 영역을 2개로 놨을까?
해당 글에서는 fragmentation(단편화) 이슈 때문이라고 표현한다. Survivor + Eden 모두에게 GC가 발생 했을 때, Swap 이후 객체들이 continuous 하게 관리되지 않기 때문에 fragmentation이 발생한다.
fragmentation에 대한 내용은 여기에 정리했었다.
CS는 결국 사람이 만든 것이라 그런지, 대부분의 기초 논리들이 많이 통용되는 것 같다. 이것이 CS가 중요한 이유라고 생각한다. 기초를 알아야 적용과 이해가 쉽다.
출처
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://stackoverflow.com/questions/10695298/java-gc-why-two-survivor-regions