'클래스로더'에 해당되는 글 1건

  1. 2013.08.13 [번역] java.lang.OutOfMemoryError: PermGen space는 왜 발생하나?

현재 우리나라 자바 엔터프라이즈 개발 시장에서 가장 빈번히 사용되고 있고, 그에 걸맞게 강력한 기능을 제공하는 개발 프레임워크는 단연 스프링 프레임워크일 것입니다. (여담입니다만, 혹자는 "소프트웨어 프레임워크는 그저 유행일 뿐이다" 라고 얘기하기도 하지만, 저 개인적으로는 분명 그 발전에는 방향성이 있다고 생각합니다. 소프트웨어 공학, 혹은 공학을 넘은 철학이 그 방향성을 잡아주고 있다고 할까요.) 사실상의(de facto) 표준이라 불리고 있으며, 이 프레임워크의 핵심 기능 중 하나인 의존성 주입(Dependency Injection) 개념은 이미 JavaEE6에 정식으로 채택되어 사용되고 있을 정도입니다. 

(또 여담입니다만, 프레임워크를 개발하는 엔지니어 입장에서, 너무 강력한 프레임워크가 나오니 허탈해졌다라고 할 정도랄까요. 그런데 어쩌겠습니까... 좋으면 제대로 알고 잘 쓰고, 이를 확장해 나갈 줄 알아야겠죠. ㅋ)


그런데, 스프링 프레임워크를 이용하여 이클립스에서 웹 개발을 해 보신 분들이라면 어플리케이션이 여러번 재배포 되었을 때 어느 순간 java.lang.OutOfMemoryError:PermGen space라는 오류가 발생하면서 WAS(혹은 서블릿 컨테이너)가 정상적으로 동작하지 못하게 되는 것을 본 적이 있을 것입니다. 그리고 그럴 때마다 WAS를 재기동하셨을 것입니다. 아, 물론 그것이 스프링 프레임워크의 잘못이라는 것은 아닙니다. 하지만 대부분의 여러분들은 "스프링 프레임워크를 쓰지 않던 시절보다 더 빈번히 이 오류를 보게" 되었을 것이라는 말입니다. 


그럼 도대체 이 에러는 왜 발생하는 것일까요? 어떻게 하면 제거할 수 있을까요? 이번에 번역한 글은 이에 대한 내용을 다루고 있습니다. 이 글을 통해 JVM이 인스턴스를 만들 때, 어떤 메커니즘이 내부적으로 수행되는지, 클래스에 대한 정보는 어디에 저장되는지, 그리고 그것들이 제대로 처리되지 않을 때 어떤 문제가 발생하는지 알게 될 것입니다. 아울러, 이 글에서는 다루고 있지 않지만, 마지막에 JVM 실행 옵션을 이용하여 어플리케이션 재배포 시에 발생하는 PermGen space 에러가 발생하지 않도록 하는 방법에 대해서도 다뤄 보겠습니다. 


서론이 길었습니다. 이제 시작하도록 하죠. 이 번역문의 원문은 http://plumbr.eu/blog/what-is-a-permgen-leak에서 찾아볼 수 있습니다.




What is a PermGen leak?


이 글에서 다룰 내용은 자바 어플리케이션 내에서 발생할 수 있는 특정 메모리 문제에 대한 실제적인 소개이다. , Stack trace 상에서 가끔씩 보는 java.lang.OutOfMemoryError: PermGen space의 원인에 대해 분석할 것이다.

그에 앞서 이 주제를 제대로 이해할 수 있도록, 이와 관련된 핵심 개념을 다뤄보기로 하겠다. 객체는 무엇인지, 클래스와 클래스로더(ClassLoader)는 무엇인지, 그리고 JVM 메모리 모델은 무엇인지도 같이 다룰 것이다. 이 개념들이 익숙하다면 바로 이 다음 장으로 넘어가도 된다. 그 다음, PermGen 메모리 누수의 전형적인 두 가지 경우를 서술하고 그에 대한 힌트나 그것을 해결하기 위한 제안을 다룰 것이다.


Objects, Classes and ClassLoaders

뭐 완전 기초부터 다룰 생각은 없다. 이 글을 읽는 사람이라면 자바 내의 모든 것들이 객체(Object)라는 개념에 익숙할 것이다. 그리고 모든 객체들은 클래스에 의해 정의된다는 것도, , 모든 객체는 그 객체에 대한 클래스의 구조(Structure)를 기술하는 java.lang.Class 객체의 참조(reference)를 갖고 있는 것도 알 것이다.

