JVM 성능 최적화, Part 3 : 가비지 콜렉션

Java 플랫폼의 가비지 수집 메커니즘은 개발자 생산성을 크게 향상 시키지만 제대로 구현되지 않은 가비지 수집기는 애플리케이션 리소스를 과도하게 소비 할 수 있습니다. JVM 성능 최적화 시리즈 의 세 번째 기사에서 Eva Andreasson은 Java 초보자에게 Java 플랫폼의 메모리 모델 및 GC 메커니즘에 대한 개요를 제공합니다. 그런 다음 조각화 (GC가 아님)가 주요 "문제!"인 이유를 설명합니다. Java 애플리케이션 성능과 세대 별 가비지 수집 및 압축이 현재 Java 애플리케이션에서 힙 조각화를 관리하는 선도적 인 (가장 혁신적이지는 않지만) 접근 방식 인 이유.

가비지 콜렉션 (GC)은 도달 가능한 Java 오브젝트에서 더 이상 참조하지 않는 점유 메모리를 확보하기위한 프로세스이며 JVM (Java Virtual Machine)의 동적 메모리 관리 시스템의 필수 부분입니다. 일반적인 가비지 수집주기에서는 여전히 참조되어 도달 할 수있는 모든 개체가 유지됩니다. 이전에 참조 된 개체가 차지하는 공간은 새 개체 할당이 가능하도록 해제 및 재생됩니다.

가비지 수집과 다양한 GC 접근 방식 및 알고리즘을 이해하려면 먼저 Java 플랫폼의 메모리 모델에 대해 몇 가지 알아야합니다.

JVM 성능 최적화 : 시리즈 읽기

  • 1 부 : 개요
  • 2 부 : 컴파일러
  • Part 3 : 가비지 수집
  • 파트 4 : GC 동시 압축
  • 5 부 : 확장 성

가비지 수집 및 Java 플랫폼 메모리 모델

-XmxJava 애플리케이션의 명령 줄 에서 시작 옵션을 지정하면 (예 :) java -Xmx:2g MyApp메모리가 Java 프로세스에 할당됩니다. 이 메모리를 Java 힙 (또는 단순히 )이라고합니다. 이것은 Java 프로그램 (또는 때때로 JVM)에 의해 생성 된 모든 객체가 할당되는 전용 메모리 주소 공간입니다. Java 프로그램이 계속 실행되고 새 객체를 할당하면 Java 힙 (해당 주소 공간을 의미)이 채워집니다.

결국 Java 힙이 가득 차게됩니다. 즉, 할당 스레드가 할당하려는 개체에 대해 충분히 큰 연속 메모리 섹션을 찾을 수 없습니다. 이 시점에서 JVM은 가비지 수집이 발생해야한다고 결정하고 가비지 수집기에 알립니다. 가비지 콜렉션은 Java 프로그램이 호출 할 때 트리거 될 수도 있습니다 System.gc(). 사용System.gc()가비지 콜렉션을 보장하지 않습니다. 가비지 수집을 시작하기 전에 GC 메커니즘은 먼저 시작해도 안전한지 여부를 결정합니다. 애플리케이션의 모든 활성 스레드가 허용 할 수있는 안전한 지점에있을 때 가비지 수집을 시작하는 것이 안전합니다. 컨텍스트를 잃어 최종 결과를 엉망으로 만들 수 있으므로 최적화 된 CPU 명령 시퀀스 (컴파일러에 대한 이전 기사 참조)를 실행합니다.

