SMALL

Stream이란 

Stream 구조 및 메서드 소개

  • 생성
  • 중간 연산자
  • 최종 연산자 

Stream 특징


Stream이란 

스트림(Streams)은 자바 8에서 추가되었고, 람다를 활용할 수 있는 기술 중 하나입니다.

Stream은 '데이터의 흐름’입니다.

배열 또는 컬렉션 인스턴스에 함수 여러 개를 조합해서 원하는 결과를 필터링하고 가공된 결과를 얻을 수 있습니다. 

또한 람다를 이용해서 코드의 양을 줄이고 간결하게 표현할 수 있습니다. 

즉, 배열과 컬렉션을 함수형으로 처리할 수 있습니다.


Stream 구조 및 메서드 소개 

Stream 구조는 크게 3가지로 나뉜다. 

스트림 생성->  중개 연산 ->  최종 연산

실제 사용법으로 표기하면 "Collections 같은 객체 집합.스트림생성().중개연산().최종연산();" 이런 식이다.

( 계속해서 . 으로 연계할 수 있게 하는 방법을 파이프라인이라고도 한다 )

 

그럼 다음으로 각각의 구조에 해당하는 연산자들이 어떠한 것들이 있는지 대표적으로 몇 가지를 살펴보자 

 

스트림 생성

보통 배열과 컬렉션을 이용해서 스트림을 만든다.

이 외에도 다양한 방법으로 스트림을 만들 수 있습니다.

String[] arr = new String[]{"a", "b", "c"};
Stream<String> stream = Arrays.stream(arr);

List<String> list = Arrays.asList("a", "b", "c");
Stream<String> stream = list.stream();

 

Stream.builder()
빌더(Builder)를 사용하면 스트림에 직접적으로 원하는 값을 넣을 수 있습니다. 

마지막에 build 메서드로 스트림을 리턴합니다.

 

Stream.generate()
generate 메서드를 이용하면 Supplier<T> 에 해당하는 람다로 값을 넣을 수 있습니다. 

Supplier<T> 는 인자는 없고 리턴 값만 있는 함수형 인터페이스 -> Supplier에서 리턴하는 값이 Stream으로 들어갑니다. 

 * 이때 생성되는 스트림은 크기가 정해져 있지 않고 무한(infinite) 하기 때문에 특정 사이즈로 최대 크기를 제한 * 

 

Stream.iterate()
iterate 메서드를 이용하면 초기값과 해당 값을 다루는 람다를 이용해서 스트림에 들어갈 요소를 만듭니다. 

Stream 생성 시, 요소가 다음 요소의 인풋으로 들어갑니다.

 * 이때 생성되는 스트림은 크기가 정해져 있지 않고 무한(infinite) 하기 때문에 특정 사이즈로 최대 크기를 제한 * 

Stream<String> builderStream = 
  Stream.<String>builder()
    .add("Eric").add("Elena").add("Java")
    .build(); 
//output - [Eric, Elena, Java]

Stream<String> generatedStream = 
  Stream.generate(() -> "el").limit(5);
//output - [el, el, el, el, el]

Stream<Integer> iteratedStream = 
  Stream.iterate(30, n -> n + 2).limit(5); 
//output - [30, 32, 34, 36, 38]

중간 연산 

값을 원하는 형태로 처리하기 위한 연산자이다. 

각각의 중간 연산자 결과로 stream을 반환한다.

그렇기 때문에 중간 연산자는 method chaining 형태로 연결하여 처리할 수 있다.

연산의 결과가 stream으로 반환되기 때문에 stream-producing 연산자라고 부르기도 한다.

 

대표적인 메서드를 예시로 몇 개 알아보도록 하자 

 

filter()
원하는 요소만 추출하기 위한 메서드이다. 인자로는 Predicate를 받는데, boolean값을 반환하는 람다식을 넣으면 된다.

Stream<T> filter(Predicate<? super T> predicate);

List<String> names = Arrays.asList("Hello", "World", "Test", "array");
List<String> filteredNames = names.stream()
        .filter(it -> it.contains("e"))
        .collect(Collectors.toList());
// output - ["Hello", "Test" ]        

 

map()
스트림 내 요소를 가공한다. 

mapper를 간단히 설명하자면, T를 인자로 받아 변환한 값 R을 반환하는 함수이다. 

이는 람다식으로 간단히 표현할 수 있다.

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

