-
Java 메모리 누수(Memory Leak), GC 튜닝Java 2023. 3. 30. 00:35
메모리 누수
자바에서 메모리 누수는 더 이상 사용되지 않는 객체들이 GC(가비지 컬렉션)에 의해 소멸되지 않고 누적되는 현상이다.
가비지 컬렉션의 소멸 대상이 되려면 다른 Reference 변수에서 참조하고 있지 않아야 한다.
예시
1. Static 변수가 객체를 참조하고 있다면, 해당 객체는 GC에 의해 소멸되지 않는다.
- Static 변수는 프로그램 종료 시점에 메모리가 반환되고, 사용하지 않아도 메모리를 점유하고 있다. 이런 static 변수를 재사용한다면 이점이 있지만, 사용하지 않으면 메모리를 점유하고 있게 된다.
2. Stack에서 Heap에 있는 객체를 참조하고 있는 동안에는 해당 객체는 GC에 의해 소멸되지 않는다.
메모리 누수가 발생하는 패턴
1. 무의미한 Wrapper 객체를 생성하는 경우
GC는 Immutable 객체를 Skip한다. 컨테이너 자체가 사라질 때 같이 삭제한다.
그래서 String은 StringBuilder와 달리 Immutable 객체이기 때문에 힙에 계속 쌓여서 메모리를 점유한다는 단점이 있다.
Wrapper class의 객체는 모두 Immutable이기 때문에 조심해서 사용해야 한다.
2. Map에 Immutable 데이터를 해제하지 않은 경우
Map에는 강력한 참조가 있어서, 내부 객체가 사용되지 않을 때도 GC 대상이 되지 않는다. Map을 더이상 사용하지 않는다면, 메모리를 점유하고 있게 된다. 즉, 데이터의 메모리를 해제하는 것이 바람직합니다. WeakHashMap은 내부 데이터를 초기화할 수 있다.
3. Connection 사용 시 Try Catch 설계
아래의 경우 Connection을 생성했지만, try 내부에서 connection을 close하기 때문에, close가 실행되지 못한다.
그래서 Connection이 열려있는 채로 메모리를 점유하게 된다.
try { Connection con = DriverManager.getConnection(); // 예외 발생 !!! con.close(); } Catch(exception e) { }
4. CustomKey 사용
Map을 사용할 때 custom key를 사용할 때는 equals()와 hashcode()를 값을 기반으로 구현해야 한다. 아래의 경우 Key값이 같은 객체로 인식하지 못해서 계속 Map에 쌓이게 되면서 메모리를 점유하게 된다.
public class CustomKey { private String name; public CustomKey(String name) { this.name=name; } public staticvoid main(String[] args) { Map<CustomKey,String> map = new HashMap<CustomKey,String>(); map.put(new CustomKey("Shamik"), "Shamik Mitra"); } }
5. 더 이상 참조되지 않는 참조 (자료 구조 설계)
배열로 Stack을 구현한 예시
public class Stack { privateint maxSize; privateint[] stackArray; privateint pointer; public Stack(int s) { maxSize = s; stackArray = newint[maxSize]; pointer = -1; } public void push(int j) { stackArray[++pointer] = j; } public int pop() { return stackArray[pointer--]; } public int peek() { return stackArray[pointer]; } publicboolean isEmpty() { return (pointer == -1); } public boolean isFull() { return (pointer == maxSize - 1); } public static void main(String[] args) { Stack stack = new Stack(1000); for(int ;i<1000;i++) { stack.push(i); } for(int ;i<1000;i++) { int element = stack.pop(); System.out.println("Poped element is "+ element); } } }
pop을 할 때 포인터만 감소하고 실제 데이터는 그대로 남아있다. 알고리즘상 문제는 없지만, 배열에 해당 요소가 존재하기 때문에 GC의 대상이 될 수 없고, 사용하지 않는 요소들을 포함한 배열이 메모리에 남게 된다.
위의 경우 pop()을 할 때 해당 배열 요소 또한 삭제해주어야 한다.
public int pop() { int size = pointer--; int element = stackArray[size]; stackArray[size] = null; return element; }
GC 튜닝의 목적
Old 영역으로 넘어가는 객체의 수 최소화 하기
JDK 7부터 본격적으로 사용할 수 있는 G1 GC를 제외한, Oracle JVM에서 제공하는 모든 GC는 Generational GC이다.
즉, Eden 영역에서 객체가 처음 만들어지고, Survivor 영역을 오가다가, 끝까지 남아 있는 객체는 Old 영역으로 이동한다.
Old 영역의 GC는 New 영역의 GC에 비하여 상대적으로 시간이 오래 소요되기 때문에 Old 영역으로 이동하는 객체의 수를 줄이면 Full GC가 발생하는 빈도를 많이 줄일 수 있다.
Full GC 시간 줄이기
Full GC 실행에 시간이 오래 소요되면(1초 이상) 연계된 여러 부분에서 타임아웃이 발생할 수 있다.
그렇다고 Full GC 실행 시간을 줄이기 위해서 Old 영역의 크기를 줄이면 - OutOfMemoryError가 발생하거나 Full GC 횟수가 늘어난다.
반대로 Old 영역의 크기를 늘리면 - Full GC 횟수는 줄어들지만 실행 시간이 늘어난다.
Old 영역의 크기를 적절하게 '잘' 설정해야 한다.
Full GC
- Heap 메모리 전체 영역에서 발생한다.
- Old, Young 영역 모두에서 발생하는 GC이다.
- Minor GC, Major GC 모두 실패했거나, Young 영역와 Old 영역 모두 가득 찼을때 발생한다.- 속도가 매우 느리다.
- Full GC가 일어나는 도중에는 순간적으로 자바 애플리케이션이 중지되기 때문에 애플리케이션의 성능과 안정성에 큰 영향을 준다.
GC의 성능을 결정하는 옵션
구분 옵션 설명 힙(heap) 영역의 크기 -Xms JVM 시작 시 힙 영역 크기 -Xmx 최대 힙 영역 크기 New 영역의 크기 -XX:NewRatio New 영역과 Old 영역의 비율 -XX:NewSize New 영역의 크기 -XX:SurvivorRatio Eden 영역과 Survivor 영역의 비율 GC 튜닝의 절차
1. GC 상황 모니터링
- GC 상황을 모니터링하며 현재 운영되는 시스템의 GC 상황을 확인해야 한다.
2. 모니터링 결과 분석 후 GC 튜닝 여부 결정
- GC 수행 시간이 1~3초, 심지어 10초가 넘는 상황이라면 GC 튜닝을 진행해야 한다.
- GC가 수행되는 시간을 확인했을 때 다음의 조건에 부합되면 GC 튜닝이 필요 없다.
- Minor GC의 처리 시간이 빠르다. (50ms 내외)
- Minor GC의 주기가 빈번하지 않다. (10초 내외)
- Full GC의 처리 시간이 빠르다. (보통 1초 내외)
- Full GC의 주기가 빈번하지 않다. (10분에 1회)
3. GC 방식 / 메모리 크기 결정
- GC 튜닝을 진행하기로 결정했다면 GC 방식을 선정하고 메모리의 크기를 결정한다. 이 때 서버가 여러 대이면 여러 대의 서버에 GC 옵션을 서로 다르게 지정해서 GC 옵션에 따를 차이를 확인하는 것이 중요하다.
4. 결과 분석
- GC 옵션을 지정하고 적어도 24시간 이상 데이터를 수집한 후에 분석을 진행한다.
5. 결과가 만족스러울 경우 전체 서버에 반영 및 종료
- GC 튜닝 결과가 만족스러우면 전체 서버의 GC 옵션을 적용하고 마무리 한다.
참고
'Java' 카테고리의 다른 글
Thread와 Runnable (1) 2023.04.11 JVM 메모리 구조 (0) 2023.03.09 [Effective Java] wait와 notify보다는 동시성 유틸리티를 애용하라 (0) 2023.02.14 [Effective Java] 스레드보다는 실행자, 태스크, 스트림을 애용하라 (0) 2023.02.14 [Effective Java] 필요 없는 검사 예외 사용은 피하라 (0) 2023.02.06