가비지 수집기는 능동적으로 참조 된 개체를 회수 해서는 안됩니다 . 그렇게하면 Java 가상 머신 사양이 깨집니다. 또한 가비지 수집기는 죽은 개체를 즉시 수집하는 데 필요하지 않습니다. 죽은 개체는 결국 후속 가비지 수집주기 동안 수집됩니다. 가비지 컬렉션을 구현하는 방법은 여러 가지가 있지만이 두 가지 가정은 모든 종류에 적용됩니다. 가비지 수집의 진정한 도전은 살아있는 (여전히 참조 된) 모든 것을 식별하고 참조되지 않은 메모리를 회수하지만 실행중인 애플리케이션에 더 이상 영향을주지 않으면 서 그렇게하는 것입니다. 따라서 가비지 수집기에는 두 가지 권한이 있습니다.

  1. 메모리가 부족하지 않도록 응용 프로그램의 할당 속도를 충족시키기 위해 참조되지 않은 메모리를 빠르게 해제합니다.
  2. 실행중인 애플리케이션의 성능 (예 : 대기 시간 및 처리량)에 최소한의 영향을 주면서 메모리를 회수합니다.

두 종류의 가비지 수집

이 시리즈의 첫 번째 기사에서는 가비지 수집에 대한 두 가지 주요 접근 방식, 즉 참조 계산 및 추적 수집기에 대해 설명했습니다. 이번에는 각 접근 방식을 자세히 살펴본 다음 프로덕션 환경에서 추적 수집기를 구현하는 데 사용되는 몇 가지 알고리즘을 소개합니다.

JVM 성능 최적화 시리즈 읽기

  • JVM 성능 최적화, Part 1 : 개요
  • JVM 성능 최적화, Part 2 : 컴파일러

참조 계수 수집가

참조 계산 수집기 는 각 Java 개체를 가리키는 참조 수를 추적합니다. 개체의 개수가 0이되면 메모리를 즉시 회수 할 수 있습니다. 회수 된 메모리에 대한 이러한 즉각적인 액세스는 가비지 수집에 대한 참조 계산 방식의 주요 이점입니다. 참조되지 않은 메모리를 유지할 때 오버 헤드가 거의 없습니다. 그러나 모든 참조 수를 최신으로 유지하는 것은 비용이 많이들 수 있습니다.

레퍼런스 카운팅 수집가의 가장 큰 어려움은 레퍼런스 카운트를 정확하게 유지하는 것입니다. 잘 알려진 또 다른 문제는 원형 구조 처리와 관련된 복잡성입니다. 두 개체가 서로를 참조하고 라이브 개체가 참조하지 않는 경우 해당 메모리는 절대 해제되지 않습니다. 두 개체 모두 0이 아닌 개수로 영원히 유지됩니다. 순환 구조와 관련된 메모리를 회수하려면 주요 분석이 필요하므로 알고리즘과 애플리케이션에 비용이 많이 드는 오버 헤드가 발생합니다.

수집가 추적

추적 수집기 는 라이브 개체로 알려진 초기 집합에서 모든 참조와 후속 참조를 반복적으로 추적하여 모든 라이브 개체를 찾을 수 있다는 가정을 기반으로합니다. 라이브 오브젝트 (전화의 초기 설정 루트 객체 하거나 뿌리 줄여서)는 가비지 컬렉션이 트리거 될 때 순간에 등록, 글로벌 필드 및 스택 프레임을 분석하여 있습니다. 초기 라이브 세트가 식별 된 후 추적 수집기는 이러한 개체의 참조를 따라 가며 라이브로 표시되도록 대기열에 추가 한 다음 해당 참조를 추적합니다. 발견 된 모든 참조 개체를 라이브로 표시알려진 라이브 세트가 시간이 지남에 따라 증가 함을 의미합니다. 이 프로세스는 참조 된 (따라서 모든 라이브) 개체를 찾아 표시 할 때까지 계속됩니다. 추적 수집기가 모든 활성 개체를 찾으면 남은 메모리를 회수합니다.

추적 수집기는 원형 구조를 처리 할 수 ​​있다는 점에서 참조 계수 수집기와 다릅니다. 대부분의 추적 수집기의 문제는 마킹 단계로, 참조되지 않은 메모리를 회수 할 수 있기 전에 대기해야합니다.