Arrays.asList("Hello", "World", "Test", "array");
        .stream()
        .map(String::toUpperCase)
        .forEach(System.out::println);
// output - [ "HELLO", "WORLD", "TEST", "ARRAY" ]        

 

flatMap() 
중첩 구조를 한 단계 제거하고 단일 컬렉션으로 만들어 주는 역할을 한다.

이러한 작업을 flattening이라고 한다.
map과 가장 큰 차이는 함수의 반환 값이 stream 형태라는 것이다. 

이는 map만으로 처리하면 복잡해지는 코드를 간결하게 만들어준다.

<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper);

int arr[][] = {{1, 2, 3}, {4, 8}, {9, 10, 20}, {11, 22}};
Stream.of(arr)
        .flatMapToInt(IntStream::of)
        .forEach(System.out::println);

 

최종 연산

가공한 스트림을 가지고 내가 사용할 결괏값으로 만들어내는 단계입니다.

따라서 스트림을 끝내는 최종 작업(terminal operations)입니다.

스트림의 요소를 소모해서 결과를 만들어낸다. 따라서 최종 연산 후에는 스트림이 닫히게 되고 더 이상 사용할 수 없다.

 

대표적인 메서드를 예시로 몇 개 알아보도록 하자 

 

reduce() 
스트림은 reduce라는 메서드를 이용해서 결과를 만들어냅니다. 

다음은 reduce 메서드는 총 세 가지의 파라미터를 받을 수 있습니다.

  • accumulator : 각 요소를 처리하는 계산 로직. 각 요소가 올 때마다 중간 결과를 생성하는 로직.
  • identity : 계산을 위한 초기값으로 스트림이 비어서 계산할 내용이 없더라도 이 값은 리턴.
  • combiner : 병렬(parallel) 스트림에서 나눠 계산한 결과를 하나로 합치는 동작하는 로직.
// 1개 (accumulator)
Optional<T> reduce(BinaryOperator<T> accumulator);

// 2개 (identity)
T reduce(T identity, BinaryOperator<T> accumulator);

// 3개 (combiner)
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);

인자가 하나만 있는 경우입니다. 

여기서 BinaryOperator<T> 는 같은 타입의 인자 두 개를 받아 같은 타입의 결과를 반환하는 함수형 인터페이스입니다.

OptionalInt reduced = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce((a, b) -> {
    return Integer.sum(a, b);
  }); // output - 6

두 개의 인자를 받는 경우입니다. 

여기서 첫 번째 인자는 초기값이고, 스트림 내 값을 더해서 결과는 16(10 + 1 + 2 + 3)이 됩니다. 

int reducedTwoParams = 
  IntStream.range(1, 4) // [1, 2, 3]
  .reduce(10, Integer::sum); 
// output - 16

마지막으로 세 개의 인자를 받는 경우입니다. 
Combiner는 병렬 처리 시 각자 다른 스레드에서 실행한 결과를 마지막에 합치는 단계입니다. 따라서 병렬 스트림에서만 동작합니다.

 

collect()  

collect 메서드는 또 다른 종료 작업입니다. Collector 타입의 인자를 받아서 처리를 하는데요, 자주 사용하는 작업은 Collectors 객체에서 제공하고 있습니다.

 

해당 메서드에서 인자로 받는 Collector 타입의 예제를 몇 개 보도록 하자

 

Collectors.toList()

스트림에서 작업한 결과를 담은 리스트로 반환합니다.

List<String> collectorCollection =
  productList.stream()
    .map(Product::getName)
    .collect(Collectors.toList());
// [potatoes, orange, lemon, bread, sugar]

Stream 특징

 

Stream은 재사용이 불가능하다.

한 번 사용한 스트림에 대해서 다시 사용하려고 하면 에러가 난다.

Stream<String> a = names.stream().filter(x -> x.contains("o"));
count = a.count();
List<String> lists = a.collect(Collectors.toList()); // error ! 

병렬 스트림은 여러 스레드가 작업한다.

stream()으로 스트림을 생성하지 않고 위처럼 parallelStream()으로 병렬 스트림을 만들 수 있다.

이렇게 하면 여러 스레드가 스트림에서 요소를 필터링하고 나온 요소 수를 계산하고 스레드끼리 다시 한번 각자 계산한 count 값들을 더해서 리턴해준다.

