GC / 메모리 누수(Memory Leak)

HyeonWoo·2021년 5월 11일
0
post-thumbnail

Java에서는 개발자가 프로그램 코드로 메모리를 명시적으로 해제하지 않기 때문에 가비지 컬렉터가 더 이상 필요 없는 (UnReachable) 객체를 찾아 지우는 작업을 한다.

GC의 설계 원칙

  • 대부분의 객체는 금방 접근 불가능한 상태(unreachable)가 된다. ⇒ minor GC

  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다. ⇒ magor GC

GC(Garbage Collection)이란?

JVM의 Heap 영역에서 사용하지 않는 객체를 삭제하는 프로세스를 말한다.

  • Heap 영역 : 동적으로 할당한 메모리 영역

ex) Object 타입의 데이터들, String, List 등 → Heap 영역의 Object를 가리키는 참조 변수가 Stack에 할당.

  • Stack 영역 : 정적으로 할당한 메모리 영역

ex) 원시 타입의 데이터가 값과 함께 할당, Heap영역에 생성된 Object 타입의 데이터의 참조 값 할당.

GC는 어떤 객체를 수거할까?

GC Roots로부터 어떤 객체에 유효한 참조가 존재한다면 Reachable, 그렇지 않다면 Unreachable이라고 한다.

  • Unreachable한 객체들이 GC의 수거 대상

  • GC Roots : Stack 영역의 데이터들, method 영역의 static 데이터들, JNI에 의해 생성된 객체들.

GC는 어떻게 동작할까?

GC 동작의 기본 원리

Mark and Sweep

  1. Mark : GC는 GC Root로부터 모든 변수를 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다. (Reachable 객체와 Unreachable 객체를 식별하는 과정)

  2. Sweep : Unreachable 객체들을 Heap에서 제거한다.

+3. (알고리즘에 따라 추가 될 수 있음) Compact : Sweep 후에 분산된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 나눈다. (메모리 단편화를 막아줌)

GC는 언제 일어나고 어떠한 과정으로 처리될까?

Young 영역 : 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다.

Old 영역 : 접근 불가능 상태(UnReachable)로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다. 이 영역에서 객체가 사라질 때 Major GC가 발생한다.

Minor GC : Eden 영역이 꽉 차게 되면 발생.

  1. Mark 실행 ( Unreachable, Reachable 식별)

2-1. Reachable 객체들은 Survivor 영역으로 이동 (0 or 1로 이동)

2-2. Sweep 실행 : UnReachable 객체들은 GC에 의해 수거

*객체들은 Survivor1 또는 Survivor2 영역에 따로 존재해야함 (Survivor 영역 중 하나는 반드시 비어 있는 상태)

  1. Survivor 영역에 있는 객체들은 age가 증가한다. (Aging)

  2. 또 다시 Eden 영역이 꽉 차게 되면 Minor GC가 발생

  3. 이전 단계에서 Reachable 객체들이 Survivor1 영역에 있었다면 Age가 1 증가하고 Survivor2 영역으로 이동

  4. 계속 반복

  5. 객체의 age가 age threshold (임계점)에 도달하면 Old Generation으로 이동.

Major GC : Old Generation이 꽉 차면 Major GC가 일어남

GC의 종류에 따라서 처리 절차가 달라지므로, 아래 GC종류에서 살펴보도록 하겠다.

Stop - the- world

  • GC를 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것.

  • GC를 실행하는 쓰레드 외의 모든 쓰레드가 작업을 중단한다.

  • GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작.

  • 대개의 경우 GC 튜닝이란 이 stop-the-world 시간을 줄이는 것이다.

GC의 종류

Serial GC

  • GC를 처리하는 쓰레드가 1개(싱글 쓰레드)
  • 다른 GC에 비해 stop-the-world 시간이 길다.
  • Mark-Compact 알고리즘 사용

Parallel GC

  • Java 8의 default GC
  • Young 영역의 GC를 멀티 쓰레드로 수행
  • Serial GC에 비해 stop-the-world 시간 감소

Parallel Old GC

  • Parallel GC를 개선
  • Old 영역에서도 멀티 쓰레드 방식의 GC 수행
  • Mark - Summary - Compact 알고리즘 사용

*sweep : 단일 쓰레드가 old 영역 전체를 훑는다.

*summary : 멀티 쓰레드가 old 영역을 분리해서 훑는다.

