함수를 호출하면 지역 변수와 매개 변수가 stack 메모리에 할당된다.
즉 함수를 호출할 때마다 새로운 스택 프레임이 생성되고, 함수 종료시 스택 프레임이 제거된다.
지역 변수 등은 함수가 끝나면 자동으로 해제되므로 메모리 누수 걱정이 없다.
동일하게 메서드 호출시 지역 변수, 매개 변수는 스택에 저장 되며, 스택은 JVM이 관리한다. 해당 메서드가 종료될 경우 자동으로 지역 변수는 해제된다.
JVM이 스택 메모리를 관리하기 때문에 개발자는 이 부분을 신경 쓸 필요가 없다.
C언어는 malloc 등의 함수로 동적 메모리 할당 및 free 함수를 "직접" 해제를 한다.
즉 개발자가 할당과 해제 모두 신경써야 하며, 잘못하면 메모리 누수나 비정상 종료가 발생할 수 있다.
객체 등이 힙 메모리 영역에 동적으로 할당이 되는데 개발자는 할당만 할뿐 해제는 하지 않는다
그럼 누가 해제를 하느냐?? 바로 가비지 컬렉터(Garbage Collector , 이하 GC) 가 해제를 한다.
GC는 "자동으로 더이상 참조되지 않는 객체를 탐지해 힙 메모리에서 해제를 한다"
👨💻정리
JAVA는 힙 메모리 영역에 대해 더이상 참조되지 않는 객체는 GC가 알아서 해제를 해준다.
GC가 관리해주긴 하지만, 여전히 메모리 누수가 발생할 케이스가 있다.
이 경우 GC가 아직 참조중이라고 판단해 GC에 수집을 할 수 없는 경우이다.
이 경우 리스너 및 콜백을 사용하고 나서 필요없으면 제거하거나(removeEventListener..) 혹은 약한 참조(Weak Reference)로 유지해 GC가 이를 수집할 수 있게 한다.
//약한 참조 소스코드 예
WeakReference<MyEventListener> weakListener = new WeakReference<>(new MyEventListener());
MyEventListener listener = weakListener.get();
if (listener != null) {
someComponent.addEventListener(listener);
} //훗날 GC가 수집할 수 있게 함
클래스로더란 필요한 클래스를 동적으로 로드하는 역할을 수행한다.
더 이상 필요하지 않은 클래스나 리소스를 계속 참조하고 있으면 GC에 수집될 수 없다.
또한 더 이상 사용되지 않는 클래스의 정적 변수에 객체등이 참조되어있다면 해당 클래스가 참조되고 있는 한 해제되지 않는다.
public class MyClass {
//static은 클래스가 로드되는 시점에 함께 로드된다.
private static SomeResource resource = new SomeResource();
//해당 클래스가 계속 메모리 상에 있다면 위 객체도 계속 참조 상태인 것이다.
//필요한 만큼 사용했다면 아래와 같이 참조를 null로 처리해 GC가 수집할 수 있게 해줘야 한다.
public static void clearResource() {
resource = null;
}
}
👨💻정리
GC는 자신이 수집할 수 없는 상태의 객체는 자동적으로 해제 처리를 할 수 없다.
위의 케이스 등은 "자원을 더 이상 안 쓰지만 계속되는 참조" 에서 문제가 발생한다.
이는 JVM이 더 이상 메모리를 할당할 수 없는 경우에 발생하며 GC가 적절한 자원 해제를 하지 못 한 채로 힙 메모리가 꽉 차버리는 것이다.
꼭 힙이 아니어도 스택 영역도 너무 깊은 재귀 호출 및 너무 큰 스택 크기 등을 사용하게 되면 stack overflow 가 발생하게 된다.
아래에 Stack 기능을 구현한 소스코드가 있고 , 여기에는 GC가 수집할 수 없는 객체가 존재한다.
(편의상 CAPACITY 확장은 고려하지 않는다.)
public class Stack {
private Object[] elements;
private int size = 0 ;
private static final int MAX_LENGTH = 16;
public Stack() {
elements = new Object[MAX_LENGTH];
}
public void push(Object e { elements[size++] = e; }
public Object pop() {
if(size == 0) { throw new EmptyStackException(); }
return elements[--size];
}
}
간단히 소스를 설명하자면,
생성자를 통해 배열 크기가 16인 Object 배열이 생성되었다.
push를 하면 순서대로 Object 객체가 배열에 할당된다.
pop을 하면 size가 가리키는 가장 상단의 Object 객체를 리턴한다 (삭제하는게 아니다!!)

위 그림을 토대로, 결국 GC는 Object 4 ~ Object 16와 같이 더는 쓰이지 않는 객체를 제거하지 못하므로 메모리 누수가 발생하는 것이다.
이에 대한 해법으로는 참조를 null처리(참조 해제) 해서 GC가 수집할 수 있게 하면 된다.

//pop 부분 변경
public Object pop() {
if(size == 0) { throw new EmptyStackException(); }
Object obj = elements[--size];
elements[size] = null; //참조 해제!
return obj ;
}
위에서는 분명 null 을 통해 참조를 해제해야 GC가 이 객체를 수집 및 해제할 수 있다고 했다.
그런데 갑자기 null을 통한 참조 해제는 특정 상황에서만 해야 한다고 교재는 설명한다.
이를 설명해보면,
(1) 자원을 해제할 때마다 null처리를 한다고 하면 오히려 코드의 가독성이나 신경쓸게 많아진다.
(2) 차라리 null 처리보다는 애초에 변수의 유효한 범위를 최소화 하는것 을 권고한다.
public void processList(List<String> items) {
for (String item : items) {
String processed = process(item);
System.out.println(processed);
}
}
위 소스코드에서 사용하는 String 객체인 processed 참조는 해당 processList 메서드가 종료되는 순간 자동으로 참조가 해제된다. (따로 null처리 할 필요가 없다.)
만약 processed 가 static 으로 클래스 상단에 선언되어 있고, 이를 processList 메서드 에서만 사용한다면?
더 이상 사용되는 곳이 없음에도 processed는 계속 참조하고 있으니 메모리 누수가 발생하는 것이다.
👨💻정리
null은 정적변수나 파일 자원 등 명시적으로 해제가 필요한 경우에 제한적으로 사용하고, 그 외에는 유효 변수 범위를 최소화 해 자원이 해제 되도록 하자
캐시는 데이터를 임시로 저장해 동일한 데이터 요청시 캐시에서 데이터를 반환하여 조회 성능을 향상 시킬 수 있다.
예) 회원조회를 위해 DB를 직접 접근하지 않고 캐시에서 찾아 사용
따라서 캐시도 동일하게 약한 참조를 만들거나 주기적으로 캐시를 청소해줘야 한다.
Map<MyKey, MyValue> cache = new WeakHashMap<>(); //약한참조인 맵을 이용해 캐시를 만든다
ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
executor.scheduleAtFixedRate(() -> {
// 캐시 청소 작업 수행
}, initialDelay, period, TimeUnit.SECONDS);
-해결방법3. LinkedHashMap의 removeEldestEntry 메서드를 오버라이드해 캐시 크기 제한 및 오래된 엔트리를 자동으로 제거하자.
Map<MyKey, MyValue> cache = new LinkedHashMap<MyKey, MyValue>(initialCapacity, loadFactor, true) {
@Override
protected boolean removeEldestEntry(Map.Entry<MyKey, MyValue> eldest) {
return size() > maxSize; // maxSize를 넘으면 가장 오래된 엔트리를 제거
}
};
👨💻정리
성능을 위해 사용하는 캐시가 메모리 누수의 주범이 될 수 있다.
따라서 위 방안등을 이용해서 GC가 수집가능하게 하자
GC가 더이상 참조되지 않는 자원을 알아서 해제해준다.
하지만 "참조는 되고 있지만 더 이상 안쓰고 있는 객체"는 개발자만이 알고 있기 때문에
이런 자원이 GC로부터 수집될 수 있도록 적절히 해제하거나 해제되도록 코드를 작성하는 것이
메모리 누수를 방지할 수 있는 방법이 되겠다.