1. Java에서도 메모리 누수는 일어날 수 있다.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Obejct[DEFAULT_INITIAL_CAPACITY];
}
...
public Object pop() {
if(size == 0) {
throw new EmptyStackException();
}
return elements[--size];
}
...
}
- Java는 기본적으로 Garbage Collector(GC)라고 하는 곳에서 메모리 자원을 관리를 해주고 있습니다. 하지만 이것이 Java라는 언어에서 메모리 누수가 일어나지 않는다고 할 수는 없습니다.
- 위의 코드에서
elements[—size]
부분이 문제가 될 수 있습니다.
- 위의 코드에서는 GC가 이미 사용이 끝난 객체를 회수하지 않습니다. 왜냐하면 GC 스스로, 해당 객체가 사용 중인지 사용하지 않는지 알 수 없기 때문입니다.
- 이는 이 코드를 작성한 개발자(우리) 가 배열 내부에서 사용할 영역인 활성 영역과 앞으로 사용하지 않을 영역인 비활성 영역을 임의로 나누었기 때문입니다.
- 개발자의 입장에서는 로직을 보면서 활성 영역과 비활성 영역을 알 수 있지만 GC의 입장에서는 해당 배열 전체가 활성 영역이라고 인지할 수 있기 때문에, 개발자의 입장에서 비활성 영역이라도, GC는 해당 객체의 메모리를 회수하지 않습니다.
- 해당 객체의 메모리만 회수하지 않는다면 그나마 다행이지만 만약 사용하지 않는 객체가 다른 사용하지 않는 객체들을 참조하고 있다면, GC는 참조하는 객체들의 메모리 회수도 불가능합니다.
- 이런 이유로, 만약 객체가 다른 객체들의 참조를 매우 많이 가지고 있다면, 단지 몇 개의 객체들 때문에 많은 메모리가 회수되지 못하고 방치될 수 있습니다.
2. 메모리 누수의 해결
- 위에서 일어나는 메모리 누수의 해결책은 매우 간단하게도 사용이 끝난 참조 변수의 값을 null로 지정하는 것입니다.
- null로 지정하는 순간 그 전에 있던 객체는 GC에 의해 회수됩니다.
- 이러한 null 처리는 예상하지 못한 에러를 잡는데에도 많은 도움을 줍니다. 미리 null로 지정해 두었다면 이후 해당 참조를 사용할 때 NullPointerException이 발생하여 잘못된 연산이 진행되고 있다는 것을 바로 알 수 있기 때문입니다.
- 그렇다면 모든 상황에 대해 객체의 사용이 끝나면 반드시 null로 처리해야 할까요? 꼭 그렇지는 않습니다.
- 사용이 끝난 참조를 해제하는 가장 좋은 방법은 그 참조를 가진 변수를 해당 변수가 존재하는 스코프 범위 바깥으로 나가는 것입니다.
public class Example1 {
static Object obj2 = new Object(); // 전역으로 선언하는 경우
public static void func() {
Object obj1 = new Object(); // 함수 Scope 내부에 선언하는 경우
// do something...
}
}
- 위의 코드를 예시로 생각해 봅시다. obj1, obj2는 모두 func() 내부에서만 사용되고 그 이후에는 사용되지 않는 참조변수 입니다.
- 전역으로 선언하는 경우, func()에서의 모든 작업이 끝나도, obj2는 사용되지 않지만 사라지지 않습니다. 따라서 GC가 메모리를 회수할 수 없으며 이 부분에서 메모리 누수가 발생할 수 있습니다.
- 해당 방식에서 일어날 수 있는 메모리 누수는 obj2 = null 이라는 코드를 func() 내부의 마지막에 넣음으로서 해결할 수 있습니다.
- 함수 Scope 내부에 선언한 obj1의 경우, 함수가 끝나는 순간 사라집니다. 동시에 사용이 끝난 obj1에 있던 객체는 GC에 의해 회수됩니다. 굳이 null을 넣어주지 않아도 자연스럽게 사라지는 것을 알 수 있습니다.