하지만 코드 내에서 새로운 객체를 생성할 때, 내부적으로는 도대체 어떤 일이 일어나는 것일까? 예를 들어, 다음과 같이 정말로 복잡한 코드를 작성했다고 했다면 말이다.


Person boss = new Person();


자바 가상 머신(Java Virtual Machine, JVM)이 객체를 생성하려면 객체의 구조를 이해해야 한다. 그리고 그러기 위해, JVMPerson이라는 클래스를 찾는다. 그리고 이 어플리케이션이 실행되는 과정에서 이 Person이라는 클래스에 최초로 접근하였다면, JVM은 이 클래스를, 정상적인 경우, Person.class라는 파일로부터 로드한다. 드라이브(디스크 드라이브)로부터 Person.class 파일을 검색하고, 그것을 메모리에 적재하고 그것의 구조를 분석(parsing)하는 것을 클래스 로딩이라고 한다. 그리고 클래스를 제대로 로딩하는 것을 보장하는 역할은 클래스로더(ClassLoader)가 맡고 있다. 클래스로더들은 java.lang.ClassLoader 클래스의 인스턴스(instance)인데, 자바 프로그램 내의 모든 개별 클래스들은 동일한 클래스로더에 의해 로드 되어야 한다. 어쨌든 여기까지 다음 그림과 같은 관계를 얻게 된다.



, 다음 그림에서 볼 수 있듯, 모든 클래스로더는 자신이 로드한 모든 클래스에 대한 참조를 보유하고 있다. 이 그림은 우리가 다루고자 하는 내용과 아주 밀접하니 주의 깊게 살펴보기 바란다.




다시 한 번 말하지만, 이 그림은 다음에 또 필요하게 될 것이다.


Permanent Generation

현존하는 거의 모든 JVM, 자바 클래스들에 대한 내부적인 표상(internal representation)을 위해, Permanent Generation(줄여서 PermGen)이라고 하는 구분된 메모리 영역을 사용한다. PermGen은 그 외에 추가 정보를 저장하기 위해서도 사용되지만, -관심이 있다면 그에 대한 상세한 내용은 포스트를 참고하기 바란다. 이 글에서는 클래스에 대한 정의만 저장된다고 가정하기로 하자. 필자가 사용하고 있는 Java1.6이 실행되고 있는 장비에서PermGen 영역에 대한 기본 크기는 그리 대단할 것 없는 82MB이다.


 이전 포스트에서 다뤘듯이, 메모리 누수(Memory Leak)라고 하는 것은, 어떤 객체들이 더 이상 사용되지 않고 있지만 가비지컬렉터(Garbage Collector, GC)가 그 클래스들이 사용되고 있지 않다는 것을 인지하지 못하는 것이다. 이것이 반복되어, 사용되지 않는 객체들이, 어플리케이션의 다음 메모리 할당 요청이 처리될 수 없을 만큼, heap 메모리를 점유하게 되면 OutOfMemoryError가 발생하게 된다.

java.lang.OutOfMemoryError: Permgen space의 원인도 이와 완벽하게 동일하다: JVM이 새로운 클래스 정의를 로드해야 하는데 이를 수행하기 위한 PermGen 공간이 충분하지 않기 때문이다. , 이미 너무 많은 클래스들이 로딩되어 있다는 말이다. 이것이 발생하는 것은, 어플리케이션이나 서버가 현재 크기의 PermGen이 처리할 수 없을 만큼 많은 클래스들을 사용하고 있기 때문일 수도 있다. 그것이 아니라면, 메모리 누수 때문일 수 있다.


Permanent Generation Leak

그런데, 도대체 어째서 PermGen 영역에서 메모리 누수가 발생하는 것일까? PermGen 영역에는 클래스에 대한 정의가 등록되어 있을 것이고, 이것들이 사용되지 않게 되는 경우는 없을 건데 말이다. 혹시 그런 경우가 있는 것일까?

실제로, 그런 경우가 있다. 어플리케이션 서버에 배포되어 있는 자바 웹 어플리케이션이 서버로부터 제거(undeployed)되면 그 어플리케이션과 관계된 EAR/WAR내의 클래스들은 모두 쓸모 없어진다. 어플리케이션 서버가 여전히 사용 중에 있으므로 JVM은 동작을 계속할 것인데, 이 수많은 클래스에 대한 정의들이 더 이상 사용할 필요가 없어진 것이다. 이들은 PermGen에서 제거되어야 하고, 만약 그렇지 않다면 PermGen 영역에서 Memory Leak이 발생한 것이다.

