본문 바로가기
Programming/Languages (Java, etc)

[JAVA] Garbage Collection (가비지 컬렉션)

by kghworks 2022. 2. 16.

목차

  • JVM 가상 머신의 메모리 관리
  • Garbage Collection
  • GC 동작 예시 (코드)
  • GC 원리 (메모리)
  • GC의 종류
  • 참고

* JAVA의 메모리 원리에 대해 안다는 전제 하에 포스팅합니다. stackheap에 대한 최소한의 이해도를 가진 후 보시길 바랍니다. 혹여 부족하시다면, 아래 포스팅을 먼저 참고하실 수 있습니다.

2022.01.26 - [개발/JAVA] - stack, heap (스택과 힙) - 자바의 메모리 런타임

 

stack, heap (스택과 힙) - 자바의 메모리 런타임

목차 자바의 메모리 영역 스택 (stack) 힙 (heap) 가비지 컬렉션 (Gabage Collection) 자바의 메모리 영역 자바의 메모리 구조는 크게 스택과 힙으로 나뉩니다. 스택 (stack) 기본 타입 (long, int, boolean,...)..

kghworks.tistory.com


JVM 가상 머신의 메모리 관리

C, C++언어는 운영체제의 메모리에 직접 접근을 합니다. 하여 free()를 통해 애플리케이션 실행 중의 메모리 관리를 개발자가 매번 명시해줌으로써 관리해줘야 합니다. 만일 누락한다면 memory leak가 발생합니다.

 

 그에 반해 JAVA는 JVM 가상 머신을 통해 메모리에 접근합니다. 이 JVM은 C 언어로 작성된 별도의 프로그램입니다. JVM이 free()를 호출하여 운영체제의 메모리를 직접 관리합니다. 따라서 개발자는 메모리 관리를 일일이 해주지 않아도 JVM에 위임했기 때문에 애플리케이션이 작동하면서 자동으로 메모리가 관리되는 편리함이 있습니다.

 

 프로그램이 실행될 때는 운영체제로부터 할당받을 메모리 사이즈를 JVM에 옵션을 설정함으로써 정할 수 있습니다. 이때 할당받은 메모리 이상을 사용하려 하면 에러가 나지만 다른 프로그램에는 영향이 가지 않습니다. 

 

 

 정리하자면, JVM은 OS로부터 별도의 메모리를 할당받아 메모리를 관리함으로써 운영체제 레벨의 memory leak를 방지해주고, 개발자가 별도로 메모리 관리를 신경 써주지 않아도 되는 편의를 제공합니다.


Garbage Collection

 JVM이 어떻게 메모리를 관리하는지 자세히 들여다 보겠습니다. 기본 메커니즘은 아래와 같습니다.

 

Heap영역 객체 중 더 이상 Stack으로부터 도달 되지 않는 객체들 (Unreachable Object)을 제거한다.

그리고 위 작업를 가비지 컬렉션 (Garbage Collection, 이하 GC)라고 합니다.

 

stop-the-world

 GC를 실행하기 위해 JVM이 GC를 실행할 스레드를 남기고 모두 멈춥니다. GC 실행 중에는 GC작업 중인 스레드 외에는 어떤 스레드도 작동할 수 없고, GC가 끝나면 중단된 스레드들이 다시 작업을 시작합니다. 어떤 알고리즘을 택하건 GC 작업 선두의 stop-the-world는 필수이며, GC 튜닝의 대부분이 이 stop-the-world 단계의 시간을 줄이는 데에 목적을 둡니다.

System.gc();

 위 코드를 명시하여 GC를 실행할 수도 있는데 이는 미친 짓입니다. 대부분이 멀티스레드 프로그램이고, 어떤 작업이 수행되고 있을지 모르는데 특정 비즈니스 로직에서 위 코드를 적는다면 모든 스레드가 일시 중지되어 다른 곳까지 영향을 끼치게 되므로 알고만 있고 절대 사용하지 말도록 하길 바랍니다.

 

Mark and Sweep

 JVM이 stack의 모든 변수를 스캔하여 heap에 reachable 한 오브젝트를 가진 변수를 marking 합니다. 그 후 mark 되지 않은 객체 (unreachable object)들을 heap에서 제거하는데 이 작업이 Sweep입니다. 조금 특이한 건 Garbage를 Collection 할 것 같지만 실제로는 Garbage를 수집하지 않고 Garbage가 아닌 것들을 marking 한 다음 그 외의 것을 한 번에 지운다는 것입니다.

 

간단히 아래 순서로 GC가 이루어집니다.

 

stop-the-world -> Mark and Sweep


GC 동작 예시 (코드)

GC가 실행됨을 확인할 수 있는 아주 간단한 코드를 준비했습니다.

li라는 리스트 객체를 선언하여 1~100의 숫자를 반복문을 무한이 돌리며 할당해주는 코드입니다.

 

