Skills/Java

Java - JVM & Garbage Collector

aoaa 2022. 9. 16. 20:19

 Garbage Collection은 Java에서 메모리 관리 방법 중의 하나로, JVM의 Heap 영역에서 동적으로 할당했던 메모리 영역 중 필요없게 된 메모리 영역을 주기적으로 삭제하는 프로세스를 의미합니다. (이 글에서 Static란에서 설명한 적이 있습니다.)

 Java의 장점 중 하나로, C나 C++같은 언어(Unmanaged Language)는 가비지 컬렉션이 없어 개발자가 수동으로 메모리 할당과 해제를 일일이 해줘야하는데, Java는 메모리 관리, 누수 문제를 대행하여 해결 해준다는 장점이 있습니다.

 가비지 컬렉션 설명에 앞서 이해를 위해 JVM의 구조를 짚고 넘어가는 것이 좋을 것 같습니다.


1. JVM(Java Virtual Machine) 구조

 

 위는 Java에서 프로그램 실행 단계를 나타낸 그림입니다. 먼저 Java 컴파일러에 의해 자바의 소스파일(.java)은 바이트 코드(.class)로 변환 됩니다. 이 바이트 코드를 JVM에서 읽어 OS를 거쳐 프로그램을 실행하게 됩니다. 

 이 때, Java 기반 애플리케이션은 JVM과 상호작용을 하는 것이기 때문에, OS와 H/W에 독립적이라는 특징을 가집니다. 예를 들어,

Linux에서 만든 자바 소스 파일을 Mac이나 Windows에서 실행하고 싶다면, 각 OS에 맞는 JVM을 설치하고 실행하면 됩니다.

 이처럼 다른 OS에서도 프로그램의 변경없이 실행이 가능하다는 큰 장점이 있습니다. 

 

1.1 JVM의 메모리 구조

 JVM의 구조를 살펴보면 Garvage Collector, Execution Engine, Class Loader, Runtime Data Area로 크게 4분류로 구분됩니다.

1.1.1 Class Loader

 클래스 로더는 컴파일된 바이트 코드 형태인 클래스 파일을 클래스로더가 읽어들입니다. 

크게 3가지 과정을 거치는데, 클래스를 읽어오는 Loading, 레퍼런스를 연결하는 Link, Static값을 초기화하고 변수를 할당하는 과정인 초기화 순으로 진행이 됩니다.

 

 먼저 클래스 파일을 읽고 내용에 맞는 binary 데이터를 생성한 뒤에 메모리의 메서드 영역에 저장하게 됩니다. 

로딩이 끝나면, 해당 클래스 타입의 class 객체를 생성하여 Heap 영역에 저장하게 됩니다. (Loading)

 

 Link 과정에서는 class 형식 파일이 유효한지 확인하는 Verify 단계를 거치고, 기본값에 필요한 메모리를 준비하는 Prepare, Symbolic Memory Reference를 실제 레퍼런스로 교체하는 Resolve 단계를 거칩니다.

 예를 들어 Person person = new Person();이라는 코드에서 person이라는 참조 변수가 Heap 저장된 실제 Book이라는 클래스를 가리킬 수 있도록 연결하는 과정입니다. (Link)

 

 마지막으로 초기화는 static으로 선언된 변수와 메서드에 메모리를 할당하고, 초기값을 채우는 과정입니다. (초기화)

1.1.2 Execution Engine

 Execution Engine은 클래스 로더에 의해 메모리에적재된 클래스들을 기계어로 바꿔 명령어 단위로 실행하는 역할을 합니다. 

1) 인터프리터는 Byte 코드를 기계가 이해할 수 있도록, Native Code로 바꾸는 작업을 합니다. Byte 코드를 한 줄마다 컴파일하여 변환하는 작업을 하게 되는데 이는 실행 시간이 길어지는 비효율적인 면이 있기 때문에, 중복되는 Byte코드에 대해서는 JIT 컴파일러를 사용합니다. 

2) JIT(Just In time) 컴파일러는 인터프리터의 효율을 높이기 위해서 반복되는 코드를 발견하면 JIT 컴파일러가 Native Code를 변환 시킵니다. 이 때, 반복된 Byte 코드는 바뀌어 있기 때문에 인터프리터가 바로 사용할 수 있게 됩니다. 

3) GC는 더 이상 참조되지 않는 객체를 정리하는 역할을 합니다. 

 

