Technical Note/JAVA

자바에서는 개발자가 프로그램에서 별도로 메모리 관리를 하지 않는다. 이것은 C/C++에서는 없던 획기적인 것이었다. 자바의 메모리 관리 개념은 개발자를 열광케 하고, 나아가 자바가 세상을 지배하는 언어가 되는데 핵심 역할을 담당하였다 그러나 애석하게도 자바에서는 여전히 메모리 문제가 발생하고있다. C/C±±처럼 빈번하지는 않지만 종종 OutOfMemoryError는 발생하고 있으며, 이러한 메모리 부족 에러는 운영자나 개발자가 가장 껄끄러워하는 문제 중에 하나로 인식되어있다.

자바의 메모리 부족 유형

Out Of Memory Error가 발생하는 원인을 보면 크게 단지 (1)메모리가 부족할 때 (2)Heavy 서비스가 수행되면서 혹은 (3) 메모리 LEAK에 의한 메모리 부족 때문으로 구분할 수 있다. 
(1)의 단지 메모리가 부족하다는 것은 설정된 메모리량이 요구되는 메모리량에 비해 부족하다는 것이다. 프로그램이나 기타 오류가 아닌 단지 값이 적게 설정되었다는 것을 의미하기 때문에 통상적으로는 -Xmx값을 늘려줌으로써 해결되는 것이 보통이다. 실제로는 의외로 JVM 시작시 메모리 옵션을 빠뜨리는 경우가 많다. 혹은 -Xmx256과 같이 메모리 단위(원하는 설정-Xmx256m)를 빠트리는 경우도 있다. 물론 옵션을 잘못 기술하는 것은 단순한 실수이지만, 자바 힙 메모리에 대한 경험이 전혀 없어서 단위 시간당 서비스 호출건수(Service Arrival Rate)에 비해 턱 없이 적은 량의 메모리를 지정하는 경우도 종종 있다.
SUN/HP JVM에서는 perm 영역이 작아서 발생하는 경우가 있다. 메모리의 perm영역에는 클래스 코드 등이 저장되는 공간으로 사용되는 클래스가 많은 경우에는 사이즈를 늘려 주어야 한다. 
(2)두번째로 OutOfMemoryError는 응용서비스(ex Web Request 처리 로직)가 순간적으로 메모리를 과도하게 사용함으로써 발생할 수 있다.
데이터 베이스에서 너무 많은 데이터를 로딩하거나 검색 조건이 잘못되어 검색서버에서 너무 많은 텍스트를 조회하는 경우가 해당된다. 또 다른 경우로 과거에 upload/download프로그램에서 자주 발견되던 것으로 byte[] buffer에서 buffer의 크기를 너무 크게 설정함으로써 연속공간 부족 현상(IBM JVM)이 발생하는 경우나, 동시에 여러 개의 요청이 발생하여 단위 시간당 절대 사용량이 많아 OuOfMemory발생하는 경우를 생각할 수 있다. 물론 후자의 경우는 application이 정상적으로 메모리를 많이 사용하는지 단지 프로그램 오류인지는 판별하기 어려울 수도 있다. 
(3)마지막으로 MEMORY LEAK이 발생하여 메모리가 부족해지는 경우이다. 보다 정확하게 자바에서 메모리 LEAK은 로직적으로 사용되지 않는 객체가 GC될 수 없는 strong reference에 의해 참조되어 메모리 사용량이 증가하는 상태를 말한다. 대부분 적절한 반환을 거치지 않아 발생한다. 대표적으로 JDBC관련 클래스를 사용하고 close()를 호출하지 않게 되어 발생하는 경우를 생각할 수 있다(물론 JDBC에 따라서 혹은 Web Application Server에 따라서 메모리 Leak이 발생하지 않을 수 도 있다.) 
메모리 LEAK의 다른 사례로 Cache의 구현 로직에 문제가 있는 경우이다. CACHE에서 불필요한 데이터의 제거 로직이 정상적으로 동작하지 않는 경우 OutOfmemoryError가 발생할 수있다. 포괄적인 의미에서 Connection Pool에서 불필요한 Connection이 증가하거나, Http Session에서 데이터가 증가하는 현상 또한 이범주에 포함 시킬수 있을 것이다.

OutOfMemoryError 발생시 원인 분석

화면이나 프로그램 로그에서 OutOfMemoryError를 보게 된다면 어떤 경우에 발생하는지를 정확히 판별해야 한다.
단지 설정이 잘못된 경우라면 통상적으로 JVM startup 혹은 startup이후 짧은 시간내에 OutOfMemory가 발생하게된다.
따라서 서버를 start 할 때 OutOfMemoryError를 보게 된다면 옵션 설정을 먼저 의심해 보아야 한다. 
그러나PERM영역이 부족한 경우에는 약간 시간을 두고 발생하게 된다. HEAP메모리가 부족하지 않는데 OutOfMemoryError가 발생하면서 SUN/HP JVM이라면 PERM영역이 부족하지 않은지를 먼저 확인해야 할 것이다.

만약 (2)Heavy Load Application이 수행되어 OutOfMemory가 발생한다면 어떤 현상이 관찰될까? HEAP Memory 그래프를 보면 갑자기 메모리 사용량이 급격히 증가하고 GC가 빈번해지는 현상이 관찰되는 것이 일반적이다. 시간에 따라 메모리 사용량이 증가하지 않고 어느 순간에 메모리 사용량이 증가하고 GC가 빈번해 지는 현상이 발생한다면 특정 Application이 메모리를 과도하게 사용함을 추정할 수 있다. 물론 Active Service가 증가한 이후에 Memory사용량이 증가하고 GC가 빈번해지면 인과관계(메모리가 부족해서 Active Service가 증가 했는지 아니면 Active Service가 증가해서 OutOfMemory가 발생했는지)가 약간 불분명할 수는 있다. 단 필자는Active Service가 증가하여 OutOfMemory가 발생하는 경우는 거의 보지 못했다.