참고로, 빠른 OutOfMemoryError 유발을 위해 vm 옵션으로 -Xmx6m을 설정해주었습니다.

-Xmx6m : Heap 사이즈를 6MB로 제한

 

GC가 정상적으로 작동하는 예시입니다.

코드

public static void main(String[] args) throws Exception {
        List<Integer> li = new ArrayList<>();
        for (int idx=1; true; idx++) {
            if (idx % 100 == 0) {
            	li = new ArrayList<>();
                Thread.sleep(100);
            }
            for(int k = 0; k < 100; k++){
                li.add(k);
            }
            System.out.println("idx : "+ idx);
        }
}

실행결과

...
(생략)
...
idx : 19898
idx : 19899
.....

 for문의 idx가 n00번이 돌아올 때마다 li 변수를 새롭게 할당합니다. 새롭게 할당함으로써 unreachable object를 만들었고, GC 대상이 되어 제거가 되었기 때문에 heap의 사이즈를 넘치지 않았습니다.

 

이번엔 li를 새롭게 할당하는 부분을 지우고 실행해보겠습니다.

코드

public static void main(String[] args) throws Exception {
        List<Integer> li = new ArrayList<>();
        for (int idx=1; true; idx++) {
            if (idx % 100 == 0) {
//                li = new ArrayList<>();
                Thread.sleep(100);
            }
            for(int k = 0; k < 100; k++){
                li.add(k);
            }
            System.out.println("idx : "+ idx);
        }
}

실행결과

...
(생략)
...
idx : 5401
idx : 5402
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at java.util.Arrays.copyOf(Arrays.java:2245)
	at java.util.Arrays.copyOf(Arrays.java:2219)
	at java.util.ArrayList.grow(ArrayList.java:242)
	at java.util.ArrayList.ensureExplicitCapacity(ArrayList.java:216)
	at java.util.ArrayList.ensureCapacityInternal(ArrayList.java:208)
	at java.util.ArrayList.add(ArrayList.java:440)
	at main.java.com.study.GandC.main(GandC.java:16)
Disconnected from the target VM, address: '127.0.0.1:5060', transport: 'socket'

 

 반복문은 5402번까지 돌았고, OutofMemoryError가 발동했습니다. 메시지는 Java heap space입니다. 에러 원인은 아래와 같이 메모리가 부족하여 JVM이 객체를 할당할 수 없고, Garbage Collector가 가용 가능한 메모리를 할당할 수도 없기 때문이라고 나오네요.

oracle odcs의 OutOfMemoryError

 li 변수의 객체에 대한 재할당 없이 계속해서 하나의 객체에 데이터를 넣었기 때문에 야기된 겁니다. 객체의 재할당이 효과적인 GC 방안이라는 것이 아니고, GC가 작동하는지를 보여주기 위한 간단한 예시였습니다.


GC 원리

 메모리 단에서 GC가 어떻게 동작하는지 살펴보겠습니다.

 

GC를 실행하는 자를 가비지 컬렉터라 합니다. 가비지 컬렉터는 아래와 같이 2가지 기본 전제 (weak generational hypothesis)를 가지고 움직입니다.

  • 대부분의 객체는 비교적 금방 Unreachable Object가 된다.
  • 오래된 객체에서 젊은 객체로의 참조는 흔치 않다.

 이 전제를 최대한 이용하기 위해 HotSpot JVM은 다음과 같은 물리적 공간을 가지게 되었습니다.

HotSpot JVM : 현재 가장 일반적인 JVM 중 하나

 

Young영역 (Young Generation 영역)

 새로 생성한 객체의 대부분이 위치하는 영역입니다. 대부분의 객체는 금방 Unreachable 하게 되기 때문에 많은 객체들은 여기에서 금방 사라지게 됩니다. 이 영역에서 객체가 사라질 때 일어나는 것이 Minor GC입니다.

 

Old 영역 (Old Generation 영역)

 Young 영역에서 살아남은 객체는 이 영역으로  promotion (이동)됩니다. 금방 사라지지 않을 객체이기에 크기를 크게 할당해주며, GC는 Young보다 적게 발생합니다. 여기서 객체가 사라지는 것을 Major GC (Full GC)라고 합니다.

 

Permanent 영역 (Method Area)

Perm 영역은 객체나 억류 (intern)된 문자열을 저장하는 영역이다. 여기서 일어나는 GC 또한 Major GC입니다.

GC 중 객체 흐름도

Card Table

 카드 테이블은 Old영역의 객체가 Yong 객체를 참조하는 것에 대한 정보를 저장하는 곳입니다. Old영역에 512바이트 chunk로 되어있습니다.  Minor GC가 일어날 때 Old영역의 객체를 모두 뒤지지 않고 카드 테이블만 뒤져서 GC 대상을 골라냅니다.  write barrier를 사용하여 관리 때문에 약간의 오버헤드가 발생하면서도 Minor GC 소요시간은 단축시킬 수 있습니다.

 

Card Table

