배경
지난번 메모리 영역의 구조와 역할에 대해 알아봤다.
beststar-1.tistory.com/14
그렇지만 클래스, 메서드, 객체, 변수 등의 메모리 할당/해제를 모두 개발자가 관리해줄 순 없다. 이를 개발자가 매 코드마다 메모리 관련 메서드로 명시적인 관리를 하지 않아도 되게 해주는 것이 '가비지 컬렉터(Garbage Collector)'의 역할 중 하나이다.
웹 애플리케이션을 만들 때 검증된 라이브러리나 프레임워크를 이용하는 게 더 안전하고 편리한 것처럼, 메모리 관리도 개발자가 직접 하기보다는 JVM에 맡기는 것이다.
프로그램 실행 시 JVM 옵션을 주어서 OS에 요청한 사이즈만큼의 메모리를 할당받아서 실행하게 된다.
할당받은 것 이상으로 메모리를 사용하게 되면 에러가 나면서 프로그램이 종료된다.
그러므로 현재 프로세스에서 메모리 누수가 발생해도 현재 실행 중인 것만 종료되고 다른 것에는 영향을 주지 않는다.
이렇게 자바는 가상 머신을 사용함으로써 OS 레벨에서의 메모리 누수(Memory Leak)는 불가능하게 된다는 장점이 있다.
💡 메모리 누수(Memory Leak)란?
메모리의 힙(Heap) 영역에 할당된 부분이 참조되지 않는데도 해제되지 않은 채로메모리를 계속 점유하고 있는 것
그러나 가비지 컬렉터가 자동으로 메모리를 관리해준다고 해서 우리가 내부구조나 동작원리를 공부할 필요가 없는 것은 아니다. 그 이유를 아래처럼 정리할 수 있겠다.
- 같은 기능의 프로그램이더라도 메모리 관리에 따라 속도나 프로세스 실행 시간 등의 성능이 좌우됨
- 결국 자동이기 때문에 최적화에 한계가 있어서한정된 메모리를 효율적으로 사용할 수 있는 코드를 작성하는 것은 개발자 몫
이제 가비지 컬렉터의 내부구조와 동작원리에 대해 하나씩 자세히 알아보자.
💡 가비지 컬렉터와 관련된 글은 내용이 방대하므로 여러 차례에 걸쳐 포스팅할 계획이므로 참고 바란다.
과정
가비지 컬렉션(Garbage Collection)
우선 가비지 컬렉터가 수행하는 가비지 컬렉션에 대해 알아보자.
가비지 컬렉션(Garbage Collection, GC)은 메모리 관리 기법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요 없게 된 영역을 해제하는 기능이다. 1959년 무렵 리스프의 문제를 해결하기 위해 존 매카시가 개발하였다.
- 위키백과 -
💡 가비지 컬렉터는 내부적으로 finalize() 메서드를 호출하여 객체를 메모리에서 해제시킨다.
가비지 컬렉션은 자바만의 기능은 아니다. 개발자가 힙을 사용할 수 있는 만큼 자유롭게 사용하고, 더 이상 사용되지 않는 객체들은 가비지 컬렉션을 담당하는 프로세스가 자동으로 메모리에서 제거하도록 하는 것이 가비지 컬렉션의 기본 개념이다. 때문에 위에서 설명한 메모리 누수에 해당되는 인스턴스들이 가비지 컬렉션 대상이 되는 것이다.
그런데 어떻게 자동으로 메모리에서 제거하도록 하는 것일까?
가비지 컬렉터가 어떻게 메모리 상황을 다 알고 가비지를 어떻게 판단하며 가비지를 어떻게 수집하는지 들여다보자.
가비지(Garbage)는 사용되지 않는 객체(Unreachable Object)
가비지 컬렉터는 힙 영역에 있는 객체를 사용되는(Reachable) 것과 사용되지 않는(Unreachable)의 상태로 구분한다.
💡 오라클 공식 문서에서는 객체의 참조 유형을 구분짓고 그 정도를 'Reachability'로 표현하고 있다.
힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나인데,
- 힙 영역 내의 다른 객체에 의한 참조
- 스택 영역의 Java 메서드 내에서 실행하는 지역 변수, 파라미터, 연산 작업중 피연산자에 의한 참조
- 메서드 영역의 상수 풀이나 정적 변수에 의한 참조
- 아직 메모리에 남아 있는 Native 메서드로 넘겨진 객체에서 참조 (JNI에 의해 생성된 객체에 대한 참조)
이들 중 힙 내의 다른 객체에 의한 참조를 제외한 나머지 3개가 Root set(시작점 세트)로 Reachability를 판가름하는 기준이 된다. Root Set으로부터 어떤 식으로든 참조 관계가 있다면 Reachable Object, 없다면 Unreachable Object라 판단한다. 간단히는 객체들 간의 참조 사슬 시작점이라 보면 된다.
💡 레퍼런스 구조에 따라 정도를 구분하고, 정도에 따른 세부적인 Reachability 유형과 GC 과정을 포함한 자세한 내용은
Naver D2의 Java Reference와 GC를 읽어보는 걸 추천한다. 2013년에 작성된 글이지만 개인적으로 훌륭한 글 같다.
컬렉션(Collection)은 Mark and Sweep(?)
Root가 참조하는 모든 객체, 또 그 객체들이 참조하는 다른 객체들을 탐색해 내려가며
할당받은 각 메모리 영역에 1비트씩 남겨서 사용 중을 표시(Mark)한다.
이게 바로 가비지 컬렉션의 첫 번째 단계인 Mark단계이다.
💡 이 과정에서 전체 스레드가 멈추는데, 이것을 오라클 공식문서에서는 'Stop the World'라고 표현하고 있다.
흔히 가비지 컬렉션에서 성능 저하를 언급하는 것은 바로 객체 탐색과 전체 스레드 멈춤 때문이다.
Mark가 끝나면 가비지 컬렉터는 힙 내부를 전체를 돌면서 Mark 되지 않은 메모리들을 해제한다.
이 과정을 Sweep이라고 부른다.
💡 그런데 컬렉션 알고리즘이 이게 끝은 아니다.
그럼에도 Mark and Sweep을 언급한 이유는 많은 블로그 포스팅에서 이것만 언급하고 말아서
마치 이것이 기본 동작이고 이것만 있는 것처럼 오해를 일으킬 수 있다고 생각했기 때문이다.
그래서 다음 글에서는 컬렉션 알고리즘에 대해 따로 다루려고 한다.
힙(Heap) 영역의 구조와 가비지 컬렉션 프로세스 (부제 : G1 GC라는 가비지 컬렉션 등장 이전까지의 힙 영역 구조)
힙 영역은 Old, Eden, S0, S1 총 네 개 영역으로 나눌 수 있다.
인스턴스의 메모리 할당 측면에서 성능을 높이려고 용도를 구분해둔 것이다.
💡 Metaspace?
클래스 메타 데이터를 native 메모리에 저장하고 메모리가 부족할 경우 이를 자동으로 늘려주는 공간을 말한다.
힙은 Young Generation, Old Generation으로 크게 두 개의 영역으로 나누어 지고, Young Generation 은 또다시 Eden, Survivor Space 0, 1 로 세분화 되어진다. S0, S1 으로 표시되는 영역이 Survivor Space 0, 1 이다. 각 영역의 역할은 가비지 컬렉션 프로세스를 알면 알 수 있다.
1. 새로운 객체는 Eden 영역에 할당된다. 두개의 Survivor Space는 비워진 상태로 시작한다.
2. Eden 영역이 가득 차면, MinorGC(Young Generation을 대상으로 하는 GC)가 발생한다.
3. MinorGC 가 발생하면, Reachable 객체들은 S0으로 옮겨진다. Unreachable 객체들은 Eden 영역이 클리어 될 때 함께 메모리에서 사라진다.
4. 다음 MinorGC 가 발생할 때, Eden 영역에는 3번과 같은 과정이 발생한다. Unreachable 객체들은 지워지고, Reachable 객체들은 Survivor Space로 이동한다. 기존에 S0에 있었던 Reachable 객체들은 S1으로 옮겨지는데, 이때, age 값이 증가되어 옮겨진다. 살아남은 모든 객체들이 S1 으로 모두 옮겨지면, S0와 Eden 은 클리어 된다.
💡 Survivor Space 간의 이동마다 age 값이 증가한다.
JVM 중 가장 일반적인 HotSpot JVM의 경우 이 age의 기본 임계값은 31이다.
객체 헤더에 age를 기록하는 부분이 6 bit로 되어 있기 때문이다.
아래 그림은 S0(From)에서 age가 1이었던 객체들이 S1(To)로 이동하면서 2로 증가한 예시이다.
5. 다음 MinorGC 가 발생하면, 4번 과정이 반복되는데, S1 이 가득 차 있었으므로 S1에서 살아남은 객체들은 S0로 옮겨지면서 Eden과 S1 은 클리어 된다. 이때도 age 값이 증가되어 옮겨진다.
💡 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 한다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 여러분의 시스템은 정상적인 상황이 아니라고 생각하면 된다.
6. Young Generation에서 계속해서 살아남으며 age 값이 증가하는 객체들은 age 값이 특정값 이상이 되면 Old Generation(Java 8 까지는 Tenured Generation라 부름)으로 옮겨지는데 이 단계를 Promotion(진급)이라고 한다.
그렇기 때문에 MinorGC 가 계속해서 반복된다면, Promotion 작업도 꾸준히 발생하게 된다.
7. Promotion 작업이 계속해서 반복되면서 Old Generation 이 가득 차게 되면 MajorGC(Old Generation을 대상으로 하는 GC)가 발생하게 된다.
💡 참고로 힙 전체인 Young/Old 공간 모두를 Clear 하는 작업을 Full GC라고 한다.
💡 다음에 별도의 글로 다루겠지만 G1(Garbage First) GC 이전까지는 이런 프로세스를 거치고 G1 GC부터는 개념이 좀 달라진다. 그럼에도 여기서 다룬 이유는 가비지 컬렉션의 개념적인 이해를 돕기 위해서이다.
지금까지의 내용을 간략화하면 아래와 같다.
가비지는 JVM 메모리의 스택 영역, 네이티브 메서드 영역, 스태틱 영역 모두에서 참조하지 않는 객체를 말하며,
가비지 컬렉터는 효율적인 메모리 관리를 하기 위해 힙 영역에서 가비지를 찾아내고 특정 기준으로 메모리 할당을 해제하기 위해 만들어진 것이다.
결과
가비지 컬렉터에 대해 아래 사항들을 공부하고 정리했다.
- 가비지 컬렉션 개념
- 가비지 개념
- 힙 영역에서의 가비지 컬렉션 프로세스 (G1 GC라는 가비지 컬렉션 등장 이전까지의 힙 영역 구조)
성과
- 게임회사에서 Unity C#을 사용할 때 AndroidJavaClass를 사용하며 메모리 누수를 겪어 앱이 죽은 적이 있었다..NET 프레임워크 CLR의 가비지 컬렉션을 이해하지 않고 생성한 클래스 인스턴스들이 회수될 것이라 가정한 것이 문제의 원인이었고, C#의 using문과 IDisposable 인터페이스로 해결했었다.
가비지 컬렉션 공부의 필요성을 깨닫지 않고 공부하지 않았다면 자바의 JVM도 같은 실수를 반복했을 것이다.
추후 VisualVM과 같은 프로그램 모니터링 툴을 활용할 때 가비지 컬렉터를 공부한 지식을 이어서 활용할 것이라 기대한다.
본 글은 JVM 글에 있던 내용을 분리해둔 것이며,
추후 자바 성능 튜닝 관련 자료를 보며 공부할 때 내용이 추가될 예정이다.
여러 자료를 취합해보니 확실히 가비지 컬렉션을 다룰 일이 있다면 책 자바 성능 튜닝으로 이어서 공부하면 좋을 것 같다.
출처
가비지 컬렉션
ko.wikipedia.org/wiki/쓰레기_수집(컴퓨터_과학)
가비지 컬렉션 기본 설명,
Mark and Sweep
yaboong.github.io/java/2018/06/09/java-garbage-collection/
imasoftwareengineer.tistory.com/103
가비지 개념
docs.oracle.com/javase/7/docs/api/java/lang/ref/package-summary.html
힙에 있는 객체들에 대한 참조, Root Set
d2.naver.com/helloworld/329631
Metaspace
starplatina.tistory.com/entry/JDK8에선-PermGen이-완전히-사라지고-Metaspace가-이를-대신-함
힙 영역에서의 가비지 컬렉션 프로세스
www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
'JAVA > JVM' 카테고리의 다른 글
가비지 컬렉터(Garbage Collector) - 가비지 컬렉션 종류와 알고리즘 (0) | 2021.04.10 |
---|---|
메모리 영역(Runtime Data Area, JVM Memory) (0) | 2021.03.28 |
클래스로더(Class Loader) (0) | 2021.03.28 |
JIT(Just In Time) 컴파일러 (0) | 2021.01.03 |
JVM(Java Virtual Machine), 바이트코드(Byte Code) (0) | 2020.12.21 |