Java 애플리케이션에서 Proxool으로 인한 메모리 누수 문제 진단 및 해결

문제 발생 상황

운영 중인 Java 애플리케이션이 갑작스럽게 응답 지연 현상이 발생했으나, 로그 출력은 정상적으로 유지되고 있었습니다. 이는 시스템 내부에 메모리 관련 이상이 발생했음을 시사하며, 즉각적인 진단과 대응이 필요했습니다.

환경 정보

소프트웨어버전
CentOS6.4
JDK1.6
Proxool0.9.0RC3

문제 원인 분석 절차

  1. top 명령어로 프로세스 상태 확인
    시스템 리소스 사용률을 모니터링하여 메모리 사용량이 급격히 증가하고 있음을 확인했습니다.
  2. jstat으로 가비지 컬렉션 동작 분석
    jstat -gcutil 13062 5000
    결과로 보여진 GC 빈도는 매우 높았으며, 힙 메모리 사용률이 거의 100%에 도달해 지속적인 GC가 발생하고 있었습니다.
  3. jmap으로 최대 객체 크기 조사
    jmap -histo:live 13062 | head -20
    분석 결과, 데이터베이스 연결 관리 클래스들이 힙 공간의 대부분을 차지하고 있었으며, 특히 org.logicalcobwebs.proxool.WrappedConnection 계층이 눈에 띄게 많았습니다.
  4. 힙 덤프 파일 생성
    jmap -dump:format=b,file=heap-dump.bin 13062
    현재 힙 상태를 바이너리 덤프로 저장하여 후속 분석을 위한 기초 자료를 확보했습니다.
  5. Mat 도구로 힙 덤프 분석
    생성된 heap-dump.bin 파일을 Eclipse Memory Analyzer (MAT)에서 열어 분석한 결과, Finalizer 큐에 수만 개의 객체가 쌓여 있으며, 전체 메모리의 약 2.9GB를 차지하고 있었습니다.
  6. jstack으로 스레드 스택 추적
    jstack 13062 > 13062_error.log
    덤프된 스택 정보에서 Finalizer 스레드가 대기 상태이며, 많은 객체들이 finalize() 메서드를 호출할 준비 상태임을 확인했습니다.
  7. 문제 원인 규명
    Proxool의 WrappedConnection 클래스가 finalize() 메서드를 오버라이드하고 있었고, 이 메서드 내부에서 isClosed() 메서드를 호출하는 구조였습니다. 그러나 이 메서드는 synchronized로 선언되어 있어, 실제 연결 사용 스레드와 Finalizer 스레드 간에 락 경쟁이 발생했습니다. 비즈니스 로직이 활발하게 동작할 경우, Finalizer 스레드는 락을 획득하지 못해 작업이 지연되며, 그 결과 해당 객체들은 참조 큐에 머물러 힙에서 제거되지 않아 메모리 누수가 발생했습니다.

해결 방안

문제의 근본 원인이 WrappedConnectionfinalize() 오버라이드 때문임을 확인한 후, 다음과 같은 조치를 취했습니다:

  • WrappedConnection 클래스를 재정의하여 finalize() 메서드를 비어 있는 것으로 오버라이드함.
  • 변경된 클래스를 컴파일 후, 애플리케이션 배포 패키지에 포함.
  • 애플리케이션 재시작 후, 추가적인 모니터링 수행.

결과 검증

패치 적용 후 다음 날 다시 힙 덤프를 생성하고 분석한 결과, Finalizer 큐에 쌓인 객체 수가 사라졌으며, 힙 사용량도 안정화되었습니다. 메모리 누수 현상은 완전히 해소되었습니다.

요약

이 사례는 고급 디버깅 도구(jstat, jmap, jstack, MAT 등)의 적절한 활용이 실시간 애플리케이션 문제 해결에 얼마나 중요한지를 보여줍니다. 특히, 싱글톤이나 연결 풀 관리 컴포넌트에서 finalize()를 잘못 사용하면 심각한 메모리 누수가 발생할 수 있으므로 주의가 필요합니다.

참고 자료

태그: Proxool java memory leak jstack jmap

7월 6일 23:00에 게시됨