본문 바로가기
프로그래밍 언어/Java

저는 GC가 처음이라니까요?

by 조엘 2021. 10. 2.

안녕하세요! 조엘입니다!

 

"처음이라니까요" 시리즈 열한 번째 토픽은 GC입니다. 🙌🙌

C나 C++을 다룰 때는 메모리 관리를 직접 명시적으로 해줬어요. 

하지만 JAVA, JavaScript, Python 등을 쓰면서부터는 메모리 관리에 크게 신경을 안 써줬는데요. 

알아서 동적 메모리 영역이 관리되는 방법! 한 번 같이 알아봅시다. 🎯🎯

 

 

*** GC란? ***

GC는 메모리 관리 기법 중 하나로, 동적으로 할당했던 메모리 영역필요 없게 된 영역을 해제하는 기능이에요.

여기서 동적으로 할당했던 메모리 영역은 프로그램 런타임에 사용되는 Heap 영역 메모리를 뜻하고,  

여기서 필요 없게 된 영역은 어떤 변수도 가리키지 않게 된 영역을 의미합니다. 

 

CC++에서 Heap 영역의 메모리를 사용하기 위해서는,

코드 레벨에서 직접 동적 메모리 영역을 할당받고, 해제하는 과정을 작성해야 했어요. 

하지만 이를 수동으로 직접 관리하는 것은 번거롭고 실수하기도 쉬워요. 

할당받고 해제하지 않으면 메모리 영역에서 메모리 릭이 발생하고요.


다행히(?) Java, JavaScript, Python 등의 언어에서는 동적 메모리 영역 해제를 GC가 알아서 해줘요.

 

 

*** GC의 장단점 ***

GC 도입의 장점으로, 메모리를 수동으로 관리하던 것에서 비롯된 에러들을 예방할 수 있어요.

개발자의 실수로 인한 메모리 누수를 막을 수 있고,
해제된 메모리에 접근하는 오류와 해제된 메모리를 또 해제하는 이중 해제 또한 막을 수 있어요.

하지만 GC를 사용하는 것의 단점도 있어요.
어떠한 메모리 영역이 해제의 대상이 될지 검사하고 해제하는 일은 순수 오버헤드에요.
또한 GC의 메모리 해제 타이밍을 개발자가 정확하게 알기 어렵고요

따라서 실시간성이 매우 강조되는 프로그램의 경우 GC에게 메모리 관리를 맡기는 것이 알맞지 않을 수 있어요.

 

좋아요. GC의 장단점을 알아봤어요.

그렇다면 GC는 어떻게 해제할 동적 메모리 영역들을 알아서 판단할 수 있었을까요?
GC를 구현하는 대표적인 2가지 알고리즘 Reference Counting과 Mark And Sweep을 소개할게요. 

 

 

*** GC 알고리즘 - Reference Counting ***

 

Reference Counting

그림을 통해 Reference Counting을 알아보아요. 

 

그림 속 Root Space는 스택 변수, 전역 변수 등 heap 영역 참조를 들고 있는 공간이에요.  

 

Reference Counting은 힙 영역의 객체들이 각각 reference count라는 숫자를 가지고 있다고 생각하면 좋아요. 
여기서 reference count는 몇 가지 방법으로 해당 객체에 접근할 수 있는지를 뜻하는데요. 

Reference count가 0에 다다르면, 즉 해당 객체에 접근할 수 있는 방법이 하나도 없다면,

가비지 컬렉션의 대상이 되는 거죠. 간단하죠?

Swift 언어가 Reference Count 방식으로 메모리 관리를 해요.


하지만 reference counting에는 한계점이 있어요. 바로 순환 참조 문제인데요. 


그림 속 Root space에서의 Heap space 접근을 모두 끊는다고 생각해보세요. 
노란색 고리 안의 객체들은 서로가 서로를 참조하고 있기 때문에 Reference count가 1로 유지될 거예요. 
결국 사용하지 않는 메모리 영역이 해제되지 못하고 Memory Leak이 발생하는 것이죠. 

 

 

*** GC 알고리즘 - Mark And Sweep ***

 

Mark And Sweep

이번에도 그림을 통해 Mark And Sweep을 알아보아요. 

 

Mark And Sweep 알고리즘은 Reference Counting의 순환 참조 문제를 해결할 수 있어요. 

Mark And Sweep은 루트에서부터 해당 객체에 접근 가능한지, 아닌지를 해제의 기준으로 삼아요. 
루트부터 그래프 순회를 통해 연결된 객체들을 찾아내고 (이게 Mark고), 

연결이 끊어진 객체들은 지우는 방식이죠. (이게 Sweep입니다). 
루트로부터 연결된 객체는 Reachable, 연결되지 않았다면 Unreachable이라고 불러요.

그림에서는 Sweep 이후에 분산되어 있던 메모리가 예쁘게 정리된 것을 볼 수 있는데요. 
이를 메모리 파편화를 막는 Compaction이라고 해요. 

다만, Mark And Sweep에서 Compaction은 필수는 아니에요. 

이렇게 Mark And Sweep 방식을 사용하면, 루트로부터 연결이 끊긴 순환 참조되는 객체들도 지울 수 있어요. 
Java와 JavaScript가 Mark And Sweep 방식으로 메모리 관리를 합니다. 

 

하지만 Mark And Sweep도 단점이 있어요. 
객체의 reference count가 0이 되면 지워버리는 reference counting 방식과는 달리, 

Mark And Sweep은 의도적으로 특정 순간에 GC를 실행시켜야 해요. 

즉 어느 순간에는 실행 중인 어플리케이션이 GC에게 컴퓨터 리소스들을 내줘야 한다는 말이죠. 
따라서 어플리케이션의 사용성을 유지하면서 효율적이게 GC를 실행하는 것이 꽤나 어려운 최적화 작업이라고 합니다. 

 

 

방금까지 알아본 알고리즘을 토대로, 언어별 GC 상세구현이 다 달라질텐데요!

다음 포스팅에서 Java, 즉 JVM에서 GC가 어떻게 실행되는지 알아볼게요! 💪💪

 

읽어주셔서 감사합니다 :)

 

 

 

반응형

'프로그래밍 언어 > Java' 카테고리의 다른 글

JVM의 GC  (2) 2021.10.03
저는 어노테이션이 처음이라니까요?  (2) 2021.06.11
Reflection API  (4) 2021.06.09
저는 JVM이 처음이라니까요?  (6) 2021.05.28
HashMap/HashSet의 원리  (4) 2021.03.13

댓글