추적 수집기는 동적 언어의 메모리 관리에 가장 일반적으로 사용됩니다. 이들은 Java 언어에서 가장 일반적이며 수년 동안 프로덕션 환경에서 상업적으로 입증되었습니다. 가비지 수집에 대한이 접근 방식을 구현하는 알고리즘 중 일부부터 시작하여이 기사의 나머지 부분에서 수집기를 추적하는 데 중점을 둘 것입니다.

수집기 알고리즘 추적

복사표시 및 청소 가비지 수집은 새로운 것은 아니지만 오늘날에도 추적 가비지 수집을 구현하는 가장 일반적인 두 가지 알고리즘입니다.

수집가 복사

전통 복사 집은 사용 에서 공간우주 , 힙이 개 개별적으로 정의 된 주소 공간이다 -. 가비지 콜렉션 시점에서 시작 공간으로 정의 된 영역 내의 라이브 오브젝트는 종료 공간으로 정의 된 영역 내의 다음 사용 가능한 공간으로 복사됩니다. 시작 공간 내의 모든 라이브 개체가 밖으로 이동하면 전체 시작 공간을 회수 할 수 있습니다. 할당이 다시 시작되면 To-space의 첫 번째 빈 위치에서 시작됩니다.

이 알고리즘의 이전 구현에서는 from-space 및 to-space 스위치가 배치됩니다. 즉, to-space가 가득 차면 가비지 콜렉션이 다시 트리거되고 to-space가 시작 공간이됩니다 (그림 1 참조).

복사 알고리즘의보다 현대적인 구현은 힙 내의 임의의 주소 공간을 To-space 및 From-space로 할당 할 수 있도록합니다. 이 경우 서로 위치를 전환 할 필요가 없습니다. 오히려 각각은 힙 내에서 또 다른 주소 공간이됩니다.

수집기를 복사 할 때의 한 가지 장점은 개체가 To-space에서 단단히 함께 할당되어 조각화를 완전히 제거한다는 것입니다. 조각화는 다른 가비지 수집 알고리즘이 어려움을 겪는 일반적인 문제입니다. 이 기사의 뒷부분에서 논의 할 내용입니다.

컬렉터 복사의 단점

복사 수집기는 일반적으로 stop-the-world 수집기입니다 . 즉, 가비지 수집이주기에있는 동안에는 응용 프로그램 작업을 실행할 수 없습니다. stop-the-world 구현에서 복사해야하는 영역이 클수록 애플리케이션 성능에 미치는 영향이 더 커집니다. 이는 응답 시간에 민감한 애플리케이션의 단점입니다. 복사 수집기를 사용하면 모든 것이 시작 공간에있는 최악의 시나리오도 고려해야합니다. 항상 라이브 오브젝트를 이동할 수있는 충분한 여유 공간을 남겨야합니다. 즉, To-space는 시작 공간의 모든 것을 호스팅 할 수있을만큼 충분히 커야합니다. 복사 알고리즘은 이러한 제약으로 인해 약간의 메모리 비효율적입니다.

마크 앤 스윕 수집가

엔터프라이즈 프로덕션 환경에 배포 된 대부분의 상업용 JVM은 표시 및 청소 (또는 표시) 수집기를 실행하므로 복사 수집기가 수행하는 성능 영향이 없습니다. 가장 유명한 마킹 수집가 중 일부는 CMS, G1, GenPar 및 DeterministicGC입니다 (참고 자료 참조).

마크 앤 스윕 컬렉터 흔적 참조 마크는 "라이브"비트가 발견 된 각 객체입니다. 일반적으로 세트 비트는 주소 또는 경우에 따라 힙의 주소 세트에 해당합니다. 예를 들어 라이브 비트는 객체 헤더, 비트 벡터 또는 비트 맵에 비트로 저장 될 수 있습니다.

