Memory Leak in Java

Jeonghwa·2023년 1월 24일
0

Memory Leak

Java는 GC에 의해 암묵적으로 메모리를 회수한다. GC는 도달할 수 없는(unreachable) 객체가 있는지, 엄밀히 말하자면 해당 객체를 가리키는 참조가 없는지 주기적으로 확인하고 발견하게 된다면 메모리를 회수한다.

하지만 프로그램에서 사용되지 않는(unused) 객체이지만 의도치 않게 참조가 있는 경우엔 GC의 수집 대상이 아니며 이는 잠재적인 메모리누수(memory leak)가 된다. 만약 이들이 계속 누적이 된다면, Old영역에 쌓일 것이고 MajorGC가 빈번하게 발생하게 되어 프로그램의 응답속도가 늦어지다 결국 OOM(OutOfMemory)오류로 프로그램이 종료될 것이다.

결론적으로 GC는 도달할 수 없는(unreachable)객체는 처리하지만 사용되지 않는(unused)객체는 확인할 수 없다. 사용되지 않는 객체는 어플리케이션 logic에 따라 다르므로 개발자는 코드에 주의를 기울여야한다.

memory leak은 다양한 방식으로 발생할 수 있으며, 아래 몇가지 예제가 있다.


Example 1: Autoboxing

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

long대신 Long(래퍼클래스)을 사용함으로써, Autoboxing으로 인해 sum = sum + l; 매 반복마다 새 객체를 생성하므로 총 1000개의 불필요한 객체를 생성한다.
따라서 primitive타입과 래퍼클래스간의 혼합을 피하고, 최대한 primitive타입을 사용해야한다.


Example 2: Using Cache

public class Cache {
    private Map<String, String> map = new HashMap<>();
    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();
    }
}

캐시에 직원의 이름과 직종을 넣었으며, display한 후엔 해당 요소를 캐시에 저장할 필요가 없다.
하지만 이 예제에서는 캐시를 지우는 것을 잊었고 캐시에 있는 객체가 더 이상 어플리케이션에 필요하지 않더라도 Map이 객체에 대한 강력한 참조(strong reference)를 가지고 있기때문에 GC가 되지 않는다. 따라서 캐시의 항목이 더이상 필요하지 않으면 캐시를 반드시 지워줘야한다.

또는 WeakHashMap으로 캐시를 초기화할 수 있다. WeakHashMap의 장점은 다른 객체에서 키를 참조하지 않으면 해당 항목이 GC된다. 하지만 GC가 언제 일어날지 모를 뿐더러, 캐시에 저장된 값을 재사용하려는 경우 키가 다른 객체에 참조되지 않을 수도 있으므로 주의해야한다.


WeakHashMap 예제

/*
* A sample for Detecting and locating memory leaks in Java
* URL: http://neverfear.org/blog/view/150/Java_References
* Author: doug@neverfear.org
*/
public class ClassWeakHashMap {

   public static class Referred {
       protected void finalize() {
           System.out.println("Good bye cruel world");
       }
   }

   public static void collect() throws InterruptedException {
       System.out.println("Suggesting collection");
       System.gc();
       System.out.println("Sleeping");
       Thread.sleep(5000);
   }

   public static void main(String args[]) throws InterruptedException {
       System.out.println("Creating weak references");

       // This is now a weak reference.
       // The object will be collected only if no strong references.
       Referred strong = new Referred();
       Map<Referred,String> metadata = new WeakHashMap<Referred,String>();
       metadata.put(strong, "WeakHashMap's make my world go around");

       // Attempt to claim a suggested reference.
       ClassWeakHashMap.collect();
       System.out.println("Still has metadata entry? " + (metadata.size() == 1));
       System.out.println("Removing reference");
       // The object may be collected.
       strong = null;
       ClassWeakHashMap.collect();

       System.out.println("Still has metadata entry? " + (metadata.size() == 1));

       System.out.println("Done");
   }

}

strong객체는 원본객체의 참조를 잃었기 때문에 GC의 대상이 되어 WeakHashMap(metadata)에서 제거된다.

