자바 Memory Leaks

Jeongmin Yeo (Ethan)·2021년 2월 15일
7

Java

목록 보기
4/4
post-thumbnail

자바 Memory Leaks에 대해 알아보고 정리합니다.

학습할 내용은 다음과 같습니다.

  • Memory Leaks 이란?
  • Types of Memory Leaks in Java

References


Memory Leaks 이란?

자바에서 Memory Leaks 이란 더이상 사용하지 않는 객체가 가비지 컬렉션(GC)에 의해서 회수되지 않고 계속 누적되는 현상입니다.

그러므로 올드 제너레이션 영역에 해당 객체는 계속해서 누적되서 빈번히 Full GC가 발생하며 Full GC가 발생하는 데도 Heap Memory 변화가 별로 없을 것입니다.

그러다 결국 응답속도가 늦어져 OutOfMemory 오류가 발생해 프로그램이 종료됩니다.

이 글에서는 자바에서 빈번하게 등장할 수 있는 Memory Leaks 현상들을 살펴보고 이를 예방하는 방법을 알아보겠습니다.


Types of Memory Leaks in Java

1. Memory Leak Through static Fields

자바의 Memory Leak은 스태틱 변수의 무분별한 사용 때문에 일어날 수 있습니다.

왜냐하면 자바에서 스태틱 변수의 라이프 사이클은 애플리케이션이 종료되기 전까지 살아있습니다.

예제는 다음과 같습니다.

public class StaticTest {
    public static List<Double> list = new ArrayList<>();

    public void populateList() {
        for (int i = 0; i < 10000000; i++) {
            list.add(Math.random());
        }
        Log.info("Debug Point 2");
    }

    public static void main(String[] args) {
        Log.info("Debug Point 1");
        new StaticTest().populateList();
        Log.info("Debug Point 3");
    }
}

이 프로그램을 실행하는 동안 힙 메모리를 분석해보면 populateList() 메소드를 호출한 후 힙 메모리는 크게 증가할 것입니다.

하지만 그 후 debug point 3에서 힙 메모리를 분석해보면 스태틱 변수이므로 가비지 컬렉터가 메모리를 회수 하지 않으므로 여전히 스태틱 변수는 힙에서 메모리를 차지할 것입니다.

더 이상 사용하지 않는 경우에 대해서 메모리를 환원하기 위해서는 스태틱 변수에서 static keyword를 제거해야 합니다.

그 경우라면 debug point 3에서 list 변수는 더 이상 참조하지 않으므로 가비지 컬렉터에 의해 메모리는 환원될 것입니다.

스태틱 변수를 사용해야하는 경우에는 Singleton Pattern이 있을 수 있습니다.

대부분의 Singleton Pattern에서 Thread-safe와 메모리 성능을 잡기 위해서 lazy-loading 방법인 Double-checked locking을 사용하라는 말이 있습니다.

하지만 Double-checked locking은 다중 프로세서 머신에서 보장할 수 없습니다.

그러므로 이 경우에는 성능보단 안정성을 잡기위해 lazy-loading을 하지 않는걸 추천합니다.

2. Through Unclosed Resources

자바에서 데이터베이스 커넥션이나 Input Stream 같은 걸 사용할 때 즉 새로운 커넥션을 만들거나 스트림을 사용할 때 이 리소스를 close() 하지 않는다면 메모리 누수가 발생할 수 있습니다.

이 문제를 해결하기 위해서는 try-finally block을 통해 리소를 반환하도록 할 수 있지만 자바 7 이후에 들어온 try-with-resources block을 통해 사용할 리소스를 선언해두고 자동으로 반환하도록 하면 됩니다.

왜냐하면 finally block에서 close() 메소드를 호출 할 때 이 경우에도 예외가 발생할 수 있으므로 try-with-resources block을 사용하는 걸 추천합니다.

3. Improper equals() and hashCode() Implementations

새로운 클래스를 정의할 때 간과하는 실수 중 하나가 equals()와 hashCode() 메소드를 재정의 하지 않는 것입니다.

이 메소드들은 컬렉션 클래스인 HashSet과 HashMap에서 빈번히 사용하고 이 메소드들이 정의되어 있지 않으면 메모리 누수가 발생할 수 있습니다.

예시는 다음과 같습니다.

// Person 클래스 정의
public class Person {
    public String name;
    
    public Person(String name) {
        this.name = name;
    }
}

