문제 발생 상황
운영 중인 Java 애플리케이션이 갑작스럽게 응답 지연 현상이 발생했으나, 로그 출력은 정상적으로 유지되고 있었습니다. 이는 시스템 내부에 메모리 관련 이상이 발생했음을 시사하며, 즉각적인 진단과 대응이 필요했습니다.
환경 정보
| 소프트웨어 | 버전 |
|---|---|
| CentOS | 6.4 |
| JDK | 1.6 |
| Proxool | 0.9.0RC3 |
문제 원인 분석 절차
- top 명령어로 프로세스 상태 확인
시스템 리소스 사용률을 모니터링하여 메모리 사용량이 급격히 증가하고 있음을 확인했습니다. - jstat으로 가비지 컬렉션 동작 분석
jstat -gcutil 13062 5000
결과로 보여진 GC 빈도는 매우 높았으며, 힙 메모리 사용률이 거의 100%에 도달해 지속적인 GC가 발생하고 있었습니다. - jmap으로 최대 객체 크기 조사
jmap -histo:live 13062 | head -20
분석 결과, 데이터베이스 연결 관리 클래스들이 힙 공간의 대부분을 차지하고 있었으며, 특히org.logicalcobwebs.proxool.WrappedConnection계층이 눈에 띄게 많았습니다. - 힙 덤프 파일 생성
jmap -dump:format=b,file=heap-dump.bin 13062
현재 힙 상태를 바이너리 덤프로 저장하여 후속 분석을 위한 기초 자료를 확보했습니다. - Mat 도구로 힙 덤프 분석
생성된heap-dump.bin파일을 Eclipse Memory Analyzer (MAT)에서 열어 분석한 결과,Finalizer큐에 수만 개의 객체가 쌓여 있으며, 전체 메모리의 약 2.9GB를 차지하고 있었습니다. - jstack으로 스레드 스택 추적
jstack 13062 > 13062_error.log
덤프된 스택 정보에서Finalizer스레드가 대기 상태이며, 많은 객체들이finalize()메서드를 호출할 준비 상태임을 확인했습니다. - 문제 원인 규명
Proxool의WrappedConnection클래스가finalize()메서드를 오버라이드하고 있었고, 이 메서드 내부에서isClosed()메서드를 호출하는 구조였습니다. 그러나 이 메서드는synchronized로 선언되어 있어, 실제 연결 사용 스레드와Finalizer스레드 간에 락 경쟁이 발생했습니다. 비즈니스 로직이 활발하게 동작할 경우,Finalizer스레드는 락을 획득하지 못해 작업이 지연되며, 그 결과 해당 객체들은 참조 큐에 머물러 힙에서 제거되지 않아 메모리 누수가 발생했습니다.
해결 방안
문제의 근본 원인이 WrappedConnection의 finalize() 오버라이드 때문임을 확인한 후, 다음과 같은 조치를 취했습니다:
WrappedConnection클래스를 재정의하여finalize()메서드를 비어 있는 것으로 오버라이드함.- 변경된 클래스를 컴파일 후, 애플리케이션 배포 패키지에 포함.
- 애플리케이션 재시작 후, 추가적인 모니터링 수행.
결과 검증
패치 적용 후 다음 날 다시 힙 덤프를 생성하고 분석한 결과, Finalizer 큐에 쌓인 객체 수가 사라졌으며, 힙 사용량도 안정화되었습니다. 메모리 누수 현상은 완전히 해소되었습니다.
요약
이 사례는 고급 디버깅 도구(jstat, jmap, jstack, MAT 등)의 적절한 활용이 실시간 애플리케이션 문제 해결에 얼마나 중요한지를 보여줍니다. 특히, 싱글톤이나 연결 풀 관리 컴포넌트에서 finalize()를 잘못 사용하면 심각한 메모리 누수가 발생할 수 있으므로 주의가 필요합니다.