위에서 설명한 최종 연산의 reduce 메서드와 같이, 우리가 의도한 바와 다른 형태로 동작할 수 있으니 잘 생각해서 사용해야 한다.

 

중개 연산은 미리 하지 않는다 -> 지연 연산을 한다.
최종 연산이 적용될 때 중개 연산도 실행된다.
이로써 얻는 장점은 미리 계산하면서 두 번 순회하는 짓을 안 할 수 있게 된다는 점이다.

 

스트림은 원본 데이터를 변경하지 않는다.
스트림은 데이터를 읽기만 할 뿐, 원본 데이터를 변경하지 않는다.

필요하다면, 정렬된 결과를 컬렉션이나 배열에 담아서 반환할 수 있다.

 

 

참고 블로그 :)

 

LIST
SMALL

람다 표현식을 사용하기 해서는 함수형 인터페이스여야 한다

그렇다면, 함수형 인터페이스가 무엇이고 , 어떠한 것들이 있는지 알아보도록 하자 

 

함수형 인터페이스

 1개의 추상 메소드를 갖고 있는 인터페이스

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

@FunctionalInterface 어노테이션을 통해서 함수형 인터페이스라는 걸 명시적으로 드러낼 수 있다.

또한, IDE에서 해당 어노테이션이 있을 시, 메소드를 추가하려 하는 경우 컴파일 에러를 발생시켜 준다.

 

java.util.function 패키지를 보면 다양한 용도의 표준 함수형 인터페이스가 담겨있다. 

람다식을 사용할 때마다 함수형 인터페이스를 매번 정의하기에는 불편하기 때문에 자바에서 라이브러리로 제공하는 것들이 있다.

자바에서 기본적으로 제공하는 함수형 인터페이스는 다음과 같은 것들이 있다

위에서 설명한 인터페이스 이외에도 다양한 함수형 인터페이스를 자바에서 기본으로 제공한다. 

기본 제공하는 인터페이스의 인자 및 리턴타입은 제네릭이기 때문에 primitive 타입을 사용하고 싶은 경우에는 ReferenceType의 Integer, Double 등등의 타입을 사용하여야 한다. 

 

이러한 부분에서 불필요한 autoBoxing을 제거하기 위해서 기본 특화형 인터페이스를 제공한다. 

  • IntConsumer, LongConsumer, DoubleConsumer...
  • IntFuction <R> IntToDoubleFunction, LongFunction <R>, ToDoubleFunction <T>.. 

이외에도 Bi와 같은 Prefix가 붙은 다양한 인터페이스가 존재하는데 각각의 PreFix의 의미를 살펴보면

  • Bi .. : 인자로 받는 타입이 2개 
  • Unary .. : 같은 타입 인자 1개, 같은 반환형
  • Binary .. : 같은 타입 인자 2개, 같은 반환형

와 같이 유추해볼 수 있다.

LIST
SMALL

Java7에서 Java8으로 넘어가면서, 대표적인 변경 사항은 람다 표현식의 추가를  말할 수 있다.

 

  • 람다 표현식의 정의
  • 왜 추가되었는지
  • 람다 표현식의 특징
    • 함수형 인터페이스
    • 상태가 없는 객체 ( StateLess Object ) 
      • 메서드 vs 함수
    • 행위 파라미터화 ( Behavior Parameterize )

 


람다 표현식의 정의

Lambda Expression은 Anonymous Function이다. 

즉, "익명 함수" 이름이 없는 함수 

풀어서 말하면 식별자 없이 실행 가능한 함수 표현식이라고 할 수 있다.


왜 추가되었는지

Functional Programming Style을 Java에 적용하기 위해서 

Java는 대표적인 객체 지향 언어이다. 하지만 Lambda Expression은 함수 지향 언어에 가깝다.

( 객체 지향 언어에 functional programming style을 적용할 수 있으니 언어의 표현력(?)이 대폭 증가했다고 할 수 있다)

Lambda Expression의 장점은 아래와 같다 

  • 행위 ( 함수 ) 를 파라미터화 할 수 있다 (아래의 람다 표현식의 특징에서 부연 설명)
  • 코드를 간결하게 만들어 가독성을 향상 시킬 수 있다
  • 일반적으로 다중 CPU를 활용하는 형태로 구현되어 병렬 처리에 유리

다 표현식의 특징