1.1.3 Runtime Data Area

  JVM의 메모리 영역으로, Java 애플리케이션을 실행할 때 사용되는 데이터를 적재하는 영역입니다. 

 1) Method Area는 모든 Thread가 공유하는 메모리 영역으로 클래스, 인터페이스, 메서드, 필드 Static 변수 등의 바이트 코드를 보관합니다. 

2) Heap Area는 new 키워드로 생성된 객체와 배열이 생성되는 영역입니다. 메서드 영역에 Load된 클래스만 생성이 가능하고 가비지 컬렉터가 참조되지 않는 메모리를 확인하고 제거하는 영역입니다. 

3) Stack Area는 메서드 호출 시 메서드만을 위한 메모리 공간인 Stack frame을 생성합니다. 이 메서드 안에서 사용되는 값을 저장하고, 호출된 메서드의 매개변수, 지역변수, return 값 및 연산 시 일어나는 값들을 임시로 저장하고, 메서드 수행이 끝나면 frame 별로 삭제합니다.

 예를 들어 Person person = new Person();이라는 소스를 작성하면 Person person는 스택영역에 생성되고 new로 생성된 Person 클래스의 인스턴스는 힙 영역에 생성되게 됩니다. 스택 영역에 생성된 person의 값으로 힙 영역의 주소값을 가지고 스택 영역에 생성된 person이 힙 영역에 생성된 객체를 참조하고 있습니다. 

4) PC register Thread가 생성될 때마다 생성되는 영역으로, 현 Thread가 실행되는 부분의 주소와 명령을 저장하고 있는 영역입니다. 이는 Thread를 돌아가면서 수행할 수 있게 합니다. 

5) Native Method Stack은 Java외 언어로 작성된 Native Code를 위한 메모리 영역입니다. 주로 C/C++의 코드를 수행하기 위한 스택입니다. 

 


2. Garbage Collection

 

 가장 위에서 간략하게 설명했지만 JVM의 Heap 영역에서 동적으로 할당했던 메모리 영역 중 필요없게 된 메모리 영역을 주기적으로 삭제하는 영역이라고 했습니다. 

 간단한 코드를 예를 들어 보겠습니다.

package Garbage

public class GarbageTest{
	public static void main(String[] args){
    	String a = new String("Hi");
        String b = new String("Bonjour");
        String c = new String("안녕하세요");
        String d, e;
        a = null;
        d = c;
        c = null;
        }
 }

 먼저 String형인 a, b, c라는 변수에 new 키워드로 생성된 객체에 a="Hi", b="Bojour", c="안녕하세요" 라는 인스턴스가 생성되었고 각각 이들을 참조하고 있습니다. (Heap에 할당)그런데 a에서 null을 실행하는 순간 "HI"라는 인스턴스와 변수 a와의 참조관계가 없어지게 되고 "Hi"라는 문자열 인스턴스는 어떤 변수도 참조하지 않게되는 Garbage 데이터가 됩니다. 

 이 때, Garbage 컬렉터가 이 잉여 데이터를 수집하여 메모리가 할당되지 않도록 수거하도록 관리하게 됩니다. 

 

2.1 Minor GC & Major GC

 

JVM의 Heap 영역은 2가지 전제로 설계되었는데, 첫 째로 대부분의 객체는 금방 접근 불가능한 상태가 되고, 둘 째로는 오래된 객체에서 새로운 객체로의 참조되는 경우는 매우 적다라는 것입니다. 

*Permanent : 초기에는 Permanent 라는 영역이 존재했지만 Java8 버전부터 없어졌습니다.

 이는 객체가 일회성이며 메모리에 오래 남아있는 경우는 드물다는 것입니다. 이에 객체가 살아있는 기간에따라 물리적인 Heap영역을 나누게 되었는데 Young과 Old 총 2가지 영역으로 설계되었습니다. 

 먼저 Young Generation은 새롭게 생성된 객체가 할당(Allocation)되는 영역으로 대부분의 객체가 단시간에 접근 불가능(Unreachable)해지기 때문에 대부분의 객체가 Young Generation에서 생성되었다 사라집니다. (Minor Garbage Collection)

 Old Generation은 Young Generation에서 접근 상태를 유지하여 살아남은 객체가 복사되는 영역으로, Young Generation보다 크게 할당됩니다. 많이 할당받은 만큼 Garbage는 적게 발생합니다. Young Generation이 수명이 짧은 객체들이 수거되기 때문에 큰 공간을 필요로 하지 않습니다. (Major Garbage Collection)

 하지만 예외적으로 Old의 객체가 Young의 객체를 참조하는 경우도 존재할 것입니다. 이를 대비하여 Old Generation에는 512bytes의 Chunk(덩어리)로 되어있는 Card Table이 존재합니다. 

 

 Card Table에는 Old Generation에 있는 객체가 Young Generation의 객체를 참조할 때 마다 그에 대한 정보가 표시됩니다.

