Skip to content

메모리누수

CS적으로 보면, 컴퓨터 프로그램이 필요하지 않은 메모리를 계속 점유하고 있는 현상이다.

할당된 메모리를 사용한 다음 반환하지 않는 것이 누적되면 메모리가 낭비된다. 즉, 더 이상 불핑요한 메모리가 해제되지 않으면서 메모리 할당을 잘못 관리할때 발생한다.

자바에서 메모리 누수

더이상 사용되지 않는 객체들이 GC에 의해 회수되지 않고 계속 누적되는 현상을 말한다. 메모리 누수가 생기면 Old 영역에 계속 누적된 객체로 인해 Major GC가 빈번하게 발생하게 되면서, 프로그램 응답 속도가 늦어지고 성능이 저하된다. 이는 결국 OutOfMemory Error로 프로그램이 종료되게 한다.

가비지 컬렉션을 소멸 대상이 되기 위해서는 어떠한 reference 변수에서 가르키지 않아야 한다.

다 쓴 객체에 대한 참조를 해제하지 않으면 가비지 컬렉션의 대상이 되지 않아 계속 메모리가 할당되는 메모리 누수 현상이 발생된다.

GC가 되지 않는 루트 참조 객체는 크게 3가지다.

1. Static 변수에 의한 객체 참조

static는 GC의 대상이 되지 않는다. Static 변수는 클래스가 생성될 때 메모리를 할당 받고 프로그램 종료 시점에 반환되므로 사용하지 않고 있어도 메모리가 할당되어 있다. 잘 활용하면 성능을 향상시킬 수 있지만, 사용하지 않는데 계속 할당 되기만 한다면 GC가 되지 않아 메모리 릭으로 이어져 시스템 자체가 돌아갈 수 없는 상태에 이를 수 있다.

2. 모든 현재 자바 스레드 스택내의 지역 변수, 매개 변수에 의한 객체 참조

자바에서 현재 실행중인 (각 스레드별로) 모든 메소드내에 선언된 지역 변수와 매개변수에 의해 참조되는 객체와 그 객체로부터 직간접적으로 참조되는 모든 객체는 참조되어 사용될 가능성이 있으며, 이 뿐만 아니라 caller 메소드로 return된 후에는 caller 메소드에서 참조하고 있는 지역변수, 매개변수에 의해 참조되는 객체와 객체로부터 직간접적으로 참조되는 모든 객체 또한, GC되지 않고 참조되어 사용될 가능성이 있다.

따라서, 각 자바 스레드의 스택 프레임내에 있는 모든 지역변수와 매개 변수에 의해 참조되는 객체와 그 객체로부터 직간접적으로 참조되는 모든 객체들이 참조되어 사용될 가능성이 있다는 것이다.

3. JNI 프로그램에 의해 동적으로 만들어지고 제거되는 JNI global 객체 참조

그 외 또 여러가지 방법으로 메모리 누수(memory leak)가 발생하는 패턴들이 있다.

1. Integer, Long 같은 래퍼 클래스(Wrapper)를 이용하여, 무의미한 객체를 생성하는 경우

public class Adder {
publiclong addIncremental(long l) {
Long sum = 0L;
sum = sum + l;
return sum;
}
public static void main(String[] args) {
Adder adder = new Adder();
for(long ; i < 1000; i++) {
adder.addIncremental(i);
}
}
}

long 대신 Long을 사용함으로써, 오토 박싱으로 인해 sum = sum + l;에서 매 반복마다 새 객체를 생성하므로 1000개의 불필요한 객체가 생성된다.

2. 맵에 캐쉬 데이터를 선언하고 해제하지 않는 경우

import java.util.HashMap;
import java.util.Map;
public class Cache {
private Map<String,String> map= new HashMap<String,String>();
public void initCache() {
map.put("Anil", "Work as Engineer");
map.put("Shamik", "Work as Java Engineer");
map.put("Ram", "Work as Doctor");
}
public Map<String,String> getCache() {
return map;
}
public void forEachDisplay() {
for(String key : map.keySet()) {
String val = map.get(key);
System.out.println(key + " :: "+ val);
}
}
public static void main(String[] args) {
Cache cache = new Cache();
cache.initCache();
cache.forEachDisplay();
}
}

캐시에 직원과 직종을 넣었지만, 캐시를 지우지 않았다. 객체가 더 이상 사용되지 않을 때도 Map에 강력한 참조가 있기 때문에 GC가 되지 않는다.

캐시의 항목이 더 이상 필요하지 않을 떄는 캐시를 지워주는 것이 바람직하다.

또한 WeakHashMap으로 캐시를 초기화 할 수 있다. WeakHashMap의 장점은 키가 다른 객체에서 참조되지 않는 경우 해당 항목이 GC가 된다는 것이다.

하지만, 캐시에 저장된 값을 재사용할때 항목이 GC되어 사라져있을 수 있기 때문에 주의하여야 한다.

3. 스트림 객체를 사용하고 닫지 않는 경우

try
{
Connection con = DriverManager.getConnection();
...
con.close();
} Catch(exception ex) {
...
}

실수로 발생할 수 있는 경우이다.

try 블록에서 연결 리소스를 닫으므로 예외가 발생하는 경우 연결이 닫히지 않는다. 이 연결이 풀로 다시 돌아 오지 않기 때문에 메모리 누수가 발생한다. 또한 닫아지지 않아서 데드락이 발생할 가능 성이 크다. 항상 finally 블록엔 닫는 내용을 넣거나, TryWhitResource를 사용하자.

4. 맵의 키를 사용자 객체로 정의하면서 equals(), hashcode()를 재정의 하지 않아서 항상 다른 키로 착각하여 데이터가 계속 쌓이게 되는 경우

public class CustomKey {
public CustomKey(String name) {
this.name=name;
}
private String name;
public static void main(String[] args) {
Map<CustomKey,String> map = new HashMap<CustomKey,String>();
map.put(new CustomKey("Shamik"), "Shamik Mitra");
String val = map.get(new CustomKey("Shamik"));
System.out.println("Missing equals and hascode so value is not accessible from Map " + val);
}
}

5. 맵의 키를 사용자 객체로 정의하면서 equals(), hashcode()를 재정의 하였지만, 키값이 불변(Immutable) 데이터가 아니라서 데이터 비교시 계속 변하게 되는 경우

import java.util.HashMap;
import java.util.Map;
public class MutableCustomKey {
public MutableCustomKey(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int hashCode() {
final int prime = 31; int result = 1;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}
@Override
pblic boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
MutableCustomKey other = (MutableCustomKey) obj;
if (name == null) {
if (other.name != null) return false;
} else if (!name.equals(other.name))
return false;
return true;
}
public static void main(String[] args) {
MutableCustomKey key = new MutableCustomKey("Shamik");
Map<MutableCustomKey, String> map = new HashMap<MutableCustomKey, String>();
map.put(key, "Shamik Mitra");
MutableCustomKey refKey = new MutableCustomKey("Shamik");
String val = map.get(refKey);
System.out.println("Value Found " + val);
key.setName("Bubun");
String val1 = map.get(refKey);
System.out.println("Due to MutableKey value not found " + val1);
}
}

속성이 변경되면 프로그램에선 찾을 수 없지만, Map에서는 참조가 있으므로 메모리 누수가 발생한다.