IBM JVM인 경우에 HEAP에 여유가 충분하고 GC가 빈번해지지도 않으면서 갑자기 OutOfMemoryError가 발생한 경우가 있는데 이때는 fragmentation에 의한 연속공간부족을 의심할 필요가 있다

마지막으로 Memory Leak이 발생하여 메모리 부족이 발생하면은 Heap Usage그래프가 시간에 따라 증가되는 현상으로 나타난다. 장시간의 메모리 사용량의 변화를 관찰하면 메모리 릭이 있는지를 확인할 수 있다. 일반적으로 서버 기동후 OutOfMemory가 발생하기 까지 시간이 단 몇시간인 경우도 있으나 한 달이 넘는 경우도 있다.

제니퍼와 OutOfMemoryError 해결

단지 설정이 잘못되거나 너무 적은 메모리를 설정한 경우에는 설정을 수정함으로써 해결하면 OK이지만 특정 Application에 의한 OutOfmemoryError나 MEMORY LEAK이 발생하는 경우에는 해결을 위해 보다 논리적 접근이 필요하다.

특정 application에 의한 불특정 시점에 OutOfMemoryError이 발생하는 것이 확인된다며 어떤 application이 Memory Error를 발생하는지를 찾아야 한다. 만약 해당 Application이 upload나 download와 관련있는 프로그램이라면 데이터를 제어하기 위한 버퍼 사이즈를 먼저확인하고 단순히 DB를 사용하는 프로그램이라면 DB Access 데이터 량을 확인하는 것이 먼저이다. 제니퍼는 서비스 수행중 OutOfMemoryError가 발생하면 ERROR을 발행하고 에러 정보를 저장한다.
통상적으로 메모리를 과도하게 사용하는 서비스가 수행되면 순간적으로 해당 인스턴스의 응답시간/CPU사용량등이 증가하게되며 XVIEW상에서 모든 프로그램의 응답시간이 급격하게 증가하는 현상을 관찰할 수있는데 이때 가장 높은 곳(응답시간이 길거나/CPU사용량이 높음)에 위치하는 것이 문제의 Application인 경우가 많다. 따라서 일자별 XView 데이터 조회를 통해서 해당 시간대를 면밀히 관찰하면 쉽게 찾을 수 있다.

MEMORY LEAK 이것은 원만큼 자바 튜닝에 공력이 붙지 않으면 해결하기 힘들다. 
긴 시간이 필요한데다가 개발 서버에서는 좀처럼 재연되지 않기 때문이다. 
일단 메모리 릭을 확신하기 위해서는 제일 먼저 시간에 따라 메모리가 증가 한다는 것을 감지 하는 것이 중요하다
따라서 짧은 시간의 메모리 사용량이 아닌 하루의 전체 HEAP 메모리 사용량 변화를 확인할 수 있는 화면이 필수이다.(제니퍼 매뉴얼 참조)

IBM JVM을 사용하는 시스템이라면 HEAP DUMP를 통해 쉽게 원인을 찾을 수 있지만
SUN/HP에서는 정말 어려운 문제이다. 특정 application을 여러 번 호출하고 메모리 변화를 확인하는 방식은 개발서버에서는 가능할지는 몰라도 운영 환경에서는 사용이 불가능하다 마찬가지고 -Xrunhprof:heap 옵션을 이용한 HEAP분석 또한 현실적으로 운영환경에서 사용하기 어렵다. APM에서 모든 메모리 릭을 해결하기 위한 기능은 운영환경(production)에서 사용 가능해야 한다.

이런 조건하에서 제니퍼는 메모리 LEAK을 추적할 수 있는 몇가지 기능이 준비되어있다.
일단 제니퍼는 JDBC 자원 미반환은 거의 완벽(일부 특수한 상황은 예외)하게 검출한다. MEMORY LEAK이 발생하면 가장먼저 JDBC 자원 미반환 코드를 수정할 것을 권고한다.

그리고 제니퍼는 Collection/Map클래스사이즈 모니터링 기능을 제공하는데 이는 제니퍼 만이 제공하는 독특한 기능이다. 나는 다수의 메모리 LEAK을 IBM JVM /HEAP DUMP분석을 통해 해결하면서 MEMORY LEAK이 대부분 Collection/Map 계열의 클래스에서 발생한다는 것을 알게 되었다. Cache/Pool 컴포넌트는 통상 내부에 Collection/Map형태의 클래스에 데이터를 저장하고 이것이 잘못되어 메모리 릭을 유발하는 경우가 가장 많았다. 이것을 반대로 보면 어떤 Collection/Map의 elements count가 증가하는지를 모니터링 할 수 있다면 상당히 많은 MEMORYLEAK을 잡을 수 있을 것이다. 제니퍼는 이를 통해 상당수의 실 싸이트에서 타사 제품이 해결하지 못한 메모리 LEAK을 해결한바 있다.

또한 제니퍼 3.2에는 새로운 기능의 Collection 모니터링 기능이 추가되었다. 
지정한 jar파일들에서 모든 static 변수에 선언된 Collection/Map을 검색하고 이것을 
구성파일에 설정함으로써 element count을 추적하는 기능이다.(제니퍼 사용자 메뉴얼 참조)