Card Table의 존재 의의는 Young Generation에서 Minor Gc가 실행될 때 모든 Old에 존재하는 객체를 검사하여 참조되지 않는 Young Generation의 객체를 식별하는 것이 비효율적이기 때문입니다. 때문에 Card Table만 조회하여 GC의 대상인지 아닌지 식별하도록 하는 것입니다. 

 

 

2.2  GC의 동작 알고리즘

 기본적으로 GC가 실행된다고 하면 아래의 두 가지 알고리즘을 따르게 됩니다. 

2.2.1 Stop the World

 Stop the World는 GC를 실행하기 위해 JVM이 애플리케이션의 실행을 멈추는 것입니다. GC가 실행될 때는 GC를 실행하는 Thread를 제외한 모든 Thread들의 작업이 중단되고, GC가 완료되면 작업이 재개됩니다.

 보통 GC의 성능 개선을 튜닝한다고 하면 Stop-the-world의 시간을 줄이는 작업을 하는 것입니다.

 

2.2.2 Mark and Sweep

 STW에서 모든 작업을 중단시키면 GC는 접근 가능한 객체를 스캔하는데 Mark라는 사용, 비사용 메모리를 식별하는 작업을 하고, Sweep이라는 Mark단계에서 사용되지 않음으로 식별된 메모리를 해제하는 작업을 하는 것입니다. 이를 Mark and Sweep이라고 합니다. 

 

 

2.3  GC의 동작 방식

 위의 알고리즘에 의거하여 작동하는 여러 GC 방식이 있는데 몇 가지 살펴보겠습니다. 

 

2.3.1 Serial GC

 Serial GC는 Young Generation에서는 앞서 설명한 Mark Sweep 알고리즘대로 수행되고, Old Generation에서도 mark and Sweep 알고리즘이 동일하게 사용되는데 기존의 Mark Sweep 외 Compact(압축)라는 작업이 추가되었습니다. 

 Compaction은 말 그대로 압축을 의미하며 Heap영역을 정리하기 위한 단계로 유효 객체들이 연속되도록 쌓이도록 Heap의 앞 부분부터 채워서 객체가 존재하는 부분과 존재하지 않는 부분으로 나누는 것입니다. 

 이 Serial GC는 서버의 CPU core가 1개일 때 사용하기 위해 개발되었으며, 모든 GC 일을 처리하기 위해 1개의 Thread만을 이용합니다. 

 

2.3.2 Parallel GC

 Parallel GC는 위의 Serial 방식과 비슷하지만 일련의 순서로 처리하는 Serial과 달리 병렬로 처리하는 것에 차이가 있습니다. 

Throughtput(처리량) GC라고도 하며, 여러 개의 Thread를 통해 Parallel(병렬)하게 GC를 수행함으로써 GC의 Overhead를 줄여줍니다. 

 이는 Multi Processor 혹은 Multi Thread 머신에서 중간 규모부터 대규모의 데이터를 처리하는 애플리케이션을 위해 고안되었으며 옵션을통해 애플리케이션의 최대 지연시간 혹은 GC를 수행할 Thread의 갯수를 설정해줄 수 있습니다. 

 

2.3.3 Concurrent Mark & Sweep GC (CMS)

 CMS는 4개의 과정을 거치는 방식입니다. 

먼저 Initial mark 과정에서는 단 시간동안 살아있는 객체를 판별하는데 이 때 STW 알고리즘이 사용됩니다.

