Java 플랫폼의 가비지 수집 메커니즘은 개발자 효율성을 크게 향상시켰지만 제대로 구현되지 않은 가비지 수집기는 너무 많은 애플리케이션 리소스를 소비할 수 있습니다. Java 가상 머신 성능 최적화 시리즈의 세 번째 부분에서 Eva Andreasson은 Java 초보자에게 Java 플랫폼의 메모리 모델과 가비지 수집 메커니즘을 소개합니다. 그녀는 왜 단편화(가비지 수집이 아님)가 Java 애플리케이션 성능의 주요 문제인지, 그리고 세대별 가비지 수집 및 압축이 현재 Java 애플리케이션 단편화를 처리하는 주요(가장 혁신적이지는 않은) 방법인 이유를 설명합니다.
GC(가비지 수집)의 목적은 활성 개체에서 더 이상 참조하지 않는 Java 개체가 차지하는 메모리를 해제하는 것입니다. 이는 Java 가상 머신의 동적 메모리 관리 메커니즘의 핵심 부분입니다. 일반적인 가비지 수집 주기 동안 여전히 참조되는(따라서 연결 가능한) 모든 개체는 유지되는 반면, 더 이상 참조되지 않는 개체는 해제되고 해당 개체가 차지하는 공간은 새 개체에 할당되기 위해 회수됩니다.
가비지 수집 메커니즘과 다양한 가비지 수집 알고리즘을 이해하려면 먼저 Java 플랫폼 메모리 모델에 대해 알아야 합니다.
가비지 수집 및 Java 플랫폼 메모리 모델
명령줄에서 Java 프로그램을 시작하고 시작 매개변수 -Xmx(예: java -Xmx:2g MyApp)를 지정하면 지정된 크기의 메모리가 소위 Java 힙인 Java 프로세스에 할당됩니다. . 이 전용 메모리 주소 공간은 Java 프로그램(경우에 따라 JVM)에서 생성된 개체를 저장하는 데 사용됩니다. 애플리케이션이 실행되고 새 개체에 대한 메모리를 지속적으로 할당하면 Java 힙(즉, 전용 메모리 주소 공간)이 천천히 채워집니다.
결국 Java 힙이 가득 차게 되는데, 이는 메모리 할당 스레드가 새 개체에 메모리를 할당할 만큼 큰 연속 공간을 찾을 수 없음을 의미합니다. 이때 JVM은 가비지 수집기에 알리고 가비지 수집을 시작하기로 결정합니다. 가비지 수집은 프로그램에서 System.gc()를 호출하여 트리거될 수도 있지만 System.gc()를 사용한다고 해서 가비지 수집이 수행된다는 보장은 없습니다. 가비지 수집 전에 가비지 수집 메커니즘은 먼저 가비지 수집을 수행하는 것이 안전한지 여부를 결정합니다. 애플리케이션의 모든 활성 스레드가 안전한 지점에 도달하면 가비지 수집이 시작될 수 있습니다. 예를 들어 객체에 메모리를 할당하는 동안 가비지 수집을 수행할 수 없거나 CPU 명령이 최적화되는 동안 가비지 수집을 수행할 수 없습니다. 컨텍스트가 손실되고 최종 결과가 올바르지 않을 수 있기 때문입니다.
가비지 수집기는 활성 참조가 있는 객체를 회수할 수 없으며, 이로 인해 JVM(Java Virtual Machine) 사양이 중단됩니다. 죽은 개체는 결국 후속 가비지 수집을 통해 재활용되므로 죽은 개체를 즉시 재활용할 필요는 없습니다. 가비지 수집을 구현하는 방법에는 여러 가지가 있지만 위의 두 가지 사항은 모든 가비지 수집 구현에서 동일합니다. 가비지 수집의 실제 과제는 개체가 살아 있는지 확인하는 방법과 애플리케이션에 최대한 영향을 주지 않고 메모리를 회수하는 방법입니다. 따라서 가비지 수집기에는 다음 두 가지 목표가 있습니다.
1. 메모리 오버플로를 방지하기 위해 애플리케이션의 메모리 할당 요구 사항을 충족하기 위해 참조되지 않은 메모리를 빠르게 해제합니다.
2. 메모리를 회수할 때 실행 중인 애플리케이션 성능(대기 시간 및 처리량)에 대한 영향을 최소화합니다.
두 가지 유형의 가비지 수집
이 시리즈의 첫 번째 기사에서는 참조 계산 및 추적 수집이라는 두 가지 가비지 수집 방법을 소개했습니다. 다음으로 이 두 가지 접근 방식을 더 자세히 살펴보고 프로덕션 환경에서 사용되는 일부 추적 수집 알고리즘을 소개합니다.
참조 카운팅 수집기
참조 카운팅 콜렉터는 각 Java 객체를 가리키는 참조 수를 기록합니다. 객체를 가리키는 참조 수가 0이 되면 해당 객체는 즉시 재활용될 수 있습니다. 이러한 즉시성은 참조 카운트 컬렉터의 가장 큰 장점이며 참조가 없는 메모리를 유지하는 데 드는 오버헤드가 거의 없지만 각 개체의 최신 참조 카운트를 추적하는 데 비용이 많이 듭니다.
참조 카운팅 수집기의 주요 어려움은 참조 카운팅의 정확성을 보장하는 방법입니다. 또 다른 잘 알려진 어려움은 순환 참조를 처리하는 방법입니다. 두 개체가 서로 참조하고 다른 활성 개체에서는 참조되지 않는 경우 두 개체를 모두 가리키는 참조 수가 0이 아니기 때문에 두 개체의 메모리는 회수되지 않습니다. 순환 참조 구조의 메모리 재활용에는 주요 분석(역자 주: Java 힙에 대한 전역 분석)이 필요하며, 이는 알고리즘의 복잡성을 증가시켜 애플리케이션에 추가적인 오버헤드를 가져옵니다.
추적 수집기
추적 수집기는 알려진 초기 라이브 개체 집합에 대한 참조(참조 및 참조 참조)를 반복하여 모든 라이브 개체를 찾을 수 있다는 가정을 기반으로 합니다. 활성 개체(루트 개체라고도 함)의 초기 집합은 레지스터, 전역 개체 및 스택 프레임을 분석하여 확인할 수 있습니다. 초기 개체 집합을 결정한 후 추적 수집기는 이러한 개체의 참조 관계를 따르고 참조가 가리키는 개체를 순차적으로 활성 개체로 표시하여 알려진 활성 개체 집합이 계속 확장됩니다. 이 프로세스는 참조된 모든 개체가 라이브 개체로 표시되고 표시되지 않은 개체의 메모리가 회수될 때까지 계속됩니다.
추적 수집기는 주로 순환 참조 구조를 처리할 수 있다는 점에서 참조 계산 수집기와 다릅니다. 대부분의 추적 수집기는 마킹 단계 중에 순환 참조 구조에서 참조되지 않은 개체를 발견합니다.
추적 수집기는 동적 언어에서 가장 일반적으로 사용되는 메모리 관리 방법이며 현재 Java에서도 가장 일반적인 방법입니다. 수년 동안 프로덕션 환경에서도 검증되었습니다. 아래에서는 추적 수집을 구현하기 위한 일부 알고리즘으로 시작하는 추적 수집기를 소개합니다.
추적 수집 알고리즘
가비지 수집기 및 마크 스윕 가비지 수집기를 복사하는 것은 새로운 것은 아니지만 오늘날에도 여전히 추적 수집을 구현하는 가장 일반적인 두 가지 알고리즘입니다.
가비지 수집기 복사
전통적인 복사 가비지 수집기는 힙에서 두 개의 주소 공간(즉, from 공간과 to 공간)을 사용합니다. 가비지 수집이 수행되면 from 공간의 활성 개체가 to 공간에 복사됩니다. 제거됩니다(번역자 주: to 공간 또는 이전 세대에 복사한 후 전체 from 공간을 재활용할 수 있습니다. 공간이 다시 할당되면 to 공간이 먼저 사용됩니다(즉, to 공간). 이전 라운드의 공간이 우주에서 새로운 라운드로 사용됩니다.
이 알고리즘의 초기 구현에서는 from 공간과 to 공간의 위치가 지속적으로 변경되었습니다. 즉, to 공간이 가득 차고 가비지 수집이 트리거되면 그림 1과 같이 to 공간이 from 공간이 됩니다. .
그림 1 기존 복사본 가비지 수집 순서
최신 복사 알고리즘을 사용하면 힙의 모든 주소 공간을 공간 및 공간에서 사용할 수 있습니다. 이렇게 하면 서로 위치를 바꿀 필요가 없고 논리적으로만 위치를 변경할 수 있습니다.
복사 컬렉터의 장점은 to 공간에 복사된 객체들이 콤팩트하게 배열되어 있고 조각화가 전혀 없다는 점입니다. 조각화는 다른 가비지 수집기가 직면하는 일반적인 문제이며 나중에 논의할 주요 문제이기도 합니다.
복사 수집기의 단점
일반적으로 복사 콜렉터는 stop-the-world입니다. 즉, 가비지 콜렉션이 진행 중인 동안에는 애플리케이션을 실행할 수 없습니다. 이 구현을 사용하면 복사해야 할 항목이 많을수록 애플리케이션 성능에 미치는 영향이 커집니다. 이는 응답 시간에 민감한 애플리케이션의 단점입니다. 복사 컬렉터를 사용할 때 최악의 시나리오도 고려해야 합니다(즉, from 공간의 모든 개체가 활성 개체입니다. 이때 이러한 활성 개체를 이동할 수 있도록 충분한 공간을 준비해야 합니다. 공간은 From 공간에 있는 모든 개체를 설치할 수 있을 만큼 충분히 커야 합니다. 이러한 제한으로 인해 복사 알고리즘의 메모리 활용도가 약간 부족합니다(역자 주: 최악의 경우 to 공간이 from 공간과 동일한 크기여야 하므로 활용률은 50%에 불과합니다).
마크 클리어 콜렉터
엔터프라이즈 프로덕션 환경에 배포된 대부분의 상용 JVM은 가비지 수집기가 애플리케이션 성능에 미치는 영향을 복제하지 않기 때문에 표시-스윕(또는 표시) 수집기를 사용합니다. 가장 유명한 마크 수집기로는 CMS, G1, GenPar 및 DeterministicGC가 있습니다.
마크 스윕 컬렉터는 객체 참조를 추적하고 플래그 비트를 사용하여 발견된 각 객체를 실시간으로 표시합니다. 이 플래그는 일반적으로 힙의 주소 또는 주소 그룹에 해당합니다. 예를 들어 활성 비트는 개체 헤더의 비트(번역자 참고 사항: 비트)이거나 비트 벡터 또는 비트맵일 수 있습니다.
마킹이 완료되면 정리 단계에 들어갑니다. 정리 단계에서는 일반적으로 힙을 다시 탐색하여(라이브로 표시된 개체뿐만 아니라 전체 힙) 표시되지 않은 연속 메모리 주소 공간(표시되지 않은 메모리는 사용 가능하고 재활용 가능)을 찾은 다음 수집기가 이를 사용 가능한 목록으로 구성합니다. 가비지 수집기는 여러 개의 사용 가능한 목록(일반적으로 메모리 블록의 크기에 따라 구분됨)을 가질 수 있습니다. 일부 JVM(예: JRockit Real Time) 수집기는 애플리케이션 성능 분석 및 개체 크기 통계를 기반으로 사용 가능한 목록을 동적으로 분할하기도 합니다.
정리 단계 후에 애플리케이션은 메모리를 다시 할당할 수 있습니다. 사용 가능 목록에서 새 개체에 대한 메모리를 할당할 때 새로 할당된 메모리 블록은 새 개체의 크기, 스레드의 평균 개체 크기 또는 애플리케이션의 TLAB 크기에 맞아야 합니다. 새 개체에 대해 적절한 크기의 메모리 블록을 찾으면 메모리를 최적화하고 조각화를 줄이는 데 도움이 됩니다.
마크 - 수집가의 결함 제거
표시 단계의 실행 시간은 힙의 활성 개체 수에 따라 달라지고 정리 단계의 실행 시간은 힙의 크기에 따라 달라집니다. 따라서 힙 설정이 크고 힙에 활성 개체가 많은 상황에서는 마크 스윕 알고리즘에 특정 일시 중지 시간이 있습니다.
메모리 집약적 애플리케이션의 경우 다양한 애플리케이션 시나리오 및 요구 사항에 맞게 가비지 수집 매개변수를 조정할 수 있습니다. 대부분의 경우 이러한 조정은 표시/스윕 단계에서 발생하는 위험을 애플리케이션 또는 서비스 계약 SLA로 연기합니다(여기서 SLA는 애플리케이션이 달성해야 하는 응답 시간을 나타냄). 그러나 튜닝은 특정 로드 및 메모리 할당 비율에만 효과적입니다. 애플리케이션 자체에 대한 로드 변경이나 수정에는 재튜닝이 필요합니다.
마크 스윕 수집기 구현
마크 스윕 가비지 수집을 구현하는 데는 상업적으로 입증된 두 가지 이상의 방법이 있습니다. 하나는 병렬 가비지 수집이고 다른 하나는 동시(또는 대부분의 경우 동시) 가비지 수집입니다.
병렬 수집기
병렬 수집은 가비지 수집 스레드에서 리소스를 병렬로 사용하는 것을 의미합니다. 병렬 수집의 대부분의 상용 구현은 가비지 수집이 완료될 때까지 모든 애플리케이션 스레드가 일시 중지되는 세계 중지 수집기입니다. 가비지 수집기는 리소스를 효율적으로 사용할 수 있기 때문에 일반적으로 처리량 벤치마크에서 더 나은 성능을 얻습니다. 사양jbb. 애플리케이션에 처리량이 중요한 경우 병렬 가비지 수집기를 선택하는 것이 좋습니다.
병렬 수집(특히 프로덕션 환경의 경우)의 주요 비용은 복사 수집기와 마찬가지로 가비지 수집 중에 애플리케이션 스레드가 제대로 작동할 수 없다는 것입니다. 따라서 병렬 수집기의 사용은 응답 시간에 민감한 애플리케이션에 상당한 영향을 미칩니다. 특히 힙 공간에 복잡한 활성 개체 구조가 많을 경우 추적해야 하는 개체 참조도 많습니다. (mark-sweep 수집기가 메모리를 회수하는 데 걸리는 시간은 활성 객체 컬렉션을 추적하는 데 걸리는 시간과 전체 힙을 순회하는 데 걸리는 시간에 따라 달라집니다.) 병렬 접근 방식을 사용하면 애플리케이션이 다음 기간 동안 일시 중지됩니다. 전체 가비지 수집 시간.
동시 수집기
동시 가비지 수집기는 응답 시간에 민감한 애플리케이션에 더 적합합니다. 동시성은 가비지 수집 스레드와 애플리케이션 스레드가 동시에 실행됨을 의미합니다. 가비지 수집 스레드는 모든 리소스를 소유하지 않으므로 가비지 수집을 시작할 시기를 결정하여 활성 개체 수집을 추적하고 애플리케이션 메모리가 오버플로되기 전에 메모리를 회수할 수 있는 충분한 시간을 허용해야 합니다. 가비지 수집이 제때 완료되지 않으면 애플리케이션에서 메모리 오버플로 오류가 발생합니다. 반면에 가비지 수집이 애플리케이션의 리소스를 소비하고 처리량에 영향을 미치기 때문에 너무 오래 걸리지 않도록 해야 합니다. 이러한 균형을 유지하려면 기술이 필요하므로 가비지 수집을 시작할 시기와 가비지 수집 최적화를 선택할 시기를 결정하는 데 경험적 방법이 사용됩니다.
또 다른 어려움은 정리 단계에 들어갈 수 있도록 표시 단계가 언제 완료되는지 알아야 하는 등 일부 작업(완전하고 정확한 힙 스냅샷이 필요한 작업)을 수행하는 것이 안전한 시기를 결정하는 것입니다. 세계가 이미 일시 중지되었기 때문에 이는 세계 정지 병렬 수집기의 경우 문제가 되지 않습니다(번역자 참고: 애플리케이션 스레드가 일시 중지되고 가비지 수집 스레드가 리소스를 독점함). 그러나 동시 수집가의 경우 마킹 단계에서 즉시 청소 단계로 전환하는 것이 안전하지 않을 수 있습니다. 애플리케이션 스레드가 가비지 수집기에서 추적하고 표시한 메모리 부분을 수정하는 경우 표시되지 않은 새 참조가 생성될 수 있습니다. 일부 동시 컬렉션 구현에서는 이로 인해 애플리케이션이 이 메모리를 필요로 할 때 여유 메모리를 확보하지 못한 채 오랫동안 반복되는 주석 루프에 갇히게 될 수 있습니다.
지금까지의 논의를 통해 우리는 각각 특정 애플리케이션 유형과 다양한 로드에 적합한 많은 가비지 수집기와 가비지 수집 알고리즘이 있다는 것을 알고 있습니다. 단지 다른 알고리즘이 아니라 다른 알고리즘 구현입니다. 따라서 가비지 수집기를 지정하기 전에 애플리케이션의 요구 사항과 자체 특성을 이해하는 것이 가장 좋습니다. 다음으로, Java 플랫폼 메모리 모델의 몇 가지 함정을 소개하겠습니다. 여기서 함정은 Java 프로그래머가 동적으로 변화하는 프로덕션 환경에서 애플리케이션 성능을 악화시키는 경향이 있는 몇 가지 가정을 참조합니다.
튜닝이 가비지 컬렉션을 대체할 수 없는 이유
대부분의 Java 프로그래머는 Java 프로그램을 최적화하기 위한 다양한 옵션이 있다는 것을 알고 있습니다. 여러 선택적 JVM, 가비지 수집기 및 성능 조정 매개변수를 통해 개발자는 끝없는 성능 조정에 많은 시간을 할애할 수 있습니다. 이로 인해 일부 사람들은 가비지 수집이 나쁘고 가비지 수집이 덜 자주 발생하거나 더 짧게 지속되도록 조정하는 것이 좋은 해결 방법이라는 결론을 내리게 되었지만 이는 위험합니다.
특정 애플리케이션에 대한 튜닝을 고려하십시오. 대부분의 튜닝 매개변수(예: 메모리 할당 속도, 객체 크기, 응답 시간)는 현재 테스트 데이터 볼륨을 기반으로 하는 애플리케이션의 메모리 할당 속도(번역자 참고: 또는 기타 매개변수)를 기반으로 합니다. 이는 궁극적으로 다음 두 가지 결과로 이어질 수 있습니다.
1. 테스트에서 통과한 사용 사례가 프로덕션에서는 실패합니다.
2. 데이터 볼륨이 변경되거나 애플리케이션이 변경되면 재조정이 필요합니다.
튜닝은 반복적이며 특히 동시 가비지 수집기에는 많은 튜닝이 필요할 수 있습니다(특히 프로덕션 환경에서). 애플리케이션의 요구 사항을 충족하려면 경험적 방법이 필요합니다. 최악의 시나리오를 충족시키기 위해 튜닝 결과는 매우 경직된 구성이 될 수 있으며, 이로 인해 많은 리소스가 낭비되기도 합니다. 이 튜닝 접근 방식은 기발한 탐구입니다. 실제로 특정 로드에 맞게 가비지 수집기를 더 많이 최적화할수록 Java 런타임의 동적 특성에서 멀어집니다. 결국, 안정적인 로드를 갖는 애플리케이션은 몇 개이며, 로드가 얼마나 안정적일 것으로 예상할 수 있습니까?
그렇다면 튜닝에 집중하지 않는다면 메모리 부족 오류를 방지하고 응답 시간을 개선하기 위해 무엇을 할 수 있을까요? 첫 번째는 Java 애플리케이션의 성능에 영향을 미치는 주요 요인을 찾는 것입니다.
분열
Java 애플리케이션 성능에 영향을 미치는 요소는 가비지 수집기가 아니라 조각화와 가비지 수집기가 조각화를 처리하는 방식입니다. 소위 조각화란 힙 공간에 여유 공간이 있지만 새 객체에 메모리를 할당할 만큼 충분한 연속 메모리 공간이 없는 상태를 말합니다. 첫 번째 기사에서 언급했듯이 메모리 조각화는 힙에 남아 있는 공간의 TLAB이거나 수명이 긴 개체 중에서 해제된 작은 개체가 차지하는 공간입니다.
시간이 지남에 따라 애플리케이션이 실행됨에 따라 이 조각화는 힙 전체에 퍼집니다. 어떤 경우에는 정적으로 조정된 매개변수를 사용하는 것이 애플리케이션의 동적 요구 사항을 충족하지 못하기 때문에 더 나쁠 수 있습니다. 애플리케이션은 이 조각난 공간을 효율적으로 활용할 수 없습니다. 아무것도 하지 않으면 가비지 수집기가 새 개체에 할당하기 위해 메모리를 확보하려고 시도하는 연속적인 가비지 수집이 발생합니다. 최악의 경우, 연속적인 가비지 수집에도 더 많은 메모리를 확보할 수 없으며(너무 많은 조각화) JVM에서 메모리 오버플로 오류가 발생해야 합니다. Java 힙에 새 객체를 할당할 연속 메모리 공간이 있도록 애플리케이션을 다시 시작하면 조각화를 해결할 수 있습니다. 프로그램을 다시 시작하면 가동 중지 시간이 발생하고 잠시 후 Java 힙이 다시 조각으로 가득 차서 다시 시작해야 합니다.
프로세스를 중단시키는 메모리 부족 오류와 가비지 수집기가 오버로드되었음을 나타내는 로그는 가비지 수집이 메모리를 확보하려고 시도하고 있으며 힙이 심하게 조각화되었음을 나타냅니다. 일부 프로그래머는 가비지 수집기를 다시 최적화하여 조각화 문제를 해결하려고 시도합니다. 하지만 저는 이 문제를 해결하려면 좀 더 혁신적인 방법을 찾아야 한다고 생각합니다. 다음 섹션에서는 조각화에 대한 두 가지 솔루션, 즉 세대별 가비지 수집과 압축에 중점을 둘 것입니다.
세대별 가비지 수집
프로덕션 환경에 있는 대부분의 개체는 수명이 짧다는 이론을 들어보셨을 것입니다. 세대별 가비지 수집은 이 이론에서 파생된 가비지 수집 전략입니다. 세대별 가비지 수집에서는 힙을 서로 다른 공간(또는 세대)으로 나누고, 각 공간에는 서로 다른 연령의 개체가 저장됩니다. 소위 개체의 수명은 해당 개체가 살아남은 가비지 수집 주기의 수입니다. 객체가 얼마나 오래되었는지) 가비지 수집 주기 후에도 여전히 참조됩니다.
새로운 세대에 할당할 남은 공간이 없을 때, 새로운 세대의 활성 개체는 Old 세대로 이동됩니다. (보통 2세대만 있습니다. 번역자 주: 특정 연령을 충족하는 개체만 Old 세대로 이동됩니다. 세대 가비지 수집은 단방향 복사 수집기를 사용하는 경우가 많습니다. 물론 일부 최신 JVM은 새로운 세대와 이전 세대에 대해 서로 다른 가비지 수집 알고리즘을 구현할 수 있습니다. 병렬 컬렉터나 복사 컬렉터를 사용하는 경우 젊은 컬렉터는 세계 최고의 컬렉터입니다(이전 설명 참조).
이전 세대는 새 세대 밖으로 이동된 개체에 할당됩니다. 이러한 개체는 오랫동안 참조되었거나 새 세대의 일부 개체 컬렉션에서 참조됩니다. 대형 개체를 이동하는 데 드는 비용이 상대적으로 높기 때문에 대형 개체를 Old Generation에 직접 할당하는 경우도 있습니다.
세대별 가비지 수집 기술
세대별 가비지 수집에서는 가비지 수집이 구세대에서는 덜 자주 실행되고 신세대에서는 더 자주 실행되며, 또한 신세대에서는 가비지 수집 주기가 더 짧아지기를 바랍니다. 드문 경우지만, 젊은 세대가 구세대보다 더 자주 수집될 수 있습니다. 이는 젊은 세대를 너무 크게 만들고 애플리케이션의 대부분의 객체가 오랫동안 유지되는 경우 발생할 수 있습니다. 이 경우, Old Generation이 수명이 긴 객체를 모두 수용하기에는 너무 작게 설정되면 Old Generation의 가비지 수집도 이동된 객체를 위한 공간을 확보하는 데 어려움을 겪게 됩니다. 그러나 일반적으로 세대별 가비지 수집을 사용하면 애플리케이션이 더 나은 성능을 얻을 수 있습니다.
새로운 세대를 나누는 것의 또 다른 이점은 단편화 문제를 어느 정도 해결하거나 최악의 시나리오를 연기한다는 것입니다. 생존 시간이 짧은 작은 개체는 조각화 문제를 일으킬 수 있지만 차세대 가비지 수집에서는 모두 정리됩니다. 수명이 긴 개체는 Old Generation으로 이동될 때 더 컴팩트한 공간이 할당되므로 Old Generation도 더 컴팩트합니다. 시간이 지남에 따라(애플리케이션이 충분히 오랫동안 실행되는 경우) 이전 세대도 조각화되어 하나 이상의 전체 가비지 수집을 실행해야 하며 JVM에서 메모리 부족 오류가 발생할 수도 있습니다. 그러나 새로운 세대를 개척하는 것은 최악의 시나리오를 연기하는데, 이는 많은 애플리케이션에 충분합니다. 대부분의 애플리케이션에서 이는 세계 최고의 가비지 수집 빈도와 메모리 부족 오류 가능성을 줄여줍니다.
세대별 가비지 수집 최적화
앞서 언급했듯이 세대별 가비지 컬렉션을 사용하면 Young Generation 크기 조정, 승격 비율 조정 등 튜닝 작업이 반복됩니다. 특정 애플리케이션 런타임에 대한 절충점은 아무리 강조해도 지나치지 않습니다. 고정된 크기를 선택하면 애플리케이션이 최적화되지만 불가피한 동적 변경에 대처하는 가비지 수집기의 능력도 감소합니다.
신세대의 첫 번째 원칙은 Stop-The World 가비지 수집 시 지연 시간을 보장하면서 이를 최대한 늘리는 동시에 장기간 생존하는 개체를 위해 힙에 충분한 공간을 확보하는 것입니다. 세대별 가비지 수집기를 조정할 때 고려해야 할 몇 가지 추가 요소는 다음과 같습니다.
1. 대부분의 신세대는 세계 최고의 가비지 수집기입니다. 신세대 설정이 클수록 해당 일시 중지 시간이 길어집니다. 따라서 가비지 수집 일시 중지 시간에 크게 영향을 받는 애플리케이션의 경우 젊은 세대의 크기를 신중하게 고려하세요.
2. 다양한 세대에서 다양한 가비지 수집 알고리즘을 사용할 수 있습니다. 예를 들어 Young 세대에서는 병렬 가비지 수집이 사용되고 Old 세대에서는 동시 가비지 수집이 사용됩니다.
3. 잦은 승격(번역자 주: 신세대에서 구세대로 이동)이 실패하는 것으로 확인되면 구세대에 프래그먼트가 너무 많아 구세대에 공간이 부족하다는 뜻이다. 새로운 세대에서 이동된 객체를 저장합니다. 이 시점에서 승격 비율을 조정하거나(예: 승격 기간 조정) 이전 세대의 가비지 수집 알고리즘이 압축을 수행하는지 확인하고(다음 단락에서 설명) 애플리케이션 로드에 맞게 압축을 조정할 수 있습니다. . 힙 크기와 각 세대 크기를 늘리는 것도 가능하지만 이렇게 하면 이전 세대의 일시 중지 시간이 더욱 연장됩니다. 조각화는 불가피하다는 것을 아십시오.
4. 세대별 가비지 수집은 이러한 애플리케이션에 가장 적합합니다. 생존 시간이 짧은 작은 개체가 많기 때문에 가비지 수집 주기의 첫 번째 라운드에서 많은 개체가 재활용됩니다. 이러한 애플리케이션의 경우 세대별 가비지 수집은 효과적으로 조각화를 줄이고 조각화의 영향을 지연시킬 수 있습니다.
압축
세대별 가비지 수집이 조각화 및 메모리 부족 오류 발생을 지연시키지만 압축은 조각화 문제에 대한 유일한 실제 솔루션입니다. 압축은 개체를 이동하여 인접한 메모리 블록을 해제하여 새 개체를 생성할 수 있는 충분한 공간을 확보하는 가비지 수집 전략입니다.
개체를 이동하고 개체 참조를 업데이트하는 것은 일정량의 소비를 가져오는 세계 최고의 작업입니다(한 가지 예외는 이 시리즈의 다음 문서에서 설명합니다). 살아남는 객체가 많을수록 압축으로 인한 일시 중지 시간이 길어집니다. 남은 공간이 거의 없고 조각화가 심한 상황(보통 프로그램을 오랫동안 실행했기 때문에)에는 라이브 개체가 많은 영역을 압축하는 데 몇 초의 일시 중지가 발생할 수 있으며, 메모리 오버플로에 접근하면 전체 힙에는 수십 초가 걸릴 수도 있습니다.
압축을 위한 일시 중지 시간은 이동해야 하는 메모리 양과 업데이트해야 하는 참조 수에 따라 달라집니다. 통계 분석에 따르면 힙이 클수록 이동하고 참조를 업데이트해야 하는 라이브 개체 수가 더 많아지는 것으로 나타났습니다. 일시정지 시간은 1GB~2GB의 라이브 객체가 이동될 때마다 약 1초이며, 4GB 크기 힙의 경우 라이브 객체가 25%일 가능성이 높으므로 약 1초의 일시정지가 가끔 발생합니다.
압축 및 애플리케이션 메모리 월
애플리케이션 메모리 벽은 가비지 수집(예: 압축)으로 인해 일시 중지되기 전에 설정할 수 있는 힙 크기를 나타냅니다. 시스템과 애플리케이션에 따라 대부분의 Java 애플리케이션 메모리 벽의 범위는 4GB에서 20GB입니다. 이것이 대부분의 엔터프라이즈 애플리케이션이 몇 개의 큰 JVM이 아닌 여러 개의 작은 JVM에 배포되는 이유입니다. 다음을 고려해 봅시다. JVM의 압축 제한으로 정의되는 최신 엔터프라이즈 Java 애플리케이션 설계 및 배포 수는 얼마나 됩니까? 이 경우 힙 조각 모음의 일시 중지 시간을 우회하기 위해 관리 비용이 더 많이 드는 다중 인스턴스 배포를 선택했습니다. 오늘날 하드웨어의 대용량 저장 기능과 엔터프라이즈급 Java 애플리케이션을 위한 메모리 증가의 필요성을 고려하면 이는 다소 이상합니다. 각 인스턴스에 몇 GB의 메모리만 설정되는 이유 동시 압축은 다음 기사의 주제인 메모리 벽을 무너뜨립니다.
요약
이 기사는 가비지 수집의 개념과 메커니즘을 이해하는 데 도움이 되고 더 많은 관련 기사를 읽을 동기를 부여하기 위한 가비지 수집에 대한 소개 기사입니다. 여기에서 논의된 것 중 상당수는 오랫동안 존재해 왔으며 다음 기사에서는 몇 가지 새로운 개념을 소개할 것입니다. 예를 들어, 동시 압축은 현재 Azul의 Zing JVM에 의해 구현됩니다. 특히 오늘날 메모리와 처리 능력이 지속적으로 향상됨에 따라 Java 메모리 모델을 재정의하려는 시도까지 하는 새로운 가비지 수집 기술입니다.
내가 요약한 가비지 수집에 대한 몇 가지 핵심 사항은 다음과 같습니다.
1. 다양한 가비지 수집 알고리즘과 구현은 다양한 애플리케이션 요구 사항에 맞춰 조정됩니다. 추적 가비지 수집기는 상용 Java 가상 머신에서 가장 일반적으로 사용되는 가비지 수집기입니다.
2. 병렬 가비지 수집은 가비지 수집을 수행할 때 모든 리소스를 병렬로 사용합니다. 이는 일반적으로 세계 최고의 가비지 수집기이므로 처리량이 더 높지만 애플리케이션의 작업자 스레드는 가비지 수집 스레드가 완료될 때까지 기다려야 하며 이는 애플리케이션의 응답 시간에 특정 영향을 미칩니다.
3. 동시 가비지 수집: 수집이 수행되는 동안 애플리케이션 작업자 스레드는 계속 실행됩니다. 동시 가비지 수집기는 애플리케이션에 메모리가 필요하기 전에 가비지 수집을 완료해야 합니다.
4. 세대별 가비지 수집은 조각화를 지연시키는 데 도움이 되지만 조각화를 제거할 수는 없습니다. 세대별 가비지 수집은 힙을 두 개의 공간으로 나눕니다. 한 공간은 새 개체를 위한 공간이고 다른 하나는 이전 개체를 위한 공간입니다. 세대별 가비지 수집은 수명이 짧은 작은 개체가 많은 애플리케이션에 적합합니다.
5. 압축은 조각화를 해결하는 유일한 방법입니다. 대부분의 가비지 수집기는 프로그램이 오래 실행될수록 개체 참조가 더 복잡해지고 개체 크기가 고르지 않게 분산되어 압축 시간이 길어집니다. 업데이트해야 할 라이브 개체와 참조가 더 많을 수 있으므로 힙 크기도 압축 시간에 영향을 줍니다.
6. 튜닝은 메모리 오버플로 오류를 지연시키는 데 도움이 됩니다. 그러나 과도한 튜닝의 결과는 엄격한 구성입니다. 시행착오 접근 방식을 통해 조정을 시작하기 전에 프로덕션 환경의 로드, 응용 프로그램의 개체 유형 및 개체 참조의 특성을 이해하고 있는지 확인하십시오. 너무 엄격한 구성은 동적 하중을 처리하지 못할 수 있으므로 비동적 값을 설정할 때 결과를 이해해야 합니다.
이 시리즈의 다음 기사는 C4(Concurrent Continuously Compacting Collector) 가비지 수집 알고리즘에 대한 심층 토론입니다. 계속 지켜봐 주시기 바랍니다!
(전체 텍스트 끝)