Tomcat의 개발자들은 Tomcat 6.0.24와 그 이후 버전에서 발생한 다양한 메모리 누수 상황들과 그것을 해결하는 방법을 다룬 위키 페이지를 작성해 두었다. 참고하기 바란다.


Leaking Threads

클래스로더 누수가 발생하는 다른 시나리오는 스레드들의 실행이 너무 길어질 때 이다. 이것은 당신이 작성한 어플리케이션이나, (내 경험으로는 다음 경우가 더 많았는데) 당신의 어플리케이션이 사용한 서드파티 라이브러리가, 과도하게 오랫동안 실행되는 스레드들을 사용할 때 발생할 수 있다. 이것의 한 예로, 특정 코드를 주기적으로 수행하는 타이머 스레드 같은 것이 있을 수 있다.

스레드의 수명이 지정되지 않았다면, 바로 문제가 될 소지가 있다. 어플리케이션의 어떤 부분이 스레드를 하나라도 시작했다면, 그 스레드가 어플리케이션보다 더 오래 살아남지 않도록 제어해야 한다. 개발자들이 이것을 책임져야 한다는 것을 아예 모르거나, 이것을 처리하는 클린업 코드를 작성하는 것을 깜빡하는 것은 흔한 일이다.

이 처리를 제대로 하지 않았다면, 스레드들은 웹 어플리케이션이 제거(undeployed)된 이후에도 계속 동작하게 될 것이고, 그 스레드는 웹 어플리케이션이 시작할 때 사용된 Context ClassLoader라 불리는 클래스로더에 대한 참조를 보유하게 될 것이다. 이 말은 즉, 제거된 어플리케이션의 모든 클래스들이 메모리 내에 지속적으로 남아있게 될 것이라는 거다. 해결 방법은? 어플리케이션이 스레드를 시작했다면, 반드시 어플리케이션 제거 과정에서 Servlet Context Listener를 이용하여 그 스레드들을 멈추고 제거해야 한다. 만약 서드파티 라이브러리를 사용했다면, 그것이 실행한 스레드를 중지하기 위한 셧다운 훅(shutdown hook)을 찾아보아야 한다. 만약 그런 것이 없다면 버그 리포팅을 해야 한다.


Leaking Drivers

메모리 누수의 또 다른 전형적인 경우는 데이터베이스 드라이버에 의해 발생된다. 우리는 Plumbr에 탑재한 우리의 데모 어플리케이션에서 이 문제가 발생하는 것을 본 적이 있었다. 이 데모 어플리케이션은 스프링 프레임워크에서 제공하는 Pet Clinic 어플리케이션을 아주 조금 수정한 것이다. 이 어플리케이션을 어플리케이션 서버에 배포하는 과정에서 발생했던 일을 간략히 기술해 보겠다.

l  서버는 새로운 java.lang.ClassLoader 인스턴스를 생성하고 그것을 사용하여 어플리케이션의 클래스들을 로딩하기 시작한다.

l  PetClinic 어플리케이션은 HSQL 데이터페이스를 이용하므로, 그것을 사용하기 위해 그에 상응하는 JDBC 드라이버, org.hsqldb.jdbcDriver를 로딩한다.

l  이 클래스는, 제대로 만들어진 JDBC 드라이버이니, JDBC 규격에서 정의된 대로 초기화 과정에서 java.sql.DriverManager에 등록(register)한다. 이 등록 과정에서 드라이버매니저의 정적 필드(static field) org.hsqldb.jdbcDriver의 인스턴스에 대한 참조를 저장하게 된다.

이제 어플리케이션이 어플리케이션 서버에서 제거되어도, java.sql.DriverManager는 여전히 그 참조를 보유하고 있게 될 것이다. HSQLDB나 스프링 프레임워크나 어플리케이션 내의 어떤 코드에서도 그 참조를 제거하지 않으니 말이다. 위에서 설명했듯이, JDBC 드라이버 객체는 여전히 org.hsqldb.jdbcDriver에 대한 참조를 유지하게 되는데, 이것은 또 어플리케이션을 로드하는 데 사용된 클래스로더에 대한 참조를 포함하고 있을 것이다. 그리고 이 클래스로더가 이 어플리케이션의 모든 클래스에 대한 참조를 포함하고 있다. 우리의 데모 어플리케이션의 경우에서, 어플리케이션이 기동될 때 거의 2000개 정도의 클래스들이 로딩되는데, PermGen 영역에서 대략 10MB 정도를 사용한다. 이 말은 어플리케이션을 5~10회 가량 재배포하면 PermGen의 기본 크기를 모두 차지하게 되고 그로 인해 java.lang.OutOfMemoryError: PermGen space 에러가 발생하게 될 것이라는 말이다.

