SMALL
  • Garbage Collection에 대한 소개 
  • Garbage Collection이 발생하는 곳
    • young, old영역
    • 왜 이렇게 나누었나
  • Young영역의 Garbage Collection 방식 
  • Old영역의 Garbage Collection 방식 

Garbage Collection

Java 이전에 C, C++에서는 OS 레벨의 메모리에 직접 접근하기 때문에 free()라는 메서드를 호출하여 할당받았던 메모리를 명시적으로 해제해주어야 한다. 그렇지 않으면 memory leak 이 발생하게 된다.

반면, 자바는 OS 의 메모리 영역에 직접적으로 접근하지 않고 JVM이라는 가상 머신을 이용해서 간접적으로 접근한다. JVM 은 C로 쓰인 또 다른 프로그램인데, 오브젝트가 필요해지지 않는 시점에서 알아서 free()를 수행하여 메모리를 확보한다. 

 

Java는 프로그램 실행시 JVM 옵션을 주어서 OS에 요청한 사이즈만큼의 메모리를 할당받아서 실행하게 된다. 할당받은 이상의 메모리를 사용하게 되면 에러가 나면서 자동으로 프로그램이 종료된다. 그러므로 현재 프로세스에서 메모리 누수가 발생하더라도 현재 실행 중인 것만 죽고, 다른 것에는 영향을 주지 않는다.

 

개발자는 Heap을 사용할 수 있는 만큼 자유롭게 사용하고, 더 이상 사용되지 않는 오브젝트들은 가비지 컬렉션을 담당하는 thread가 자동으로 메모리에서 제거하도록 하는 것이 가비지 컬렉션의 기본 개념이다.

 

자바는 가비지 컬렉션에 아주 단순한 규칙을 적용한다.

- Heap 영역의 오브젝트 중 stack 에서 도달 불가능한 (Unreachable) 오브젝트들은 가비지 컬렉션의 대상이 된다.

 

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

stop-the-world란, GC을 실행하기 위해 JVM이 애플리케이션 실행을 멈추는 것이다. stop-the-world가 발생하면 GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈춘다. GC 작업을 완료한 이후에야 중단했던 작업을 다시 시작한다. 


Garbage Collection이 발생하는 곳

HotSpot VM (Oracle이라는 Vender에서 만든 JVM의 이름)에서는 크게 2개로 물리적 공간을 나누었다. 둘로 나눈 공간이 Young 영역과 Old 영역이다.

 

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

 

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

 

이러한 형식으로 영역을 나눈 이유는 "weak generational hypothesis"라는 가정( 또는 전제조건)이 있기 때문이다.

  • 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다
  • 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다

Young영역의 Garbage Collection 방식 

그럼 다음으로 각각의 영역의 GC가 어떻게 이루어 지는지 알아보도록 하자

Young 영역(Yong Generation 영역)은 다시 3 영역으로 나뉜다. 

Eden 영역, Survivor 영역 (2개)

각 영역의 처리 절차를 순서에 따라서 기술하면 다음과 같다

  • 새로 생성한 대부분의 객체는 Eden 영역에 위치
  • Eden 영역에서 GC가 한 번 발생한 후 살아남은 객체는 Survivor 영역 중 하나로 이동
  • Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓인다.
  • 하나의 Survivor 영역이 가득 차게 되면 그중에서 살아남은 객체를 다른 Survivor 영역으로 이동한다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태
  • 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 된다.
Stop and Copy Algorithm : 
힙 메모리를 'active'와 'inactive' 지역으로 나눈다. 각 지역 중, 한 지역에 메모리를 할당하다가(active region) 메모리가 꽉 차게 되면 살아있는 객체들만 다른 지역(inactive region)으로 복사하고 쓰레기를 수집한다. 살아있는 객체들을 복사하는 과정에서 메모리를 연속적으로 저장하기 때문에 파편화를 피할 수 있게 된다. 이후, 살아있는 객체들이 복사된 지역이 active region이 되어 역할이 바뀌고, 이 과정을 반복한다.

