본문 바로가기

서버운영 (TA, ADMIN)/미들웨어

[tomcat] 톰캣 메모리릭 문제

https://knight76.tistory.com/947

 

톰캣 메모리릭 문제에 대해서 많이 좋아질것으로 예상된다. 스프링소스의 개발자이며, 톰캣 Committer인 Mark Thomas에 의해 Memory Leak Protection 기능이 새로 생기게 되었다. 원래는 톰캣7부터 적용하려고 했는데, 톰캣 6.0.24부터 적용되었다고 한다. 6.0.25버전에서는 Memory leak 체크 api도 지원한다. 

 

Mark는 OOME가 발생하는 원인을 톰캣이 아닌 Jvm, library라고 언급하였고, 그 예는 다음과 같다고 한다.

 

(1) Application, Library code에 의해서 발생

     A. JDBC driver

     B. Logging framework

     C. ThreadLocal의 object를 저장하고 remove하지 않아서

     D. 쓰레드를 시작시키고 멈추지 않아서

(2) Java Api에 의해서도 발생

     A. Using the javax.imageio API (the Google Web Toolkit can trigger this)

     B. Using java.beans.Introspector.flushCahces() (Tomcat does this to prevent memory leaks caused by this caching)

     C. Using XML parsing (the root caouse is unknown due to a bug in the JRE)

     D. Using RMI (somewhat ironically, causes a leak related to the garbage collector)

     E. Reading resources from JAR files

 

이런 문제가 나타나는 것의 이유 중의 하나는 java의 Class Loader에서는 아직 object에 대해서 de-register 기능이 없다. 그래서 톰캣 web class loader는 이런 부분을 지원했다.

 

Mark는 이런 OOME 문제 해결을 위해서 다음을 하였다.

org.apache.catalina.loader.WebappClassLoader 클래스에 clearReferences() 메소드를 추가하여 톰캣에 의해 load된 object들을 de-register하여 gc가 되도록 하였다. PermGen을 싹 지우고 시작을 하게된다.

 

또한 org.apache.catalina.core.JreMemoryLeakPreventionListener 클래스를 추가하여 원인파악을 쉽게 할 수 있도록 api를 제공하였다. Mark는 java.util.logging처럼 JRE에서 로딩되는 경우에 대해서는 Tomcat단에서 처리할 수 없으니, 다른 LogManager를 쓰라고 권고하고 있다.

 

Static ThreadLocal의 경우도 해결이 힘들었는데, Tomcat 6.0.24는 아래와같이 detect를 하고 webclassloader에서 ThreadLocalMap을 inspect함으로써 잘 테스트했다고 적혀있다.

Mar 17, 2010 10:23:13 PM org.apache.catalina.loader.WebappClassLoader clearThreadLocalMap

SEVERE: A web application created a ThreadLocal with key of type [java.lang.ThreadLocal] (value [java.lang.ThreadLocal@44676e3f]) and a value of type [test.leak.threadlocal.value.MyCounter] (value [test.leak.threadlocal.value.MyCounter@62770d2e]) but failed to remove it when the web application was stopped. To prevent a memory leak, the ThreadLocal has been forcibly removed

 


https://atin.tistory.com/438

 

Apache Tomcat6에서 구동하던 웹애플리케이션에서 다음과 같은 에러를 발견하였다. 이 에러는 톰캣을 시작하거나 종료할때는 발생하지 않는다. 오직 톰캣을 완벽하게 내리지않고, 해당 애플리케이션을 재시작하였을때에만 발생한다. 운영에는 문제는 없다.

 

위에서는 3개의 에러가 났다. JDBC와 관련되어서 2개가 발생하였고, 세번째는 ThreadPool에서 관리하는 쓰레드를 중지하지 못해서 난것이다. 톰캣에서 발생한 이 메모리 누수의 발생 이유는 위를 참고한다.

 

A. JDBC Driver, D.쓰레드를 시작시키고 멈추지 않아서이다. "D.쓰레드를 시작시키고 멈추지 않아서"로 발생한 에러를 해결하는 방법에 대해 써보겠다. 우선 Java에서 쓰레드에 대해 얘기해보자. Java에서 쓰레드는 Thread클래스를 상속 받거나 Runnable 인터페이스를 구현해서 만들어진다. 그리고 Thread클래스의 start() 메소드를 통해 시작하고 stop() 메소드를 통해 중지되어진다.

 

그러나 한 두개의 쓰레드를 쓸 경우가 아니라면, 쓰레드를 관리하기 위해서 ThreadPool을 사용한다. 위 문제는 ThreadPool을 사용하면서 발생하였다. 웹 애플리케이션의 종료시에 ThreadPool에서 관리하는 쓰레드들을 shutdownNow 메소드를 이용하여 중지시켰다. 그러나 shutdownNow메소드는 내부에서 관리되는 쓰레드들을 중지시키지 못했다.

 

우선 Thread의 stop메소드와 ExecutorService의 shutdownNow에는 차이가 있다. stop 메소드는 쓰레드를 무조건 중지시킨다. 설령 내부에 멈출수 없는 무한반복이 있더라도 중지시킨다. 그러나 ExecutorService의 shutdownNow는 그렇지 못한다. shutdownNow는 내부에서 관리되고 있는 쓰레드들에게 인터럽트를 호출해줄 뿐이다.

 

그리고 이것이 쓰레드가 종료되지 않은 이유이다. 왜냐하면 shutdownNow 메소드가 stop메소드와 같이 중지가 될것이라 생각하고 쓰레드 내부에 while(true)와 같은 무한반복문을 넣었던 것이다. 그리고 내부에는 인터럽트 되어 지지 않는 sleep과 같은 메소드를 통해서 대기를 하고 있었다.

 

그러므로 ThreadPool을 통해 사용되어지는 쓰레드는 일반 쓰레드를 구현할때와 달라야만 한다.


Java ThreadPool에서 쓰는 쓰레드 설계시, 유의사항

1) 무한 반복문

어떤 형태로든지 쓰레드가 끝나면 상관없지만, 웹 애플리케이션과 생명주기를 같이하는 쓰레드라면 무한반복을 하게된다. 그런데 이때 while(true)와 같이 사용하면 안된다. 이부분은 자바 병렬 프로그래밍에 대해서 공부하면 나온다. while(isInterrupted())와 같이 사용해야 한다. Runnable의 구현체라면 while(Thread.currentThread().isInterrupted()) 와 같이 사용해야 한다.

 

2) InterruptedException catch

InterruptedException이 무엇이냐면 외부에서 쓰레드에 대하여 인터럽트를 발생시켰는데 쓰레드가 특정 상태에 빠져서 인터럽트가 걸리지 않아서 발생하는 예외이다.

 

대표적인 것이 Thread클래스의 sleep 메소드이다. 다른 예로는 LinkedBlockingQueue의 take메소드이다. take메소드는 큐가 비어있다면 큐에 새로운 객체가 들어오기전까지 대기하게 된다. 즉 sleep메소드처럼 대기하게 되고 이 상태에서는 인터럽트가 걸리지 않게 되는 것이다. 그러므로 InterruptedException이 발생할 수 있는 메소드는 반드시 잡아내어서(catch) 중지해야 한다.

while(!isInterrupted()) {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        break;
    } catch (Exception e) {
        // ...
    }
}