해결하는 방법은? 한 가지 방법은 Servlet Context Listener에 어플리케이션이 셧다운 되는 동안 드라이버매니저에서 HSQLDB 드라이버를 등록 해제하는 코드를 작성하는 것이다. 이는 아주 간단하게 처리할 수 있다. 하지만 이 드라이버를 사용하는 모든 어플리케이션에 코드를 작성해야 할 것이다.

최신 버전의 Plumbr와 데모 어플리케이션을 다운로드하여 메모리 누수가 발생하는지 확인하고, Plumbr가 그것을 어떻게 찾아내고 우리가 그것을 어떤 방식으로 설명하는지 확인해보기 바란다.


Conclusion

어플리케이션에서 java.lang.OutOfMemoryError: PermGen space 가 발생하는 데에는 수많은 이유가 있다. 가장 주된 원인은 어플리케이션이 셧다운 된 이후에도 어플리케이션의 클래스 로더에 의해 로딩된 클래스나 클래스에 대한 참조들이 남아있기 때문이다. 혹은 클래스로더에 대한 직접적인 링크 때문일 수도 있다. 어쨌든 이것을 해결하는 방법은 대부분의 경우에 유사하다고 할 수 있다. 먼저 여전히 참조가 유지되고 있는 곳을 찾아내고, 그 다음, 어플리케이션이 제거되거나 셧다운 될 때 이들을 해제할 수 있는 셧다운 훅을 어플리케이션에 추가한다. 이것은 Servlet Context Listener나 서드파티 라이브러리가 제공하는 API를 이용하여 처리할 수 있을 것이다.




이 글은 Permgen Space나 클래스로더, 그리고 심지어 OutOfMemory에 대해 지나치게 단순화하여 설명하는 경향이 있습니다. 대신, java.lang.OutOfMemory : PermGen space 오류가 왜 발생하는지에 대해서는 깔끔하게 잘 설명하고 있죠. 목표로 하는 내용 외의 기타 등등의 세부사항을 적절히 배제한 것이 이해를 돕는데는 큰 도움이 되네요.

참고로, 자바에서 OutOfMemory는 메모리 누수에 의해서만 발생하는 것은 아닙니다. 그 외 여러 가지 경우에서도 발생할 수 있는데, 추후에 이에 대해 별도의 포스트로 언급해 보도록 하겠습니다.

또, 클래스로더 역시 이 글에서 다룬 것과 같이 아주 단순하지만은 않습니다. 이에 대해서도 따로 다뤄보도록 하겠습니다.


아참, 그런데... 이 원글은 스프링 프레임워크에 대해서는 전혀 다루지 않고 있는데, 저는 왜 스프링 프레임워크를 굳이 언급한 것일까요? 스프링 프레임워크의 어떤 특징 때문에 이 문제가 좀 더 빈번하게 발생한다고 생각하고 있는 것일까요? 혹시 그것이 아니라면, 사용자들이 스프링 프레임워크를 뭔가 잘 못 사용하기 때문에 이 문제가 빈번하게 발생하는 것은 아닐까요? 이 부분은 여러분 스스로 생각해 보시기 바랍니다. (최근에 어떤 책을 읽었는데, 가장 중요한 부분은 스스로 생각하라고 하더군요... 하지만 그 책의 문제는, 가장 중요한 것은 그렇다 쳐도 그 밖에 중요하다고 할 만한 것은 아무것도 다루고 있지 않는다는 것이었습니다.)


마지막으로, 자바 실행 옵션 중, PermGen 영역에 로딩되어 있는 클래스들을 메모리에서 해제하고 다시 로딩할 수 있도록 하는 것이 있습니다. 위에서 언급한 메모리 누수가 아닌, 단순 어플리케이션 재로딩시에 발생하는 Permgen space에러를 해결하는 데에 다음 실행 옵션이 도움이 될 것입니다.

-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled


그 외 자바 실행 옵션에 대한 상세한 내용은 [여기]에서 확인해 보시기 바랍니다.

신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요


티스토리 툴바