출처:(https://d2.naver.com/helloworld/1329)

 

Old영역의 Garbage Collection 방식 

Old 영역(Old Generation 영역)은 기본적으로 데이터가 가득 차면 GC를 실행한다. GC 방식에 따라서 처리 절차가 달라진다.

 

Serial GC (-XX:+UseSerialGC)

Old 영역의 GC는 mark-sweep-compact이라는 알고리즘을 사용한다.

  • Old 영역에 살아 있는 객체를 식별(Mark)하는 것이다.
  • 그다음에는 힙(heap)의 앞부분부터 확인하여 살아 있는 것만 남긴다(Sweep).
  • 마지막 단계에서는 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눈다(Compaction). 

Serial GC는 적은 메모리와 CPU 코어 개수가 적을 때 적합한 방식이다.

(Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식이다. Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어진다 )

 

Parallel GC (-XX:+UseParallelGC)

Parallel GC는 Serial GC와 기본적인 알고리즘은 같다. 그러나 Serial GC는 GC를 처리하는 스레드가 하나인 것에 비해, Parallel GC는 GC를 처리하는 스레드가 여러 개다. 그렇기 때문에 Serial GC보다 빠른 게 객체를 처리할 수 있다. Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리하다. Parallel GC는 Throughput GC라고도 부른다.

 

CMS GC (-XX:+UseConcMarkSweepGC)

  • Initial Mark 단계에서는 클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것으로 끝낸다.
    따라서, 멈추는 시간은 매우 짧다.
  • Concurrent Mark 단계에서는 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다.
    이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것이다.
  • Remark 단계에서는 Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
  • Concurrent Sweep 단계에서는 쓰레기를 정리하는 작업을 실행한다.
    이 작업도 다른 스레드가 실행되고 있는 상황에서 진행한다. 

이러한 단계로 진행되는 GC 방식이기 때문에 stop-the-world 시간이 매우 짧다. 모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며, Low Latency GC라고도 부른다.
그런데 CMS GC는 stop-the-world 시간이 짧다는 장점에 반해 다음과 같은 단점이 존재한다.

다른 GC 방식보다 메모리와 CPU를 더 많이 사용한다.
Compaction 단계가 기본적으로 제공되지 않는다.
따라서, CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 한다. 그리고 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC 방식의 stop-the-world 시간보다 stop-the-world 시간이 더 길기 때문에 Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 한다.

 

G1 GC

마지막으로 G1(Garbage First) GC에 대해서 알아보자. G1 GC를 이해하려면 지금까지의 Young 영역과 Old 영역에 대해서는 잊는 것이 좋다.

G1 GC는 바둑판의 각 영역에 객체를 할당하고 GC를 실행한다. 그러다가, 해당 영역이 꽉 차면 다른 영역에서 객체를 할당하고 GC를 실행한다. 즉, 지금까지 설명한 Young의 세 가지 영역에서 데이터가 Old 영역으로 이동하는 단계가 사라진 GC 방식이라고 이해하면 된다. G1 GC는 CMS GC를 대체하기 위해서 만들어졌다.

G1 GC의 가장 큰 장점은 성능이다. 지금까지 설명한 어떤 GC 방식보다도 빠르다.

LIST
SMALL

JVM의 구성요소 중에 Execution Engine에 해당하는 내용을 살펴보도록 하겠다

  • Execution Engine이란
  • Execution Engine의 구성요소
    • Interpreter
    • JIT(Just In Time) Compiler
    • Garbage Collector

 


Execution Engine이란

 

JVM은 Java 컴파일러가 컴파일한 ByteCode를 ClassLoader를 이용해 메모리(RuntimeDataArea)에 실행 가능한 상태로 적재한다.

Execution Engine은 메모리(RuntimeDataArea)에 할당된 byteCode를 실행하는 역할을 담당한다.

ByteCode는 human-readable형태로 나타내어진다, 그러므로 Execution Engine을 통해서 Machine이 읽을 수 있는 형태로 ByteCode를 변환해 주어야 한다. 

Execution Engine은 ByteCode를 한 줄 마다 읽으며 변환하는 작업을 거친다. 

( CPU가 command를 실행하는 방식과 유사하게 동작)

( 각 command의 ByteCode의 구성은 1byte의 OpCode와 추가적인 Operand로 구성되었다) 

다만, Execution Engine이 어떠한 형태로 실행되여야 하는지는 JVM specification에 나타나지 않아, 해당 구현은 vendor에 따라 달라질 수 있다. 


Execution Engine의 구성요소

Interpreter

ByteCode를 기계가 이해할 수 있도록 Native Code로 바꾸는 작업을 한다

ByteCode 한 줄마다 컴파일을 하여 Native로 변환하는 작업을 하게 되는데 중복되는 Byte Code들에 대해서도 매번 컴파일을 하게 되면 비효율적이며 Running Time도 길어지게 된다

이러한 중복되는 Byte Code에 대해서는 JIT 컴파일러를 사용한다.

 

JIT(Just In Time) Compiler

Interpreter 효율을 높히기 위해 Interpreter가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 Native Code로 바꾼다.
그렇게 되면 반복된 Byte Code는 Native Code로 바뀌어 있기 때문에 Interpreter가 바로 사용할 수 있게 된다.

 

Garbage Collector

RuntimeDataArea의 Heap 영역의 더 이상 참조되지 않는 객체를 정리한다

이에 대한 내역은 Garbage Collector를 자세히 다루는 포스팅으로 소개하도록 하겠다.

 

LIST
SMALL

Java에서는 Date 표현에 해당하는 다양한 Class들을 제공한다.

대표적으로는 java.util.Date Class와 java.time.LocalDateTime Class가 있을 수 있다.

 

각각의 Class에 특징에 대해서 살펴보고, 정리해 보도록 하자

 

java.util.Date 클래스는 JDK 1.1 버전부터 지원하는 클래스였지만 다양한 문제가 제기되면서, 많은 기능들이 deprecated 되었다. 

java.util.Date 클래스의 문제점을 간단히 정리해 보면 아래와 같다 

  • 불변 객체가 아니다
  • int 상수 필드의 남용
  • 헷갈리는 월지정 및 일관성 없는 요일 상수

이 중 가장 불편한 점은 Mutable 객체라는 점이다. Date의 값이 변경되면 멀티 스레드 환경에서 불안정성 등의 부작용이 나타날 수 있게 된다. C#, Python 같은 언어에서는 날짜 클래스가 한번 생성된 이후에는 내부 속성을 바꿀 수 없다.

 

실제 Date 클래스를 사용하는 코드를 살펴보자

Date date_now = new Date(System.currentTimeMillis()); // 현재시간을 가져와 Date형으로 저장한다
System.out.println(date_now); // 기본 포멧으로 출력한다
		
SimpleDateFormat fourteen_format = new SimpleDateFormat("yyyyMMddHHmmss"); 
String str_source = fourteen_format.format(date_now); // 14자리 포멧으로 출력한다
System.out.println(str_source); 
		
SimpleDateFormat input_format    = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); // 입력포멧
Date date_parsed = fourteen_format.parse(str_source); // 문자열을 파싱해 Date형으로 저장한다
String str_parsed = input_format.format(date_parsed);
System.out.println(str_parsed);

 

JDK 1.8버전에서 java.time 패키지에 LocalDateTime과 타임존 개념까지 포함할 수 있는 ZonedDateTime이 추가되었다. 이 패키지에서 제공하는 모든 class들은 immutable 하기 때문에 thread-safe 하게 사용 가능하다.

 

실제 LocalDateTime 클래스를 사용하는 코드를 살펴보자

LocalDateTime nowDateTime = LocalDateTime.now();
// 2018년 12월 11일 15시 23분 32초, 2018-12-11T15:23:32
LocalDateTime ofDateTime = LocalDateTime.of(2018, 12, 11, 15, 23, 32);
System.out.println(ofDateTime);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
String formatted = formatter.format(ofDateTime);
formatted = ofDateTime.format(formatter);
LocalDateTime parsedDate = LocalDateTime.parse(formatted, formatter);

 

LIST

+ Recent posts