// HashMap을 통한 메모리 누수 예제
@Test
public void givenMap_whenEqualsAndHashCodeNotOverridden_thenMemoryLeak() {
    Map<Person, Integer> map = new HashMap<>();
    for(int i=0; i<100; i++) {
        map.put(new Person("jon"), 1);
    }
    Assert.assertFalse(map.size() == 1);
}

매번 Jon이라는 중복된 Person 객체를 HashMap에 삽입하고 있는 예제입니다.

하지만 hashCode() 메소드가 재정의 되어있지 않으므로 이 매번 생성된 중복된 객체들 HashMap에서 다른 키로 인식되어 삽입되어 힙 메모리에 많이 쌓일 것입니다.

이 문제를 해결하기 위해선 HashMap에서 올바른 객체를 찾기 위해 사용되는 새로운 객체의 equals() 메소드와 객체를 삽입할 때 사용하는 새로운 객체의 hashCode() 메소드를 재정의해야 합니다.

다음과 같이 재정의 할 수 있습니다.

public class Person {
    public String name;
    
    public Person(String name) {
        this.name = name;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (!(o instanceof Person)) {
            return false;
        }
        Person person = (Person) o;
        return person.name.equals(name);
    }
    
    @Override
    public int hashCode() {
        int result = 17;
        result = 31 * result + name.hashCode();
        return result;
    }
}

4. Inner Classes That Reference Outer Classes

자바에서 내부 클래스를 이용할 땐 조심해야 합니다. 내부 클래스를 생성할 땐 외부 클레스에 대한 참조를 유지하므로 이로인해 메모리 누수가 발생할 수 있습니다.

아래와 같이 내부 클래스와 외부 클래스가 있다고 가정해봅시다.

public class EnclosingClass
{
   public class EnclosedClass
   {
   }
}

이 클래스를 컴파일 한 후 javap(Java Print) tool을 통해 disassemble 해보면 다음과 같이 내부 클래스는 외부 클래스의 참조를 가지고 있는 걸 볼 수 있습니다.

Compiled from "EnclosingClass.java"
public class EnclosingClass$EnclosedClass {
  final EnclosingClass this$0;
  public EnclosingClass$EnclosedClass(EnclosingClass);
}

즉 이를 통해 메모리 누수가 발생할 수 있습니다. 외부 클래스에서 내부 클래스를 생성하는 메소드가 있다고 했을 때 내부 클래스를 생성할 때마다 외부 클래스 멤버 필드에 엑세스 할 수 있는 참조를 가질 것입니다.

이는 내부 클래스가 GC 되기 전까지 참조를 유지할 것입니다. 그러므로 내부 클래스에서 외부 클래스에 대한 참조가 불필요하다면 내부 클래스를 static class로 바꾸는게 좋습니다.

5. Autoboxing

public class Adder {
       public long 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);
              }
       }
}

이 예제에서 sum 변수는 long 타입의 primitive 변수가 아니라 Wrapper Class를 이용했습니다.

Wrapper Class는 Immutable한 특징이 있으므로 매번 sum = sum + l 연산을 할때마다 새로운 오브젝트가 생겨날 것입니다.

즉 불필요한 오브젝트가 생겨날 수 있으므로 가능하다면 primitive 변수를 사용하는 걸 추천합니다.

6. Using WeakHashMap

public class WeakHashMapTest {
 
    public static void main(String[] args) {
        WeakHashMap<Integer, String> map = new WeakHashMap<>();
 
        Integer key1 = 1000;
        Integer key2 = 2000;
 
        map.put(key1, "test a");
        map.put(key2, "test b");
 
        key1 = null;
 
        System.gc();  // Garbage Collection
 
        map.entrySet().stream().forEach(el -> System.out.println(el));
 
    }
}

// expected output
2000=test b

일반적인 HashMap의 경우에는 Key와 Value가 put되면 사용여부와 관계없이 해당 내용은 삭제되지 않습니다.

즉 Map안의 Element들이 일부는 사용되고 일부는 사용되지 않을 수 있는 경우에도 삭제되지 않습니다.

하지만 키 값의 객체가 null로 된다면 이제 더 조회되지 않으므로 GC 되야 한다고 생각할 수 있습니다.

WeakHashMap은 WeakReference의 특성을 이용해 Key에 해당하는 객체가 더는 사용되지 않는다고 생각하면 Element를 자동으로 GC 해버립니다.

profile
좋은 습관을 가지고 싶은 평범한 개발자입니다.

0개의 댓글