모든 것이 라이브로 표시되면 스위프 단계가 시작됩니다. 수집기에 스위프 단계가있는 경우 기본적으로 표시되지 않은 모든 항목을 찾기 위해 힙 (라이브 세트뿐 아니라 전체 힙 길이)을 다시 탐색하는 메커니즘이 포함됩니다. 연속적인 메모리 주소 공간의 청크. 표시되지 않은 메모리는 무료이며 회수 가능합니다. 그런 다음 수집기는 이러한 표시되지 않은 청크를 정리 된 무료 목록으로 함께 연결합니다. 가비지 수집기에는 일반적으로 청크 크기로 구성되는 다양한 사용 가능 목록이있을 수 있습니다. 일부 JVM (예 : JRockit Real Time)은 애플리케이션 프로파일 링 데이터 및 개체 크기 통계를 기반으로 목록의 크기를 동적으로 조정하는 휴리스틱을 사용하여 수집기를 구현합니다.

스위프 단계가 완료되면 할당이 다시 시작됩니다. 새로운 할당 영역은 사용 가능한 목록에서 할당되며 메모리 청크는 개체 크기, 스레드 ID 당 개체 크기 평균 또는 응용 프로그램 조정 TLAB 크기와 일치 할 수 있습니다. 사용 가능한 공간을 응용 프로그램이 할당하려는 크기에 더 가깝게 맞추면 메모리가 최적화되고 조각화를 줄이는 데 도움이 될 수 있습니다.

TLAB 크기에 대한 추가 정보

TLAB 및 TLA (Thread Local Allocation Buffer 또는 Thread Local Area) 파티셔닝은 JVM 성능 최적화, Part 1에서 설명합니다.

마크 앤 스윕 수집가의 단점

표시 단계는 힙에있는 라이브 데이터의 양에 따라 다르지만 스윕 단계는 힙 크기에 따라 다릅니다. 메모리를 회수 하려면 마크스윕 단계가 모두 완료 될 때까지 기다려야 하므로이 알고리즘은 더 큰 힙과 더 큰 라이브 데이터 세트에 대해 일시 중지 시간 문제를 유발합니다.

메모리를 많이 사용하는 애플리케이션을 도울 수있는 한 가지 방법은 다양한 애플리케이션 시나리오와 요구를 수용하는 GC 조정 옵션을 사용하는 것입니다. 대부분의 경우 튜닝은 이러한 단계 중 하나가 애플리케이션 또는 SLA (서비스 수준 계약)에 대한 위험이되지 않도록 최소한 연기하는 데 도움이 될 수 있습니다. (SLA는 애플리케이션이 특정 애플리케이션 응답 시간 (예 : 대기 시간)을 충족하도록 지정합니다.) 모든로드 변경 및 애플리케이션 수정에 대한 튜닝은 반복적 인 작업이지만 튜닝은 특정 워크로드 및 할당 속도에 대해서만 유효합니다.

마크 앤 스윕 구현

마크 앤 스윕 (mark-and-sweep) 수집을 구현하기 위해 상업적으로 이용 가능하고 입증 된 접근 방식이 두 가지 이상 있습니다. 하나는 병렬 접근 방식이고 다른 하나는 동시 (또는 대부분 동시) 접근 방식입니다.

병렬 수집기

병렬 수집 은 프로세스에 할당 된 리소스가 가비지 수집을 위해 병렬로 사용됨을 의미합니다. 대부분의 상업적으로 구현 된 병렬 수집기는 모 놀리 식 stop-the-world 수집기입니다. 전체 가비지 수집주기가 완료 될 때까지 모든 애플리케이션 스레드가 중지됩니다. 모든 스레드를 중지하면 모든 리소스를 병렬로 효율적으로 사용하여 표시 및 스윕 단계를 통해 가비지 수집을 완료 할 수 있습니다. 이는 매우 높은 수준의 효율성으로 이어지며 일반적으로 SPECjbb와 같은 처리량 벤치 마크에서 높은 점수를 얻습니다. 처리량이 애플리케이션에 필수적인 경우 병렬 접근 방식이 탁월한 선택입니다.