-
가비지 컬렉션 GC(Garbage Collection)JAVA 2022. 8. 5. 19:20
GC 란?
가비지 컬렉션은 자바의 메모리 관리 방법 중의 하나로 JVM의 Heap 영역에서 동적으로 할당했던 메모리 영역 중 필요 없게 된 메모리 영역을 주기적으로 삭제하는 프로세스를 말한다.
- 장점
- C나 C++에서는 이러한 가비지 컬렉션이 없어 프로그래머가 수동으로 메모리 할당과 해제를 일일이 해줘야 한다
- Java는 JVM에 탑재되어 있는 가비지 컬렉터가 메모리 관리를 대행
- 개발자 입장에서 메모리 관리, 메모리 누수(Memory Leak) 문제에서 대해 완벽하게 관리하지 않아도 되어 오롯이 개발에만 집중
- 단점
- 개발자가 메모리가 언제 해제되는지 정확하게 알 수 없음
- 가비지 컬렉션(GC)이 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 발생
- GC가 너무 자주 실행되면 소프트웨어 성능 하락의 문제 발생 가능
- 실시간으로 계속 동작해주어야 하는 시스템들(ex)열추적 미사일의 경우)에는 잠깐의 소프트웨어 일시정지로도 목표한 결과가 달라질 수 있기 때문에 GC의 사용이 적합하지 않을 수 있음
GC 동작방식
- Serial GC 방식 : GC를 수행할 때는 GC를 수행하는 스레드 이외의 스레드는 모두 정지(Stop-the-world)
- Minor GC
- young 영역 : 새롭게 생성한 객체들이 위치, 많은 객체가 Young 영역에 생성되었다가 사라짐
- 대부분의 객체는 금방 unreachable한 상태가 되기 때문에 Minor GC가 많이 발생 (메모리의 부담이 적도록 고안됨)
- Eden 영역이 가득 참에서 부터 시작
- Eden 영역에서 참조가 남아있는 객체를 mark하고 survivor0 영역으로 복사
- Eden 영역 비움
- Survivor 영역도 가득차면 같은 방식으로 다른 Survivor1 영역에 복사하고 비움 (2개의 Survivor 영역 중 1개는 반드시 비어있어야 됨)
- 위의 동작들이 반복되어 특정 횟수만큼 살아남은 객체는 old 영역으로 이동
- Major GC
- old 영역 : Young 영역에서 계속 사용되어 살아남은 객체가 복사되는 영역, Young 영역보다 크게 할당, 더 적은 GC 발생
- 삭제되어야 하는 객체를 mark하고 sweep(지움)
- Mark-Sweep-Compact 알고리즘
- Minor GC
JVM 메모리는 마음대로 조정할 수 없기 때문에 GC 튜닝을 이용한다. GC 튜닝이란 GC 수행시 시스템이 멈추기 때문에 의도치 않은 장애의 원인이 될 수 있어 이를 방지하기 위해 힙 영역을 조정하는 것이다.
GC 의 종류
- Serial GC
- GC를 처리하는 쓰레드가 1개
- 다른 GC에 비해 stop-the-world 시간이 길다
- mark-sweep-compact 알고리즘
- Old영역에서 살아있는 객체를 식별(Mark)하고, 살아있는 객체만을 남김.(Sweep)
- 이후 객체들을 앞부분부터 채워 객체가 존재하는 부분과 존재하지 않는 부분으로 나눔(Compaction)
- Parallel GC
- Java 8의 default GC
- Young Generation의 GC를 멀티 쓰레드로 수행
- Serial GC에 비해 stop-the-world 시간 감소
- 기본적인 알고리즘은 Serial GC와 같지만 여러 쓰레드를 이용하여 GC를 처리
- Parallel Old GC(Parallel Compacting GC)
- Parallel GC를 개선
- Old Generation에서도 GC를 멀티 쓰레드로 수행
- Mark-Summary-Compact 알고리즘 사용
- Serial GC의 Sweep 알고리즘 대신 Summary를 사용
- Summary 단계는 앞서 GC를 수행한 영역에 대해서 별도로 살아있는 객체를 식별
- Sweep보다 조금 더 복잡
- Concurrent Mark & Sweep GC(이하 CMS)
- Initial Mark 단계에서는 살아 있는 객체를 찾는 것으로 끝 (Stop-the-World 시간이 짧음)
- compact 과정이 없음
- initial Mark - GC Root에서 참조하는 객체들만 식별
- Remark - 이전 단계에서 식별한 객체를 다시 추적, 추가되거나 참조가 끊긴 객체 확정
- Concurrent Mark - 이전 단계에서 식별한 객체들이 참조하는 모든 객체 추적
- 찾은 객체에서 참조하는 객체를 Concurrent하게(여러 쓰레드가 동시에) 따라가는 Concurrent Mark 단계가 수행
- Concurrent하게 Remark가 동작한다
- 그 이후에 Stop-the-World가 실행
- Concurrent Sweep - unreachable 객체들을 삭제
- 애플리케이션의 응답속도가 매우 중요할 때 사용한다.
- G1(Garbage First) GC
- CMS GC를 개선
- Java 9+의 default GC
- Heap을 일정한 크기의 Region으로 나눔
- 전체 Heap이 아닌 Region 단위로 탐색
- compact 진행
- 바둑판의 각 영역에 객체를 할당하고 GC를 실행
- 위에서 설명한 Young영역과 Old영역에 대한 개념을 사용하지 않고, 객체를 할당
GC의 작동 문제의 진단 및 해결
Survivor 영역 중 하나는 반드시 비어 있는 상태로 남아 있어야 하는데 만약 두 Survivor 영역에 모두 데이터가 존재하거나, 두 영역 모두 사용량이 0이라면 시스템이 정상적인 상황이 아니라고 생각할 수 있다. 추가적으로, GC에 대한 로그를 확인하여 옵션을 수정할 지 코드를 수정할 지 정해야 한다.
GC에 의한 시스템 중단 시간(Stop-the-world)을 줄이는 법
- 옵션을 변경하여 GC의 성능을 높이기
- young 영역과 old 영역의 힙 크기를 높여 GC의 빈도를 줄이는 것
- 객체의 할당과 promotion을 줄이는 것
힙의 크기를 높이면, GC 영역이 넓어져 실행시간이 길어지므로 무의미해진다고 생각할 수 있지만 Minor GC의 실행시간은 힙의 크기보다는 살아남은 객체의 수에 의해 더욱 지연되기 때문에 short-lived 객체를 위한 young 영역의 크기를 높인다면 GC의 실행 시간과 호출 빈도를 모두 줄일 수 있다. (하지만 만약 애플리케이션에서 long-lived 객체를 많이 사용한다면, survivor영역으로 복제되는 객체가 많아져 GC에 의한 멈추는 시간이 증가할 수 있다.)
- 설정을 변경하여 GC의 성능을 높이기
- 애플리케이션을 중단시킨 후에 GC를 병렬로 동시에 진행시키는 것
- 애플리케이션과 GC작업을 동시에(concurrent) 진행시키는 것
- 개발자의 코드를 변경하여 GC의 성능을 높이기
- Collection 등을 활용할 때 사용할 객체의 크기를 명시해주기
- 모든 Java의 Collections와 그를 확장하여 구현한 구현체들(Trove나 Google의 Guava)은 내부적으로 배열을 사용
- 처음에 설정한 크기를 초과하여 계속 item을 담으려고 하면 내부적으로 새로운 크기의 배열을 생성하고 item을 복사
- 사용되지 않는 메모리 즉 가비지 증가
- 스트림을 바로 사용하기
- 변경 전: byte[] fileData = readFileToByteArray(new File("myfile.txt"));
- 변경 후: FileInputStream fis = new FileInputStream(fileName);
- 예측 불가한 데이터의 크기가 너무 크다면 JVM이 해당 파일의 내용을 할당할 수 없어 OutOfMemoryErrors가 발생할 수 있으며, 할당이 되었다 하더라도 이후에 상당히 큰 규모의 가비지가 될 수 있음
- InputStream은 내부적으로 Buffer를 두고 있어 일정한 크기(Chunk)만큼씩 데이터를 조회
- InputStream을 사용하면 Buffer를 재사용함으로써 OutOfMemoryErrors를 방지할 수 있고, 가비지의 생성을 최소화
- String 사용 최적화
- 중복된 String 생성시 JVM 옵션 활용하기
- . Java 8u20 업데이트부터는 동일한 문자열에 의해 불필요한 메모리를 사용을 줄이도록 새로운 JVM 파라미터(UseStringDeduplication)를 추가
- 해당 옵션을 사용시 중복되는 String 인스턴스들을 Global Single Char[]로 관리하여 힙 메모리의 사용을 최적화
- java -XX:+UseStringDeduplication -jar Application.java
- StringBuilder의 활용
- String은 불변
- String 연산은 내용 변경이 아닌 새로운 문자열 할당
- 불필요한 객체의 생성을 줄이는 방법은 사전에 StringBuilder를 이용하는 것
- 중복된 String 생성시 JVM 옵션 활용하기
- 불변(Immutable) 객체 사용하기
- 불변의 객체가 먼저 생성되어야 컨테이너가 이 객체를 참조할 수 있. 즉, 컨테이너는 컨테이너가 참조하는 가장 젊은 객체들보다 더 젊다는 것(늦게 생성되었다는 것)
- GC가 수행될 때, 가비지 컬렉터가 컨테이너 하위의 불변 객체들은 Skip할 수 있도록 도와줌
- 해당 컨테이너가 살아있다는 것은 하위의 불변 객체들 역시 처음에 할당된 그 상태로 참조되고 있다는 것을 의미하기 때문
- 불필요한 Collection의 생성을 피하기
- Collection 재사용 하기
- Collection 등을 활용할 때 사용할 객체의 크기를 명시해주기
불변객체의 예시는 다음과 같다.
public class MutableHolder { private Object value; public Object getValue() { return value; } public void setValue(Object o) { value = o; } } public class ImmutableHolder { private final Object value; public ImmutableHolder(Object o) { value = o; } public Object getValue() { return value; } }
- MutableHolder는 계속 다른 값을 참조하여 생존하며 Old 영역으로 이동
- Old 영역으로 가서도 참조하는 객체가 바뀌기 때문에, Minor GC를 수행할 때 Old 영역으로 와서 MutableHolder까지 검사하여 Young 영역을 정리해야 함
- 검사해야 하는 범위가 늘어남
- ImmutableHolder는 참조하는 값이 먼저 존재해야 ImmutableHolder가 존재할 수 있습니다.
- ImmutableHolder가 Old 영역으로 이동하게 되면 MutableHolder Minor GC에 대한 검사를 하지 않아도 됨
- 스캔의 범위를 줄여 성능을 높일 수 있음
(참고한 사이트)
https://mangkyu.tistory.com/94
https://mangkyu.tistory.com/120
https://www.youtube.com/watch?v=Fe3TVCEJhzo&ab_channel=%EC%9A%B0%EC%95%84%ED%95%9CTech
https://coding-factory.tistory.com/829
'JAVA' 카테고리의 다른 글
인프런) 실습으로 배우는 선착순 이벤트 시스템 (1) 2024.06.05 인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (2) (1) 2024.06.05 인프런) 재고시스템으로 알아보는 동시성 이슈 해결 (1) (0) 2024.06.04 Enum Type ( 열거형 ) (0) 2021.10.26 - 장점