😎서론
Java가 유명해진 큰 이유 중 하나로는 역시 다른 언어와는 다르게 GC라는 녀석이 있기 때문이다.
그렇기에 Java를 제대로 이해하고 사용하려면
GC가 무엇이고, 어떤 걸 하는지, 어떻게 무엇을 튜닝해야 할지를 알아야
Java 개발자로서 성장하는 데에 필수 조건이라 생각하기에 한번 정리를 해본다!
😏본론
GC(Garbage Collection) ?
직역하자면, 쓰레기 수거를 의미합니다.
결국 불필요한 즉, 사용되지 않는 메모리에 할당된 객체들을 직접 정리해주는 역할을 하고 있습니다.
한 줄로 정의하자면,
Heap 영역에서 더이상 사용되지 않는 객체들을 메모리에서 제거합니다.
GC가 왜 필요하지?
GC가 없는 다양한 언어들에서는 메모리를 직접 정리해주어야 합니다.
그렇기 때문에 객체를 하나하나 연결을 끊는 노력을 들여야 하기 때문에 코드도 지저분해질 수 있고,
메모리에 대해서 계속 개발자가 신경써서 개발해야 한다는 단점들이 발생하게 되었죠.
GC는 이러한 단점들을 거의 없애주고 개발자들이 메모리 관리를 신경쓰지 않도록 해주는 역할을 합니다.
왜냐하면 GC가 동적으로 할당했던 메모리 영역 중에 필요가 없게된 영역을 알아서 해제해주기 때문이죠.
그렇지만, 장점만 존재하는 건 아닙니다. 장단점을 정리해보겠습니다.
장점
- 직접 메모리를 관리해주기 때문에 메모리 누수를 막아준다.
- 해제한 메모리를 또 해제하는 이중 행위를 하지 않는다.
- 코드 내에서 메모리 정리 로직을 별도로 추가하지 않아도 된다.
- 개발자가 메모리 관리에 대해 덜 생각하면서 코드를 작성할 수 있다.
단점
- GC 작업은 다른 스레드를 모두 중단하고 진행하기 때문에 오버헤드가 발생할 수 있다.
- 개발자가 언제 GC가 메모리를 해제하는 지 알기가 어렵다.
그러면 어떻게 메모리에서 객체를 지울까?
GC의 알고리즘은 두 가지가 있습니다.
- Reference Counting
- Mark and Sweep
Reference Counting
GC ROOTS 에 대해서는 맨 아래에서 자세히 설명해두었습니다.
객체들은 본인에게 접근할 수 있는 수를 나타내는 reference count 의미의 숫자를 가지고 있습니다.
reference count 가 0이 되면 해당 객체에 접근 중인 객체가 없다는 의미로 GC의 대상이 됩니다.
그렇기에 별도로 GC를 통해 삭제될 객체들을 찾는 일을 하지 않아도 되지만
여기엔 아래 이미지와 같은 큰 단점이 존재합니다.
위처럼 GC ROOTS 영역에서 참조 중이지 않고, Heap Space 객체들끼리 순환참조 중일 경우
GC의 대상이 되지 않기 때문에 Memory Leak이 발생하게 됩니다.
Mark And Sweep
위에서 설명한 Reference Counting 의 순환참조 문제를 해결하는 알고리즘 입니다.
이 알고리즘은 Root Space 영역에서 부터 제거할 객체를 찾는 방식이기 때문에 아래와 같은 특징이 존재합니다.
- 의도적으로 GC를 실행시켜야 한다.
- 애플리케이션 실행과 GC 실행이 병행된다.
동작순서
Mark
1. GC ROOTS로 부터 모든 변수들을 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾아서 마킹을 합니다.
2. Marking되지 않은 객체는 사용이 안 되고 있는 객체이기 때문에 GC의 대상이 됩니다.
Sweep
3. 마킹되지 않은 객체들을 Heap 영역에서 제거 합니다.
이 후 Mark And Sweep을 사용하는 방식 중에서 Compact 라고 하는 과정이 추가되는 경우도 존재합니다.
Compact
4. Sweep 후에 분산 된 객체들을 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 나눕니다.
Compact 과정을 추가하게 된 이유는 다음과 같습니다.
Fragmenting and Compacting
Sweep 과정에서 메모리를 정리하다보면 메모리 단편화(Memory Fragmentation)가 발생할 수 밖에 없게 됩니다.
그 말은 메모리 영역 중간중간 비어있는 곳들이 존재하고, 채워져있는 곳들이 존재하게 되는 것입니다.
이렇게 된다면 다음과 같은 문제들이 발생하게 됩니다.
- 새로운 객체를 할당할 적절한 여유 공간을 가지는 메모리 블록을 찾기 위해서 시간을 더 소모하게 된다.
- 객체를 새로 생성할 때는 그 객체가 점유할 메모리 공간은 반드시 연속적이어야 한다.
- 그렇기에 전체 가용 메모리 공간은 여유롭지만 연속된 공간이 부족하여 객체 생성에 실패할 수 있게 된다.
이렇기에 Mark And Sweep 알고리즘을 활용한 방법들 중에서는 몇몇은 Compact 작업을 추가하였습니다.
현재 JVM에서는 Mark And Sweep을 활용한 알고리즘들을 사용하고 있습니다.
그렇다면 Heap영역에서 GC가 어떻게 동작할까?
위에 설명한 방식으로 GC가 동작하는 건 이제 알겠지만,
JVM에 있는 Heap 영역에서는 또 어떻게 동작하는지에 대해서 짚고 넘어가야 합니다.
여기서 잠깐!
JVM에서 GC가 동작하는 곳은 Heap 영역입니다.
그렇기에 static으로 선언된 변수들은 Heap 영역이 아닌 Method Area 영역에 저장되기 때문에
GC 대상이 절대 될 수 없고, 프로그램이 종료될 때까지 살아있게 됩니다.
결국 static은 GC를 통한 메모리 관리 해택을 받을 수 없기 때문에 최대한 적게, 조심히 사용해야 하고
static을 사용할 일이 생긴다면, final로 선언하는 것을 권장드립니다.
Heap영역은 크게 Young Generation 과 Old Generation으로 구분이 됩니다.
Old Generation은 하나 그대로 사용되지만, Young Generation은 그 안에서도 3 종류로 나뉘게 됩니다.
Eden 1개 / Suvivor 2개
동작 순서는 아래와 같습니다.
- 새로 생성된 객체는 Eden 영역에서 생성이 됩니다.
- Eden 영역이 다 차게되면 Minor GC가 발동이 되고, 살아있는 객체는 Survivor 0 영역으로 이동합니다.
- 다시 Eden 영역이 다 차게 되면, Eden 영역과 Survivor 영역에서 Minor GC가 발동되고, 살아있는 객체 모두 Survivor 1 영역으로 이동하게 됩니다.
- 1~3 번이 반복 됩니다.
- 정해진 임계치까지 살아남은 객체는 Old 영역으로 이동하게 됩니다.
- Old 영역이 다 차게되면 Major GC가 동작하게 됩니다.
동작 순서를 알아보면서, 몇 가지 설명이 빠진 내용들이 존재합니다.
- 정해진 임계치가 뭐지?
- Minor GC랑 Major GC는 무슨 차이지?
- 왜 이렇게 영역을 나눠놨지?
하나하나 알아봅시다!
정해진 임계치가 뭐지?
Eden -> Survivor 0 -> Survivor 1 -> Survivor 0 -> Survivor 1 -> Survivor 0 ...
이처럼 Young 영역에서 Eden 영역이 다 찰 때마다 아직 사용 중인 객체(Rachable)은 계속 이동하게 됩니다.
이렇게 이동될 때에 각 객체에 Minor GC에서 살아남은 횟수를 기록하는 age bit을 가지고 있으며,
Minor GC가 발생할 때마다 age bit 값은 1씩 증가하게 됩니다.
age bit 가 MaxTenuringThreshold 라는 설정 값을 초과하게 되는 경우 Old 영역으로 이동하게 됩니다.
MaxTenuringThreshold 는 0 ~ 15까지 설정할 수 있으며,
java 7 버전 이하는 4, java 8은 6, javaj 9+는 15로 설정되어 있으며 이는 GC 알고리즘에 따라 다르게 설정되어 있습니다.
Minor GC랑 Major GC는 무슨 차이지?
Minor GC
- Young 영역에서 발생하는 GC를 의미합니다.
- 객체 할당률이 높을 수록 Minor GC는 자주 발생하게 됩니다.
- 일반적으로 Minor GC는 stop-the-world가 발생하지 않는다고 생각하지만, 똑같이 스레드를 중지시킵니다.
- 보통 Major GC보단 빠르지만, 새로 생성된 객체가 GC에 적합하지 않을 경우
Minor GC 소요 시간이 느려질 수 있습니다.
Major GC vs Full GC
- 두 용어는 공식적인 정의가 없기 때문에 작성된 글마다 다른 의미를 띄웁니다.
- 어디는 Major GC와 Full GC를 같은 의미로 사용하고, 다른 곳은 Full GC는 Young과 Old를 모두 GC한다고 말합니다.
- Major GC는 Old 영역에서 발생하는 GC를 의미합니다.
- Minor GC에 비해 속도가 매우 느려 성능과 안정성에 문제를 끼칠 수 있습니다.
왜 이렇게 영역을 나눠놨지?
GC를 동작하게 되면 stop-the-world 즉, 진행 중인 모든 쓰레드를 중지시키고 GC를 실행시킵니다.
이는 객체가 많으면 많을수록 모든 GC를 처리하는 데 아주 오랜 시간이 걸리고 이는 성능에 큰 이슈를 범합니다.
그러나 더 작은 메모리 영역으로 작업할 가능성이 있다면 어떻게 될까요?
가능성을 조사하면서 한 그룹의 연구원들은
대부분의 애플리케이션 내부 할당이 두 가지 범주로 분류되는 것을 관찰했습니다.
- 오래된 객체에서 젊은 객체로의 참조는 아주 적게 존재합니다.
- 대부분의 객체들은 금방 GC 의 대상으로 바뀝니다.
이러한 연구를 통해 나온 가설을 '약한 세대 가설(weak generational Hypothesis)' 이라 부릅니다.
이렇게 분리해서 개별적으로 GC를 진행하게 된다면 GC 성능 향상에 도움이 되고,
더욱 다양한 알고리즘들을 사용할 수 있습니다.
😣결론
우리가 편하게 사용 중인 메모리 관리를 GC에서는 매우 복잡하게 진행되는 걸 알게 되었습니다.
다음으로는 다양한 GC 방식들을 살펴보고,
GC 튜닝하는 방법을 알아보겠습니다!
다음글
🙄못다한 용어 정리
GC ROOTS
- Root Space 영역 이라고도 표현한다.
- 해당 객체들은 다음과 같다.
- Class Loader에 의해 로딩된 클래스
- Local Variable / Parameters (지역변수 / 전역변수)
- Active Threads (현재 활성화된 스레드)
- Static fields (정적 변수)
- JNI Reference (Native Method stack의 c/c++로 작성된 JNI)
- JNI 메소드의 지역 변수 / 매개 변수
- 전역 JNI 참조 변수
- Monitor로 사용된 객체
Reachable Object
- 연결되어 있는 객체
UnReachable Object
- 연결이 되어 있지 않은 객체
Stop-the-world
- GC를 실행하는 쓰레드를 제외한 나머지 쓰레드를 모두 멈춘다.
- 대게 GC 튜닝이란 Stop-the-world 시간을 줄이는 걸 초점으로 한다.
참고자료
https://howtodoinjava.com/java/garbage-collection/all-garbage-collection-algorithms/
https://d2.naver.com/helloworld/1329
https://mirinae312.github.io/develop/2018/06/04/jvm_gc.html
https://memostack.tistory.com/229
https://www.youtube.com/watch?v=Fe3TVCEJhzo
https://www.youtube.com/watch?v=FMUpVA0Vvjw
https://support.oracle.com/knowledge/Middleware/1283267_1.html
https://stackoverflow.com/questions/38209495/default-value-for-maxtenuringthreshold-flag-for-cms-garbage-collector
https://plumbr.io/handbook/garbage-collection-in-java/generational-hypothesis
'JVM > Java' 카테고리의 다른 글
[Java] 자바 제네릭 불공변 / 공변 / 반공변 처음 들어봅니다. (0) | 2022.04.22 |
---|---|
[Java] GC Implementations - 가비지 컬렉션 구현 (1) | 2022.03.13 |
[Java] 22년 부터는 Integer 타입을 사용할 때 조심하자! (0) | 2022.01.06 |
[Java] static inner class 는 언제 로드가 될까? 로드와 초기화? (20) | 2021.11.25 |
[Java] junit5 파라미터로 List 전달하는 방법 (0) | 2021.11.19 |