클래스로더 메모리 누수 원인과 해결법 (ThreadLocal 사용시 주의점)
두 줄 요약
원인은 ThreadLocal을 사용 후 명시적으로 remove 하지 않았기 때문이라고 할 수 있다. 따라서 해결법은 ThreadLocal을 사용 후에는 반드시 `remove()`를 호출해야 한다.
ThreadLocal의 작동 방식
ThreadLocal은 쓰레드마다 각기 다른 값을 저장하는 기능을 제공한다. `ThreadLocal.set(value)` 를 호출하면 현재 쓰레드의 ThreadLocalMap에 value가 저장된다. 이 값은 앱이 종료되거나 ThreadLocal.remove()를 호출하지 않는 한 유지된다.
ThreadLocal이 상당한 메모리를 물귀신 하는 과정
ThreadLocal 변수로 저장된 것이 만약 어떤 클래스의 인스턴스라고 해보자. 이 인스턴스는 자신의 블루프린트인 클래스를 참조하고 있고, 클래스는 자신을 로드한 WebAppClassLoader를 참조한다. 그리고 이 WebAppClassLoader는 자신이 로드한 모든 클래스를 참조한다. 즉 결과적으로 어떤 앱 인스턴스가 ThreadLocal 변수로 어떤 스레드로부터 참조되고 있기 때문에 WebAppClassLoader가 로드한 모든 클래스가 GC의 대상이 되지 못하고 계속 남게 되는 것이다.
메모리 누수를 만드는 방법
직접적으로 메모리 누수를 발생시키기 위해서는 위와 같이 remove()하지 않은 ThreadLocal이 있는 앱을 undeploy했다가 다시 deploy해야 한다. ThreadLocal에 의해 붙잡힌 인스턴스만 아니었다면 앱 컨텍스트가 unload(undeploy) 되면서 WebAppClassLoader가 로드한 클래스들과 그 인스턴스들이 다 GC이 대상이 되었을텐데, 그렇지 못하고 메모리에 계속 저장된다.
앱은 꺼지는데 쓰레드는 안꺼지는 이유
앱(=앱 컨텍스트)이 꺼지는데도 ThreadLocal 때문에 앱 데이터가 줄줄이 다 고아 객체가 된다는 것에서 짐작할 수 있듯이, 앱 컨텍스트와 쓰레드는 별개이다. 톰캣 같은 웹 컨테이너는 쓰레드 풀을 통해 쓰레드를 재사용하도록 되어있고 이 쓰레드풀은 톰캣이 관리하는 것이다. 따라서 앱 컨텍스트가 undeploy 되는 것과 관계 없이 톰캣이 종료되지 않는 한 톰캣이 생성하고 관리하는 쓰레드 풀과 그 쓰레드는 살아 있게 되고, 해당 쓰레드에 저장된 ThreadLocal 또한 살아 있게 되는 것이다.
WebAppClassLoader가 로드한 클래스가 아니면 괜찮다?
사실이다. 실제로 메모리 누수가 발생하는 부분은 앱 관련 메모리들, 즉 WebAppClassLoader가 로드한 데이터들이 앱 undeploy된 후에도 남아있는 것이기 때문에 부트스트랩 클래스 로더나 시스템 클래스가 로드한 기본적인 JDK 클래스나 Java 표준 라이브러리 클래스들은 ThreadLocal로 남아있어도 문제가 없다.