- 이번 포스팅에선 JVM 성능 튜닝의 가장 대표적인 GC 튜닝을 알아보기 전에,
JVM Garbage Collector 에 대한 개념을 이해하는 글을 정리하겠습니다.
[자바 메모리 공간]
- 자바는 가상 머신인 JVM으로 돌아가는 프로그램이니만큼, 직접 메모리를 할당하고 제거하는 C와는 다른 메모리 공간 할당 정책을 사용합니다.
C의 경우는 C언어 메모리 레이아웃이라고 하여, C 프로그램만의 메모리 사용 공간이 있다면,
자바의 경우에는 자바 가상 머신이 관리하는 메모리 공간이 있습니다.
- 이는, 프로그램이 메모리 공간을 효율적으로 사용 및 관리하기 위한 논리적 구조이지, 물리적으로 나뉘어진 구조가 아님을 명심하세요. 메모리라는 것도, 전기가 흐르는 흐름을 전자적으로 제어하는 전자 회로의 일종의 묶음일 뿐입니다.
- Runtime Data Areas
: 위의 RDA가 바로 JVM이 사용하는 메모리 공간입니다.
자바 프로그램이 실행되면, JVM 클래스 로더가,
static한 자원들과, 바이트 코드들을 올리고,
Execution Engine이 코드를 실행하며 사용하는 공간이죠.
- RDA 간단한 설명
1. Method Area는, 말 그대로 메소드 바이트 코드와, static한 변수들을 저장하는 공간으로, 이곳에 적제된 데이터들은, 프로그램이 실행됨과 동시에 초기화 되고, 프로그램이 끝날 때 해제 됩니다.
2. Heap 영역은, GC(Garbage Collection)이 일어나는 공간으로, 추후 자세히 설명할 부분입니다.
3. Stack 영역은, 자바 코드가 실행되고, 메서드가 실행되며, 그 안에 필요한 지역변수 등이 다이나믹하게 쌓이고 제거
되는 FILO(First In Last Out) 방식의 메모리 영역입니다.
4. PC Register는, CPU Register 관련한 메모리 영역입니다.
5. Native Method Stack : JNI를 이해하셔야 합니다. native 키워드를 사용하여 C/C++의 함수를 사용할 때, 그 네이티브 함수에 사용되는 변수에 대한 메모리 영역입니다.
(Heap 영역)
- 힙 영역은, 주로 사용되는 메모리 공간인 스택과는 달리, 원래 자동으로 제거되는 공간이 아닙니다.
스택의 경우는, 변수 사용 범위를 지정해두고,
해당 변수가 사용된 이후, 차례로 해제가 되는 로직을 가지는데,
힙 영역의 경우는, 원한다면 영구히 데이터를 보관할수 있고, 보다 자유로운 공간입니다.
- C언어를 공부 해보았다면, malloc, calloc, realloc 과 같은 키워드로 이 힙 영역에 데이터를 집어넣고,
free로 해제하는 것을 해보았을 것입니다.
자바에서는, 힙 영역에 집어넣는다는 개념 자체에 대해 신경쓸 필요가 없고,
해제 역시 개발자가 따로 신경쓰지 않습니다.
바로 GC가 이 힙영역을 감시하며, 안쓰는 데이터를 삭제하며, 메모리 공간을 관리해주기 때문이죠.
(선행 지식)
- '참조'라는 개념을 알아봅시다.
메모리 주소를 참조하는 것을 말합니다.
객체지향 언어인 자바에서, int, float과 같은 기본 자료형은, '값'으로써 다뤄지며,
int a = 10;
int b = a;
b += 1;
을 하면, a의 값은 10, b의 값은 11이 되는 것처럼, 해당 변수가 소유한 '값'에 대해 접근하고 사용하게 됩니다.
이를 call by value라고 부르죠.
반면 참조, 즉 참조형 변수는 다릅니다.
내부에 기본 자료형과 메서드를 가지고 있고, java.lang.Object 클래스를 상속받는 자바의 객체형 변수,
즉 배열이나, 클래스 변수와 같은 것은,
MyClass mc1 = new MyClass();
처럼, new 키워드를 사용하여, 해당 클래스를 참조하여 만든 메모리 공간(클래스라는 설계도에 따라 마련된 공간)이 위치한 첫번째 주소값을 mc1이 받고,
만약,
MyClass mc2 = mc1;
이라는 식으로, 대입을 하게되면, mc2는, 새로이 MyClass라는 클래스에 맞게 메모리 공간에 '인스턴스'를 만들어 내는 것이 아니라,
그저 mc1이 만들어낸 메모리 공간의 인스턴스에 대한 주소값을 이어받게 되어,
결론적으로 mc2와 mc1은 모두 같은 실체를 가리키는 것이 됩니다.
이를 참조라고 하고,
이러한 참조형 변수가 생성되면, 스택 영역에 형성되는 것이 아니라, 바로 힙 영역에 할당이 되게 되는 것입니다.
[자바 Garbage Collection]
- GC(Garbage Collector)가 하는 역할
Heap 내의 객체 중에서 garbage 를 찾아낸다.
찾아낸 Garbage를 처리해서 힙의 메모리를 회수한다.
(JVM 의 GC 에서 메모리 정리는 따로 쓰레드로 동작하고 있습니다.)
- 객체의 상태
: GC는 객체의 상태를 Reachability라는 기준을 사용하여 둘로 분류하여 봅니다.
하나는, Reachable,
다른 하나는, UnReachable.
해당 객체에 대해 접근할 수단이 있으면, Reachable이라 하며,
반대로, 접근할 수단들이 사라지면 unReachable이라 합니다.
위에서 설명한 참조 개념을 생각해보시면,
MyClass mc1 = new MyClass();
MyClass mc2 = new MyClass();
이와 같이, 2개의 객체를 메모리 상에 만들고,
이를 mc1과, mc2라는 이름의 변수를 사용하여 접근이 가능합니다.
이를 Reachable이라 부르는데,
만약,
mc2 = mc1;
이런식으로 해버린다면,
mc1이 가리키는 객체의 경우는, mc1과, mc2라는 이름으로 모두 Reachable 한데,
기존에 mc2가 가리키고 있던 메모리 공간의 객체의 경우에는, 코드상 접근이 불가능해 진 것입니다.(메모리 주소를 이용하여 억지로 읽어낼수야 있겠지만, 일단 이 경우에는 프로그램상으로 사용하지 않는 버려진 공간이라는 뜻입니다.)
이렇게, 더이상 접근이 불가능하게 되어, 조작도, 해제도 할수 없이, 힙 영역에 놓여진 객체가 바로 UnReachable이라 불리는 상태가 된 것이죠.
C언어에서는 이러한 상태는 최악입니다.
C언어에서 malloc 등의 방법으로 힙영역에 정보를 올려놓고, free를 하기도 전에, 위와같이 접근이 끊기면, 해당 메모리 공간은 프로그램 실행 내내 쓸모없이 잔류되게 됩니다.
하지만 자바의 경우는, 메모리 관리를 따로 하지 않아도 되게 만들어진 언어답게, 메모리 영역도 따로 구분을 안해도 되고,
해제도 자동으로 됩니다.
- GC가 객체와 Garbage를 구분하는 기준
위에서 설명한, Reachable과 unReachable의 개념이 있지만, 사실상 GC가 메모리 해제를 하는데 판단하는 기준은,
해당 객체가 다른 곳에서 쓰이고 있는지 아닌지에 대한 것입니다.
만약 객체 a가, 객체 b의 메서드에서,
a.printMyName();
과 같이, 참조되고, 사용되는 상태라면, 객체 a를 제거하면 안되겠죠?
마찬가지로, 객체 b 역시 모든 method가 사용되고, stack 상으로 b 객체에 대한 정보가 모두 사용된 상태에서, 다른 객체에서 참조되고 있지 않은 상태에서는, 해당 객체의 존재 이유가 없기에, GC가 그것을 타겟으로 정하게 되는 것입니다.
- stop-the-world
: GC가 발생될 시, GC를 실행하는 쓰레드를 제외한 나머지 쓰레드는 모두 작업을 멈추는 것을 말합니다.
그러니까 메모리 관리를 위해서 어쩔수 없이 JVM의 모든 기능이 일시적으로 중단되는 것을 말하는 것이죠.
바로 이 'stop-the-world 시간을 줄이는 것이 GC 튜닝의 목표'입니다.
(심장 수술 할때 심장을 잠깐 멈춰두는 것과 같다고 생각하세요.)
- Java는 프로그램 코드에서 메모리를 명시적으로 지정하여 해제하지 않습니다.
명시적으로 해제하려고 해당 객체를 null로 지정하거나 System.gc() 메서드를 호출하기도 하지만,
null로 지정하는 것은 큰 문제는 안 되더라도 효과가 없고,
System.gc() 메서드를 호출하는 것은 시스템의 성능에 매우 큰 영향을 끼치므로 System.gc() 메서드는 사용하지 않는 것을 강력하게 추천한다고 합니다.
- GC 전제조건
: David ungar가 1984년 ‘Generation Scavenging: A Non-disruptive High Performance Storage Reclamation Algorithm’이라는 한 논문을 발표.
거기서 Generational GC라는 것을 발표하며, 가설을 발표했습니다.
1. 대부분의 객체는 금방 접근 불가능 상태(unreachable)가 된다.
(통계로도 생성된 객체의 98%의 객체가 곧바로 쓰레기 객체가 된다)
2. 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재한다.
: 이를 weak generational hypothesis 이라고 부르고,
이러한 보편적인 메모리 가설에 의하여, HotSpot VM에서는 크게 2개로 물리적 공간을 나누었습니다.
바로 Young 영역과 Old 영역.
바로 이러한 전제가 GC 알고리즘의 기반이 됩니다.
: Weak Generational 가설은, 생각해보면 매우 당연한 것입니다.
1번의 경우는, 몇몇 재활용성 강한 객체보다는, 객체지향에서의 객체는 데이터의 묶음으로써 존재하기 때문에, 사용처가 끝나면 금방 사라지는 것이 맞고, 프로그래밍에서, 한참 후에 쓰일 객체를 미리 생성해두는 고의나 실수를 제외하면, 대다수 객체가, 사용 후 사라지는 것입니다.
2번의 경우는, 우리가 객체간 관계를 맺을 때, 사용될 객체를 먼저 만들고(Old), 그것을 사용할 객체를 새로 만들어서, 객체 참조를 하지(new), 그 반대로, 미리 사용할 객체를 만들고, 사용될 객체를 이후에 만드는 일은, 없지는 않지만, 드문 일입니다.
(다형성을 이용하여 사용되는 객체를 바꿔주는 경우는 존재하겠네요. 혹은 null 처리를 하여, 사용 객체가 없을 때와, 입력되었을 때의 로직을 다르게 만들어 주거나...)
- GC의 경우는, 기존 프로그래밍에 있어서, 개발자가 일일이 행하던 메모리 관리에 대해 자동화를 부여한 것입니다.
바로 위와 같은 'WG 가설'을 기반으로, 메모리를 관리하고, 용도가 사라진 메모리 공간에 대한 해제와 정리가 가능하도록 만든 기술이라 생각해주세요.
- Old 영역과 Young 영역
Young 영역(Yong Generation 영역): 새롭게 생성한 객체의 대부분이 여기에 위치한다. 대부분의 객체가 금방 접근 불가능 상태가 되기 때문에 매우 많은 객체가 Young 영역에 생성되었다가 사라진다. 이 영역에서 객체가 사라질때 Minor GC가 발생한다고 말한다.
Old 영역(Old Generation 영역): 접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체가 여기로 복사된다. 대부분 Young 영역보다 크게 할당하며, 크기가 큰 만큼 Young 영역보다 GC는 적게 발생한다.(탐색에 시간이 걸리기에...) 이 영역에서 객체가 사라질 때 Major GC(혹은 Full GC)가 발생한다고 말한다.
- Aging 개념
: GC는, 객체에 대하여 Aging 개념을 사옹합니다.
각 객체가 old한지, young한지를 판별하는 기준으로,
GC가 몇번 실행될 동안 살아남았는지에 대한 횟수를 해당 객체의 Age라고 치며,
이 Age를 기준으로 young 객체인지, old 객체인지를 판별합니다.
보통은, Age 8 -> 9 부터, young -> old가 됩니다.
이렇게, minor GC를 실행하는동안, Young이던 객체가 Aging이 되어, old 영역으로 넘어가는 현상을,
Promotion이라 부릅니다.
- Permanent 영역
: Permanent Generation 영역(이하 Perm 영역)은 Method Area라고도 합니다.
객체나 억류(intern)된 문자열 정보를 저장하는 곳이며, Old 영역에서 살아남은 객체가 영원히 남아 있는 곳이 아니란걸 주의하세요.
이 영역에서 GC가 발생할 수도 있는데, 여기서 GC가 발생해도 Major GC의 횟수에 포함됩니다.
1. Class 의 Meta정보 (pkg path 정보라고 보면 됨, text 정보)
2. Method의 Meta 정보
3. Static Object
4. 상수화된 String Object
5. Class와 관련된 배열 객체 Meta 정보
6. JVM 내부적인 객체들과 최적화컴파일러(JIT)의 최적화 정보
프로젝트가 커지면 perm gen이 커서 에러가 나는데 이떄는 MaxPermSize를 jvm 옵션에 줘서 크게 하면 해결됩니다.
java8에서는 고질적인 perm gen space error를 해결하기 위해서 perm 영역을 없애버렸습니다.
그래서 -XX:MaxPermSize 설정이 사라지고 -XX:MaxMetaspaceSize 로 바뀌게 되었습니다.
- 자바 메모리 개념 정리
쓰레드별로 할당받는 영역들 | PC Register 현재 수행중인 JVM 명령의 주소를 가짐. |
Native Method Stack 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다. | |
JVM Stack 스레드가 라이프 사이클 주기에 맞춰 생성되고 소멸되며 스택 프레임이란 구조체를 저장하는 스택. JVM 은 오직 JVM 스택에 스택 프레임을 추가하고 제거하는 동작만 수행. - 스택 프레임 JVM 내에서 메서드 수행될때마다 하나의 스택프레임이 생성되며 해당 스택프레임에는 지역변수배열, 피연산자스택, 해당 메소드 실행되는 클래스 상수 정보 등을 가진 런타임 풀 레퍼런스를 가진다. (런타임 풀 레퍼런스에는 클래스의 메서드와 필드에 대한 모든 레퍼런스를 담고 있기때문에 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아 참조한다.) |
|
독자적으로 할당받는 영역들 | Heap 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이 되며, 곧 성능 이슈를 일으키는 공간이다. |
Method Area JVM 이 시작될 때 생성되는 공간으로 JVM이 일어들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, static 변수, 메서드의 바이트 코드등을 보관한다. 흔히 Permanenet Area, Permanant Generation(PermGen) 이라 불린다. 가비지 컬렉션 대상 여부는 선택사항. PermGen 이라 보통 표현되는 이 영역은 클래스의 모든 정보를 가지고 있는 영역이며, 특별히 가비지 컬렉션 대상으로 설정하지 않는 한 지속적으로 그 영역이 커진다. (자바는 클래스를 로드할 때에 미리 모든 정보를 로드하는 것이 아니라 런타임으로 로드를 하기때문에 특정 클래스가 참조될 때에 PermGen 에 해당 클래스 정보가 없을시 그때에 PermGen 영역에 해당 클래스 정보를 로드하고 이를 활용하여 객체를 생성하며 Heap 에 적재한다.) 매우 중요한 영역이라 그런지 Java 8 에서 부터는 metaspace 라는 이름으로 대체되었으며 PermGen 영역이 사라졌다. (아래 세번째 참고에 보면 static object, class, method meta data 의 증가(hot deploy 로 인한 클래스 정보 적재) 등의 사유로 해당 영역을 없앤 것으로 추정.) Java 8 의 메모리는 간략하게 아래에 정리. |
|
Runtime Constant Pool 각 클래스와 인터페이스의 상수, 메서드와 필드에 대한 모든 레퍼런스를 담고 있는 테이블로 메서드나 필드의 실제 메모리상 주소를 찾을땐 해당 풀을 참조한다. |
- card table
: GC는 old와 young 각각에 대해서 실행하는데,
young 영역에 대해 GC를 실행할 때, 해당 객체를 참조하고 있는 다른 객체가 있는지를 확인하여 이것의 삭제 여부를 정합니다.
만약 young 영역의 객체를 old 영역의 객체가 가리킨다면, 이 여부를 GC가 어떻게 파악할까요?
young 영역에 대해서는, 각각의 객체의 참조 관계를 먼저 파악할 테지만, 넓은 old 영역(대략 young 영역 사이즈의 2배라고 합니다.)의 객체가 어떤 객체를 참조하고 있는가에 대해서 알기 위해선, 기본적으로 old 영역 내의 객체들의 참조 상태도 파악을 해야합니다.
하지만 그렇게 되면 young 영역만을 판단하기 위해 old 영역도 파악을 해야하는 문제가 생기죠.(영역 탐색에 생기는 STW를 막기 위해, 영역을 나눴는데, 그런 의미가 사라지게 되는 것이죠)
그것을 막기 위한 것이 바로 card table입니다.
old 영역에는,
old 영역 객체가, young 영역의 객체를 참조할 때에, 그 정보를 기록하는 ,512바이트의 덩어리(chunk)의 카드 테이블이 있습니다.
young 영역에서 GC가 일어날 때는, Old 영역의 모든 참조 관계를 검색하는 것이 아닌,
바로 이 카드 테이블만을 확인하면 되기에, 쓸데없는 작업을 피할수 있습니다.
(Young 영역 구성)
Eden 영역
Survivor 영역(2개)
위의 그림에서도 볼 수 있듯이, Young 영역은, 총 3개의 영역으로 다시 나뉩니다.
(Young 영역 GC)
1. 새로 생성한 대부분의 객체는 Eden 영역에 위치합니다.
2. Eden 영역이 꽉차게 되면 GC가 실행되며, 살아남은 객체는 Survivor 영역 중 하나로 이동됩니다. 이때 Survivor 영역에 들어가기에 너무 큰 객체는 바로 Old 영역으로 이동합니다.
3. Eden 영역에서 GC가 발생하면 이미 살아남은 객체가 존재하는 Survivor 영역으로 객체가 계속 쌓입니다.
4. 하나의 Survivor 영역이 가득 차게 되면(From 영역) 그 중에서 살아남은 객체를 다른 Survivor 영역(To 영역)으로 이동합니다. 그리고 가득 찬 Survivor 영역은 아무 데이터도 없는 상태로 됩니다.
5. 이 과정을 반복하다가 계속해서 살아남아 있는 객체는 Old 영역으로 이동하게 됩니다.
이와같이, Young 영역에서 일어나는 GC를 Minor GC라 부릅니다.
이 절차를 확인해 보면 알겠지만 Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 합니다. 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 여러분의 시스템은 정상적인 상황이 아니라고 생각하면 됩니다.
(GC 종류)
- Old 영역은 기본적으로 '데이터가 가득 차면' GC를 실행합니다.
- GC 방식에 따라서 처리 절차가 달라집니다.
즉, GC 관련 여러 방식이 있다는 것이죠.
GC 방식은 JDK 7을 기준으로 5가지 방식이 있습니다.
- GC 방식
Serial GC
Parallel GC
Parallel Old GC(Parallel Compacting GC)
Concurrent Mark & Sweep GC(이하 CMS)
G1(Garbage First) GC
- Serial GC
: 운영 서버에서 절대 사용하면 안 되는 방식이 Serial GC입니다.
Serial GC는 데스크톱의 CPU 코어가 하나만 있을 때 사용하기 위해서 만든 방식입니다.
Serial GC를 사용하면 애플리케이션의 성능이 많이 떨어집니다. (stop-the-world 상태가 길어지게 되므로)
Young 영역에서의 GC는 앞 절에서 설명한 방식을 사용합니다.
Old 영역의 GC는 mark-sweep-compact이라는 알고리즘을 사용합니다.
1. Mark: Old 영역에 살아 있는 객체를 식별(객체의 생존은 누군가에게 참조 되는 것을 말합니다.).
2. Sweep : 힙(heap)의 앞 부분부터 확인하여 살아 있는 것만 남김(다른건 지워버립니다).
3. Compaction : 각 객체들이 연속되게 쌓이도록 힙의 가장 앞 부분부터 채워서 객체가 존재하는 부분과 객체가 없는 부분으로 나눔(메모리의 비워진 부분을 차곡차곡 쌓습니다.).
: -XX:+UseSerialGC
를 사용해서 설정이 가능합니다.
- Parallel GC(Throughput GC)
: 64비트 JVM이나 멀티 CPU 유닉스 머신에서 기본 GC로 설정이 되어있던 GC.
Parallel GC는 Serial GC와 기본적인 알고리즘은 같습니다.
Serial GC는 GC를 처리하는 스레드가 하나인 것에 비해, Parallel GC는 GC를 처리하는 쓰레드가 여러 개입니다.
때문에 Serial GC보다 빠른게 객체를 처리할 수 있습니다.(Stop the World를 병렬적으로 처리)
Parallel GC는 메모리가 충분하고 코어의 개수가 많을 때 유리합니다.
: -XX:+UseParallelGC
를 사용해서 설정이 가능합니다.
- Parallel Old GC(Parallel Compacting GC)
: Parallel Old GC는 JDK 5 update 6부터 제공한 GC 방식입니다.
앞서 설명한 Parallel GC와 비교하여 Old 영역의 GC 알고리즘만 다릅니다.
이 방식은 Mark-Summary-Compaction 단계를 거치는데,
Summary 단계는 '앞서 GC를 수행한 영역에 대해서 별도로 살아 있는 객체를 식별'한다는 점에서 Mark-Sweep-Compaction 알고리즘의 Sweep 단계와 다르며, 약간 더 복잡한 단계를 거칩니다.
: -XX:+UseParallelOldGC
를 사용해서 설정이 가능합니다.
GC를 사용하는 스레드 개수는 -XX:ParallelGCThreads=n 옵션으로 조정할 수 있습니다.
: Parallel GC와의 차이점은,
Parallel GC는, Young gen에서, 멀티쓰레드, Old gen에서는 여전히 싱글입니다.
하지만 Parallel Old GC의 경우는, Old 영역에서도 병렬적으로 GC를 실행합니다.
이를, Mark – Summary – Compact 라고 하며,
Sweep은 단일 스레드가 Old 영역 전체를 훑어 살아있는 객체만 찾아내는 방식이지만, Summary는 여러 스레드가 Old 영역을 분리하여 훑습니다.
앞선 GC에서 Compaction된 영역과 이번에 새로 나온 영역을 별도로 탐색하는 것이죠.
- Concurrent Mark & Sweep GC(CMS)
: 어떻게 하면 풀 GC의 stop-the-world 상태를 좀 줄여볼 수 있을까? 라는 고민에서 출발한 GC.
1. Initial Mark : '클래스 로더에서 가장 가까운 객체 중 살아 있는 객체만 찾는 것'으로 끝낸다. 따라서, 멈추는 시간은 매우 짧다.
2. Concurrent Mark : 방금 살아있다고 확인한 객체에서 참조하고 있는 객체들을 따라가면서 확인한다. 이 단계의 특징은 다른 스레드가 실행 중인 상태에서 동시에 진행된다는 것이다.
3. Concurrent pre-clean
4. Remark : Concurrent Mark 단계에서 새로 추가되거나 참조가 끊긴 객체를 확인한다.
5. Concurrent Sweep : 쓰레기를 정리하는 작업을 실행한다. 이 작업도 다른 스레드가 실행되고 있는 상황에서 진행한다.
(실행중인 상태에서 Sweep 함으로, 사용중인 메모리 공간에 대해 조작이 불가능하므로, compaction 할수 없습니다.)
6. Concurrent Reset
7. CPU 리소스가 부족해진다거나, 메모리 파편화가 너무 심해서 메모리 공간이 부족해지면 기존 방식대로 Stop the World를 하여, Compaction합니다.
장점
이러한 단계로 진행되는 GC 방식이기 때문에 stop-the-world 시간이 매우 짧습니다.
모든 애플리케이션의 응답 속도가 매우 중요할 때 CMS GC를 사용하며, Low Latency GC라고도 부릅니다.
단점
다른 GC 방식보다 메모리와 CPU를 더 많이 사용합니다.
Compaction 단계가 기본적으로 제공되지 않습니다.(그만큼, 다이나믹하게 메모리를 사용할수는 있지만, 메모리가 중구난방...)
CMS GC를 사용할 때에는 신중히 검토한 후에 사용해야 합니다.
그리고 조각난 메모리가 많아 Compaction 작업을 실행하면 다른 GC 방식의 stop-the-world 시간보다 stop-the-world 시간이 더 길기 때문에 Compaction 작업이 얼마나 자주, 오랫동안 수행되는지 확인해야 합니다.
: -XX:+UseConcMarkSweepGC
를 사용해서 설정이 가능합니다.
etc : Concurrent와 Parallel의 차이점 -
Concurrent의 경우는, Stop the World를 일으키지 않은 실행 상황에서 동적으로 객체를 처리하는 것입니다.
사용되는 메모리 구역은 피하기에, 동적으로 처리가 가능하지만, 그렇기에, compaction 과정을 진행할수 없어서 데이터 파편화가 일어날 수 있고,
Parrallel은, 일단 Stop the World를 일으키기는 Serial과 마찬가지지만, 그 사이 Task에 대한 처리를 병렬적으로 처리함으로써, Stop the World 시간을 단축하는 기법입니다.
- G1(Grabage First) GC
CMS의 문제점이었던 CPU리소스를 많이 차지한다(1) 그리고 메모리파편화(2) 중에 메모리파편화를 해결한 GC입니다.
자세한 내용은 추후 다시 다루겠습니다.
장점
Java 9의 디폴트 GC(이전 버전 java 에서는 안정성 문제가 있습니다.)로 검증되었으며,
G1 GC의 가장 큰 장점은 성능입니다.
지금까지 설명한 어떤 GC 방식보다도 빠릅니다.
힙 영역이 매우 큰 머신(최소 4GB이상의 메모리에서, 2GB 이상의 힙 영역을 사용하는 환경)에서 돌리기에 적합한 GC입니다.
(GC 종류 정리)
- 위에서 설명한 GC 들을 표로 정리하겠습니다.
GC 명 | 옵션 | Young | Old | GC 방식 | 장단점 |
Serial GC | -XX:+UseSerialGC | generation algorithm | mark-compact-algorithm | 1) old 살아있는 개체 식별(mark) 2) heap 앞부분부터 확인해 살아있는것만 남김(sweep) 3) 각 객체들이 연속되게 쌓이도록 heap 의 가장 앞 부분부터 적재(compact) |
적은 메모리와 단일 코어 서버운용으론 불가 STW 시간 소요 |
Parallel GC | -XX:+UseParallelGC -XX:ParallelGCThreads=value |
generation algorithm (multiple thread) |
mark-compact-algorithm | serial GC 와 동일하나 Young Gen 을 병렬처리 |
serial GC 보단 빠르나 많은 메모리와 코어 갯수가 많을 때나 유리 STW 시간 소요 |
Parallel Old GC | -XX:+UseParallelOldGC | generation algorithm (multiple thread) |
parallel compactiong algorithm (mark-summary-compaction) |
Serial GC 를 기본으로 수행을 하지만, compaction 단계 이전에 summary 라는 단계를 가진다. 해당 작업에서는 이전 GC 이후의 메모리를 인덱싱하는 작업을 우선 수행하고 해당 인덱스가 마무리된 메모리에 compact 작업을 수행하게 된다. 공간에 대한 인덱싱 작업때문에 약간의 메모리를 더 소모할 수 있다. 참고 : https://stackoverflow.com/questions/20430058/parallel-compacting-collector-algorithm |
Parallel 의 장점을 가져가고, 동시에 old 영역에 대한 GC 처리량도 늘림 약간의 메모리 소모가 더 발생할 수 있다. |
CMS (Concurrent Mark-Sweep) |
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:CMSInitiatingOccupancyFraction=value -XX:+UseCMSInitiatingOccupancyOnly |
parallel copy algorithm | concurrent mark-and-sweep algorithm | 1) ROOT set 에 의해 직접 참조되는 객체들을 Marking (initial mark) 2) 애플리케이션 동작 중 작업하며 살아있는 객체 식별 (concurrent mark) 3) 2번에서 새로 추가로 참조가 끊긴 객체를 확인 (remark) 4) 참조가 끊긴 모든 쓰레기 정리 (concurrent sweep) 모든 작업은 독립 쓰레드가 병렬로 처리. compact 단계는 기본으론 제공하지 않음. |
애플리케이션 응답속도가 매우 빠르다. 다른 GC 보다 메모리와 CPU 를 많이 사용, Compact 단계가 없다. (살아있는 객체에 대해 메모리 정리하는 작업) Compact 단계가 존재하지 않아 조각난 메모리가 많다. (메모리의 단편화) 자칫 위의 사유로 무한 Full GC 발생 가능 |
G1 GC | -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC |
evacution pause | concurrent marking | new, old 영역에 연속된 메모리 주소에 대한 구조적 개념을 배재, 메모리 영역을 통으로 관리. 전체 메모리를 Region 이라 부르는 블럭 형식 단위로 분할하고 이 영역에 객체를 할당 XX:G1HeapRegionSize=size 로 블럭단위 설정 가능. JDK6 u26 에서는 init heap size / 2048 이 기본 단위. 기존의 메모리 할당(new => old) 을 promotion 이라 칭했는데 이를 G1 에서는 Evacuation 이라 칭함. 내부적으로 GC동작에 대한 기준은 STW 시간에 대한 목표치를 기반. XX:MAXGCPauseMillis=<n>, 기본값 : 200ms 전체 region 을 대상으로 객체 referrer 를 참조하는 card table 소지.(card table 은 old 객체가 현재 참조중인 new 객체에 대한 메모리 주소를 관리, 이를 Remembered Sets 이라 한다.) GC 가 시작되면 Young region 정리(minor gc) 후 evacuation 과 compation (major gc) 가 연속으로 진행된다. 1) G1 으로 설정한 JVM 은 New/s0,1/old 로 구분되는 물리적 메모리 구분없이 region 으로 불리는 메모리블럭을 구성하면서 기동 (논리적으로 구분되어있지만, 물리적 구분 없음) 2) 초기 구동시에 생성되는 객체들은 임의의 region 에 생성되며 해당 객체의 referrer 정보는 remembered set 에 저장. 3) young gc (minor gc) 수행은 a. remembered set referrer 정보를 참조하여 살아있지 않는 것만 마킹하며 싱글스레드로 진행 b. region 의 살아있는 객체 밀도가 낮은 region 에 대해 evacuation 진행 정리대상에 region 내에 살아있는 객체는 다른 region으로 복사되며 나머지 폐기 4) old gc (major gc) 수행은 a. young gc 의 evacuation와 동일하게 진행 b. 이때 메모리가 부족하거나 GC 수행시간이 설정보다 길어질것으로 판단되면, GC 를 다시 수행하면서 survivor, old 영역으로 지정된 region 들이 메모리 앞쪽으로 정리하는 compation 단계를 거침. (region 단위로 이동) |
server style gc 라고 불림. region 들의 referrer 를 관리하기 위한 overhead 존재로 Xmx 2G 이상의 heap 메모리 요구 |
(GC 사용법)
: 특정 GC를 선택하고 싶다면,
java -XX:+UseSerialGC -jar Application.java
이나,
java -XX:+UseParallelGC -jar Application.java
같이, 실행 단계에서, 해당 어플리케이션이 사용할 GC를 옵션으로 지정해줄수 있습니다.
GC 옵션의 경우는 위의 표를 확인해주세요.
(GC 실행 시점)
1) 각 영역의 할당된 크기의 메모리가 허용치를 넘을 때
2) 개발자가 컨트롤할 영역은 아님.
: 선택한 GC 옵션에 따라 이미 정해진 시점에서 GC가 실행됩니다.
(각 GC 성능 비교)
[GC 로그 확인법]
: GC 관련 로그를 분석하여, 자바 프로그램을 Analyzing 할수 있습니다.
일반적으로 수집된 로그는 위와 같으며, 이러한 로그를 분석하여 GC를 최적화 시키는 것을 바로 GC 튜닝이라 합니다.
- 이상입니다.
GC 튜닝은 따로 정리하겠습니다.
//출처 : https://d2.naver.com/helloworld/1329
https://okky.kr/article/379036
https://www.slipp.net/wiki/pages/viewpage.action?pageId=26641949
'Programming Language' 카테고리의 다른 글
[Java] TLV(Tag-Length-Value) 설명 및 해석 함수 구현 (0) | 2024.10.26 |
---|---|
[Java] 자바 Thread Dump 개인정리 (0) | 2024.10.13 |
[Java] JNI 정리 및 개발 방식 정리 (2) | 2024.10.13 |
[Java] 자바를 사용한 병렬 프로그래밍 정리와 synchronized, volatile 설명 (0) | 2024.10.13 |
JVM 메모리 누수 방지를 위한 체크사항 (5) | 2024.09.29 |