Initial mark를 마치면 전 과정에서 확인한 객체들이 참조하고 있는 객체들을 따라가면서 확인하는 Concurrent mark 과정을 거칩니다. 이때는 STW 알고리즘이 적용되지 않고 확인하게 됩니다.

 이 후, Concurrent mark 과정에서 새로 추가되거나 참조가 끊어진 객체를 확인하는 Remark 과정을 거치는데 STW 알고리즘이 적용됩니다. 마지막으로는 Concurrent Sweep으로 말그대로 청소라는 뜻으로 쓰레기 객체를 정리하는 과정으로 STW 알고리즘이 적용되지 않습니다. 

 CMS 방식은 STW시간이 매우 짧아서 애플리케이션의 응답속도가 중요할 때 사용하며 이를 Low Latency GC(지연시간 최소)라고도 합니다. 

 빠른 응답속도라는 장점에 반해 다른 GC보다 메모리와 CPU 점유율이 높고 compaction 단계가 제공되지 않는 단점을 갖고 있습니다.

시스템이 장기적으로 운영되다 조각난 메모리들이 많아지게 될텐데, 이 때 압축하는 Compaction 단계가 수행된다면 STW시간이 길어지는 문제가 발생할 수 있습니다. 

 

2.3.4 G1(Garbage First) GC

 G1 GC는 장기적으로 문제를 일으킬 수 있는 CMS GC의 문제점을 대체하기 위해 Java7부터 지원된 방식입니다. 

기존의 GC 알고리즘에서는 Heap을 물리적으로 Young, Old 영역으로 나눠 사용했는데, G1 GC는 Eden 이라는 영역에 할당하고 Survivor로 Copy하는 과정을 사용하지만 이를 물리적으로 메모리 공간으로 나누지 않습니다.

그 대신 Region(지역)이라는 개념을 도입하여 Heap을 균등하게 여러 Region으로 나누고 각 Region을 역할과 함께 물리적이 아닌 논리적으로 구문하여 객체를 할당합니다. 

 Eden, Survivor, Old 역할 뿐만 아니라 Humongous(Region 크기의 50%를 초과하는 객체를 저장하는 Region)과 Available/Unused(사용되지 않은 Region)라는 역할을 추가했습니다.

 

 다른 GC와 마찬가지로 Minor와 Major GC로 나누어 수행되는데 살펴보겠습니다.

 

1) Minor GC

 먼저 한 Region에 객체를 할당하다 해당 Region이 꽉 차게되면 다른 Region에 객체를 할당하고 Minor Gc가 실행됩니다. 이 때 Garbage가 가장 많은 (Garbage first) Region을 찾아 Mark and Sweep을 수행합니다.

 Eden Region에서 GC가 수행되면 남은 객체를 Mark하고 메모리를 Sweep합니다. 그리고 남은 객체를 다른 Region으로 이동시키는데

복제되는 Region이 Available/Ununsed Region 이면 Survior Region이 되고 Eden 은 Available/Ununsed Region이 됩니다. 

2) Major GC(Full GC)

 시스템이 계속 운영되다 객체가 너무 많아 메모리를 빠르게 회수 할 수 없을 때, Major GC가 실행됩니다. 

기존의 GC는 모든 Heap 영역에서 GC가 수행되었으며, 그에 따라 처리시간이 오래걸렸는데 G1 GC는 Garbage가 얼마나 많은지 알고 있기 때문에 GC를 수행할 Region을 조합해 해당 Region에 대해서만 GC를 수행합니다. 이 작업은 Concurrent(동시)하게 수행되기 때문에 애플레케이션의 지연도 최소화 할 수 있습니다. 

 

 이러한 구조의 G1 GC는 다른 GC방식보다 처리속도가 빠르며 큰 메모리 공간에서 Multi Process 기반으로 운영되는 애플리케이션을 위해 고안되었습니다. 처리속도가 빠르기 때문에 Java9부터는 기본 GC로 사용되게 되었습니다. 

 


3. 마치며

 사실 이 글은 자세히 설명한것도 아니고 표면적으로 필요한 설명만 기술한 것인데 이정도로 방대한 양이 될 줄 몰랐습니다. GC의 알고리즘이나 JVM에 대해 더 자세히 쓸 수 있는데, 필요 이상인 것 같아서 나중에 사용하거나 적용할 일이 생기면 응용하면서 설명해보겠습니다. 

 

 

 

 

 

 

 

 

 

참조

'Skills > Java' 카테고리의 다른 글

Java - equals(), hashcode() 메서드의 사용  (0) 2022.10.01
Java - 바이트 코드  (0) 2022.09.20
Java - static import  (0) 2022.08.29
Java - 추상 클래스와 인터페이스  (0) 2022.08.23
Java - ConcurrentHashMap  (0) 2022.08.21