Minor GC

 Young 영역에서 발생하는 GC입니다. Young 영역은 다시 3가지로 구성됩니다.

 

  • Eden 영역
  • Survivor 영역 (2개, 임의로 A, B로 나누겠음)

 

Minor GC 순서는 아래와 같습니다.

 

  1. 새롭게 생성한 객체는 Eden에 위치한다.
  2. Eden 영역에서 GC가 발생 후 살아남은 객체는 Survivor A 영역으로 이동한다.
  3. Survivor A가 가득 차면 그 안의 객체를 다른 Survivor B로 이동시킨다. (Survivor A를 비움)
  4. 2~4를 반복한다. (A가 가득 차면 B로, B가 가득차면 A로)
  5. 객체가 가득차 다른 Survivor로 이동할 때는 Age값이 증가된다.
  6. Survivor 영역 하나는 언제나 빈 상태이다.
  7. 특정 Age값이 넘어간 객체는 Old Generation으로 이동한다. (promotion)

Minor GC 흐름도


GC의 종류

 Major GC란 Old 영역에서 발생하는 GC를 말합니다. Old 영역은 기본적으로 데이터가 가득 차면 GC를 실행합니다. Old 영역에 대한 GC는 아래 GC의 기본 종류들을 나열하면서 같이 설명하겠습니다. GC의 종류는 아래와 같습니다.

 

  • Serial GC
  • Parallel GC
  • Parallel Old GC(Parallel Compacting GC)
  • Concurrent Mark & Sweep GC
  • G1(Garbage First) GC

 

Serial GC

 PC CPU의 코어가 한 개일 때를 위해 만든 방식으로 애플리케이션의 성능을 많이 떨어뜨리므로 운영서버에서 절대 채택하면 안 되는 방식입니다. 이 GC는 적은 메모리, CPU 코어 개수에 적합합니다. 

 

 mark-sweep-compact 알고리즘을 사용하는데, Old영역에 살아있는 객체를 Mark 하고, Heap 앞부분부터 확인하여 Mark하지 않은 것들을 Sweep (제거)합니다. 그 후 Heap 앞부분부터 객체가 연속되게 쌓일 수 있게 객체의 존재 부분과 비존재 부분을 나눕니다. (compact)

 

Parallel GC (Throughtput GC)

 기본 알고리즘은 Serial GC와 같으나, 처리하는 스레드가 여러 개입니다. 따라서 GC속도가 Serial GC에 비해 빠른 이점이 있습니다. 메모리가 충분하고 CPU 코어 개수가 많을 때 적합합니다.

 

Parallel Old GC(Parallel Compacting GC)

  앞서 설명한 Parallel GC와 old 영역의 GC 알고리즘만 다르고 동일합니다. 이 GC는 mark-summary-compaction 단계를 거치는데, 앞서 mark-sweep-compaction 단계와는 sweep 부분이 다릅니다.. 이 summary는 sweep 보다 더 복잡한 단계입니다.

 

Concurrent Mark & Sweep GC (이하 CMS GC)

 아래 그림은 CMS (우측) Serial GC (좌측)와 비교한 것입니다.

Serial GC vs CMS

 GC 튜닝의 맹점이라 할 수 있는 stop-the-world 시간이 훨씬 짧습니다. Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아있는 객체를 확인하고 끝냅니다. 다음  Concurrent Mark에서는 Initial Mark 단계에서 살아있다고 확인한 객체들을 일일이 따라가서 확인하는데 이때 다른 스레드가 실행되면서 동시에 실행됩니다. 

 

 Remark단계에서는 Concurrent Mark에서 새롭게 추가되거나, 참조가 끊긴 객체를 확인하고, 마지막 Concurrent Seep에서 비로소 쓰레기들을 지워버립니다.

 

 당연히 더 많은 메모리와 CPU를 사용하기에 주의해야 합니다. 또한 Compaction 단계가 없다는 단점도 있기 때문에 조각난 메모리를 Compaction 해주는 추가적인 절차가 필요하고 이는 GC 방식들의 stop-the-world 시간보다 길기 때문에 Compaction 작업의 주기와 소요시간을 체크해야 합니다.

 

G1(Garbage First) GC

 이 GC는 위에 설명한 Old, Young 영역의 개념이 아예 없는 GC입니다. 바둑판 영역에 객체를 할당하고 GC를 실행합니다. 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행합니다. 앞서 설명한 young의 세 가지 영역과 old 영역의 개념이 아예 사라진 GC 방식입니다. 


참고

 

https://d2.naver.com/helloworld/1329

https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html

 

Java Garbage Collection Basics

Java Overview Java is a programming language and computing platform first released by Sun Microsystems in 1995. It is the underlying technology that powers Java programs including utilities, games, and business applications. Java runs on more than 850 mill

www.oracle.com

https://docs.oracle.com/javase/8/docs/api/

 

Java Platform SE 8

 

docs.oracle.com

 

댓글