람다 표현식의 특성 중 하나는 메서드의 인수로 전달될 수 있고, 변수로 저장될 수 있다는 점입니다.
기본적인 표현의 구성은 아래와 같습니다.

( Persion p1, Persion p2 ) -> p1.getAge().compareTo(p2.getAge())
  • Parameter list: (Persion p1, Persion p2)
  • 화살표: 람다의 파라미터와 바디를 구분
  • Lambda body: 람다의 반환값에 해당하는 표현식
    • (parameters) -> expression
    • (parameters) -> { statements; }

- 함수형 인터페이스

람다 표현식은 구현해야될 추상 메서드가 1개인 인터페이스를 구현한 것이다. 

인터페이스를 구현하고자하는데 어차피 구현해야 될 메서드가 1개뿐이니 이름이고 뭐고 다 지워버린 것이다. 

그럼 메서드가 2개일땐 어떻게 해야 할까? 람다로는 지원하지 않는다. 

람다 표현식으로 구현이 가능한 인터페이스는 오직 추상 메서드가 1개뿐인 인터페이스만 가능하며 그렇기 때문에 추상 메서드가 1개인 인터페이스를 부르는 명칭이 추가됐다. 그것이 함수형 인터페이스다.

@FunctionalInterface
public interface Predicate<T> {
    boolean test(T t);
}

- 상태가 없는 객체(Stateless Object)

익명객체를 구현할 때는 인스턴스 필드를 추가할 수 있다. 

하지만 람다는 바로 메서드를 구현해버리니 인스턴스 필드가 들어갈 공간이 없어 보인다. 

람다 표현식에 인스턴스 필드를 추가하려면 어떻게 해야할까? 방법은 없다.

 

메서드와 함수의 가장 큰 차이는 메서드는 객체에 종속되어있다는 것이다.

함수란 인풋(Input)에 의해서만 아웃풋(Output)이 달라져야 하는데

메서드는 객체에 종속적이기때문에 인풋이 달라지지 않아도 객체의 상태에 따라 결과값이 다를 수 있다.

함수형 프로그래밍에서 함수는 인풋에 의해서만 아웃풋이 달라져야하며 그것을 지원하기 위해 람다 표현식으로 구현할 때 객체는 상태를 가질 수 없다.

Thread th = new Thread(new Runnable(){
	int cnt = 99 ; // State 
    
	@Override
	public void run() {
    		System.out.println("new Thread!! ");
	}
});

- 행위 파라미터화(Behavior Parameterize)

보통 코드를 짤때 데이터를 매개변수로 전달하고 해당 데이터를 가지고 무언가 행위를 하는 메서드를 구현하기 마련이다. 

하지만 데이터를 전달하는 것이 아닌 행위를 전달하게 되면 좀 더 유연한 코드가 될 수 있다 ( 람다 표현식의 특징! )

    List<Product> filteredByName = filterByName(products, "새우깡");
    List<Product> filterByPrice = filterByPrice(products, 1000);

    public static List<Product> filterByName(List<Product> products, String name) {
        List<Product> filteredProducts = new ArrayList<>();
        for (Product product : products) {

            if (product.getName().equals(name)) { // filter ! 
                filteredProducts.add(product);
            }
        }

        return filteredProducts;
    }

    public static List<Product> filterByPrice(List<Product> products, int price) {
        List<Product> filteredProducts = new ArrayList<>();
        for (Product product : products) {

            if (product.getPrice() <= price) { // filter ! 
                filteredProducts.add(product);
            }
        }
        return filteredProducts;
    }

위의 코드는 일반적인 데이터를 매개변수로 전달하는 코드이다. 

 

public interface FilterPredicate {
    public abstract boolean filter(Product product);
}

List<Product> filteredByName011 = filter(products, p -> p.getName().equals("새우깡"));
List<Product> filterByPrice011 = filter(products,
    p -> p.getName().equals("새우깡") && p.getStore().equals("owner"));

public static ArrayList<Product> filter(ArrayList<Product> products, FilterPredicate filterInterface) {
    ArrayList<Product> filteredProducts = new ArrayList<>();

    for (Product product : products) {
        if (filterInterface.filter(product)) { // point ! 
            filteredProducts.add(product);
        }
    }
    return filteredProducts;
}

비교하는 행위를 파라미터로 입력받아 if문 내 조건을 하드코딩에서 동적으로 처리

 

 

 

 

참고 블로그 :)

 

LIST
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

+ Recent posts