참조: http://www.zeroturnaround.com/blog/rjc201/

클래스로더에서 클래스

자바에서 메모리 누수가 발생하는 경우는 보통 정리해야 할 레퍼런스가 남아버려서 그렇다. 클래스로더는 그 중에서도 매우 특별한 경우로 클래스로더가 누수되면 서버에서 애플리케이션을 몇번 재배포 할 때마다 OutOfMemoryError를 보게 될 것이다.

reloading-object

이전 글에서 살펴봤던 내용을 다시 보면, 새로운 클래스를 로딩할 때마다 이전 클래스로더는 버리고 매번 새 객체를 만들고 이전 객체 그래프를 복사해왔다.

모든 객체는 자신의 클래스를 참조하고 있고, 또 해당 클래스는 자신을 로딩한 클래스로더를 참조하고 있다. 또! 모든 클래스로더는 자신이 로딩한 모든 클래스들을 참조하고 있으며 각 클래스들은 해당 클래스 내부의 static 필드를 들고 있다.

classloader-refs

(캬.. 정말 멋진 그림이닷.)

즉..

  1. 만약에 클래스로더가 누수되면 그 클래스로더가 들고 있는 모든 클래스와 static 필드를 들고 있게 된다.(즉 GC되지 않는다.) static 필드는 보통, 캐시, 싱글톤, 애플리케이션 상태나 설정 정보로 사용한다. 직접 작성한 코드에 static 필드가 없더라도 사용중인 라이브러리에서 static 필드를 사용중일 수도 있으니 클래스로더 누수는 심각한 상황을 초래할 수 있다.
  2. 어떤 클래스로더를 누수시키려면 그 클래스로더로 로딩한 어떤 클래스의 어떤 객체에 대한 레퍼런스 하나만 남겨두는 걸로 충분하다. 객체가 아무리 무해해 보이더라도 그 객체는 분명히 자신의 클래스로더를 참조하고 있는 것이고 즉 모든 애플리케이션의 상태를 들고 있는 것이나 마찬가지다. 어느 한 곳에서라도 적절하게 정리되지 않고 레퍼런스가 남아버리면 누수가 발생한다. 특히 써드파티 라이브러리에 그런 문제가 있다면 고치기 어렵다.

메모리 누수를 만들어 보자.

이전 코드에 ILeak과 그 구현체 Leak을 추가하는데 특이한 점은 Leak 클래스가 ILeak 타입의 멤버 변수를 하나 들고 있다는 점인데.. 이 멤버변수를 사용해서 누수를 시킬겁니다.

public class Leak implements ILeak {
  private ILeak leak;

  public Leak(ILeak leak) {
    this.leak = leak;
  }
}

그리고 Example에다가 ILeak 타입의 멤버 변수를 추가하고 복사하는 와중에 leak이 누수 되도록 코딩합니다.

//Example.java

public class Example implements IExample {
  private int counter;
  private ILeak leak;
  private static final long[] cache = new long[1000000];

public ILeak leak() {
  return new Leak(leak);
}

public IExample copy(IExample example) {
  if (example != null) {
    counter = example.counter();
    leak = example.leak();
  }
  return this;
}

이렇게요. 이러면 없어져야 할 오래된 leak이 계속 남아서 새로운 Example의 leak 변수의 leak 변수에 담겨서 유지되겠죠. 이때.. 메모리를 먹게 하려고 크기가 좀 큰 배열을 같이 만들어 뒀습니다. 여기서 주목할 건.. Leak이 누수 됐는데 왜 Example의 배열 때문에 누수가 나느냐 입니다. 왜인지는 이미 위에서 설명했듯이

classloader-leak

leak –> Leak 클래스 –> 클래스로더 –> Example –> 엄청큰 배열

2

 

정확하게 보려면 힘덤프를 뜨고 덤프 분석 툴로 보면 되는데.. 그건… 다음에~

ps: 소스 코드는 참조한 링크에 보시면 Resources에 있습니다.