결과 : 
Creating weak references
Suggesting collection
Sleeping
Still has metadata entry? true
Removing reference
Suggesting collection
Sleeping
Good bye cruel world
Still has metadata entry? false
Done

코드출처 neverfear-Java_References


Example 3: Closing Connections

try
{
  Connection con = DriverManager.getConnection();
  …………………..
    con.close();
}

Catch(exception ex)
{
}

try블록에서 연결(Costly)리소스를 닫으므로 예외가 발생하는 경우 연결이 닫히지 않는다. 따라서 이 연결이 풀로 반환되지 않으므로 memory leak이 발생한다.
따라서 항상 닫는 내용은 finally블록에 넣어야한다.


Example 4: Using CustomKey

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);
    }
}

equals()hashCode()의 재정의를 빠뜨렸기 때문에, 같은 이름의 CustomKey로 map.get()사용해 value를 찾으려해도 찾을 수 없다. 결과적으로 이는 어플리케이션에서 접근할 수 없지만 map에서는 참조되고 있기 때문에 GC대상이 아니며 완벽한 memory leak이다.

결과 : 
Missing equals and hascode so value is not accessible from Map null

equals()hashCode()이전 글을 참고


Example 5: Mutable CustomKey

public class MutableCustomKey {
    private String name;
    
    public MutableCustomKey(String name) {
        this.name = 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
    public 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);
    }
}

equals()hashCode()를 재정의 하였지만, map에 저장된 후 의도치 않게 원본 key의 속성이 변경되었다. 결과적으로 이 entry(key-value)는 프로그램에서 찾을 수 없지만, map에서는 참조되고 있기 때문에 memory leak이 발생한다.
따라서 항상 사용자 지정 key를 immutable하게 만들어야한다.

결과 : 
Value Found Shamik Mitra
Due to MutableKey value not found null

Example 6: Internal Data Structure

public class Stack {
    private int maxSize;
    private int[] stackArray;
    private int pointer;

    public Stack(int s) {
        maxSize = s;
        stackArray = new int[maxSize];
        pointer = -1;
    }

    public void push(int j) {
        stackArray[++pointer] = j;
    }

    public int pop() {
        return stackArray[pointer--];
    }

    public int peek() {
        return stackArray[pointer];
    }

    public boolean 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 = 0; i < 1000; i++) {
            stack.push(i);
        }
        for (int i = 0; i < 1000; i++) {
            int element = stack.pop();
            System.out.println("Poped element is " + element);
        }
    }
}

내부적으로 Stack은 배열을 보유하지만, 어플리케이션 관점에서 스택의 활성 부분은 포인터가 가리키는 곳이다.
따라서 Stack이 1000으로 증가하면 내부적으로 stackArray에 원소가 채워지만, 나중에 모든 원소를 Pop하면 포인터가 0이 되므로 어플리케이션 관점에서는 비어있지만 stackArray에는 Pop된 모든 참조를 포함하고 있다.
Java에서는 이를 폐기된 참조(obsolete reference)라고 부르며, 폐기된 참조란 역참조 할 수 없는 참조이다.
결론적으로, 이 참조는 Pop된 후 필요없지만 내부배열에서 해당 원소들을 보유하고 있기 때문에 GC될 수 없다.

해결방법은 Pop이 발생할 때 null값을 할당해주면 해당 객체가 GC될 수 있다.
(이 예제에서는 primitive type 배열이므로 default값으로 변경해줌)

    public int pop() {
        int size = pointer--;
        int element = stackArray[size];
        stackArray[size] = 0;
        return element;
    }

결론

  1. primitive타입과 래퍼클래스간의 혼합을 피하고, 최대한 primitive타입을 사용하라.
  2. 캐시의 항목이 더 이상 필요하지 않으면 캐시를 반드시 지워라.
  3. Connection은 try블록이 아닌, finally블록에서 닫아라.
  4. Custum key 사용시, key객체의 꼭 equals()hashCode()를 재정의하라.
  5. Custum key는 immutable 하게 만들어라.
  6. 자료구조 내부에서 Pop()된 객체는 꼭 지워라.

출처 : Dzone - Memory Leaks and Java Code

profile
backend-developer🔥

0개의 댓글