CMS GC(Concurrent Mark Sweep)

  • 모든 애플리케이션의 응답 속도가 매우 중요할 때 사용.
  • stop-the-world 시간을 줄이기 위해 고안됨
  • compact 과정이 없음.(메모리 단편화가 일어날 수 있음), 다른 GC 방식보다 메모리와 CPU를 더 많이 사용.
  • UnReachable객체와 Reachable 객체를 한번에 찾지 않고 순차적으로 찾는 것이 특징.

  1. Initial Mark : GC Root에서 참조하는 객체들만 우선 식별

  2. Concurrent Mark : 이전 단계에서 식별한 객체들이 참조하는 모든 객체 추적

    (GC를 처리하는 쓰레드는 하나지만 다른 작업들도 계속계속해서 처리가 가능)

  3. Remark : 이전 단계에서 식별한 객체를 다시 추적. 추가되거나 참조가 끊기 객체 확정

  4. Concurrent Sweep : 최종적으로 unreachable 객체들을 삭제

G1 GC(Garbage First)

  • CMS GC를 개선 (메모리 단편화 문제) , 어떤 GC 방식보다 빠름.
  • JAVA 9+의 default GC
  • 바둑판의 각 영역에 객체를 할당하고 GC를 실행. 그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행.
  • Heap을 일정한 크기의 Region으로 나눔
  • 전체 Heap이 아닌 Region 단위로 탐색
  • compact 진행

메모리 관리

자바는 OS의 메모리 영역에 직접적으로 접근하지 않고 JVM 이라는 가상머신을 이용해서 간접적으로 접근. ⇒ 메모리 관리라는 까다로운 부분을 자바 가상머신에 모두 맡겨버린 것.

이렇게 자바는 가상머신을 사용함으로써 (운영체제로부터 독립적이라는 장점 외에도) OS 레벨에서의 memory leak은 불가능하게 된다는 장점이 있다.

*자바의 메모리 누수 : 메모리 누수는 더이상 사용하지 않는 객체가 가비지 컬렉션(GC)에 의해서 회수되지 않고 계속 누적되는 현상. Old 영역에 누적된 객체로 인해서 Major GC가 빈번하게 발생하게 되고, 프로그램의 응답 속도가 늦어지다 결국 OOM(OutOfMemory) 오류로 프로그램이 종료된다.

하지만!!

memory leak이 발생할 수 있다.

그 이유는 실제로 사용 되지 않는 객체의 reference를 프로그램에서 잡고 있으면 그 객체는 GC에 의해 처리되지 않고 프로그램내에서도 접근하여 사용될 수 없는 사실상 쓰레기로서 메모리(주소 공간)를 점유하게 된다.

자바의 사용되는 메모리란 사용될 가능성이 있다는 것 일뿐이므로 논리적으로도 정확하게 사용되고 있는 객체가 아닌 사실상의 쓰레기 객체가 있을 수 있으며 이러한 객체들이 메모리 누수 현상을 초래.

EX) 빈번한 전역변수의 선언, 리스트나 해쉬맵 같은 컬렉션에 저장한 객체를 해제하지 않고 계속 유지하게 되면 주로 발생

메모리 누수가 발생하는 상황들은 이 사이트에 잘 정리되어 있다.

  • Integer, Long 같은 래퍼 클래스를 이용하여, 무의미한 객체를 생성하는 경우
  • Map에 캐쉬 데이터를 선언하고 해제하지 않는 경우
  • 스트림 객체를 사용하고 닫지 않는 경우
  • Map의 키를 사용자 객체로 정의하면서 equals(), hascode()를 재정의 하지 않아서 같은 키로 착각하여 데이터가 계속 쌓이게 되는 경우
  • Map의 키를 사용자 객체로 정의하면서 equals(), hashcode()를 재정의 하였지만, 키값이 불편 데이터가 아니라서 데이터 비교시 계속 변하는 경우
  • 자료구조를 생성하여 사용하면서, 구현 오류로 인해 메모리를 해제하지 않는 경우

마무리

아직은 대규모 프로젝트를 경험한 적이 없어서 GC 튜닝이라는 주제가 많이 와닿지 않지만 그래도 GC에 대해 깊은 이해가 있어야 훌륭한 Java 개발자가 될 수 있다고 생각한다. GC가 어떤 방식으로 동작하는지, 튜닝을 어떤 방식으로 해야하는지, 메모리 누수는 어떤 현상에서 발생하는지 알아야 일정 규모의 프로젝트 진행시 조금 더 성능이 개선된 코드를 작성할 수 있다고 생각한다.


참고자료
https://d2.naver.com/helloworld/1329
https://www.youtube.com/watch?v=Fe3TVCEJhzo

profile
학습 정리, 자기 개발을 위한 블로그

0개의 댓글