Java 프로그래밍: Out Of Memory 오류

Java로 소프트웨어를 개발하는 사람이라면 누구나 한번쯤 Out Of Memory Error(이하 OOME)를 만나 보았을 것이다. OOME의 경우 Java의 다른 Error와 다르게 원인과 대응을 하기가 쉽지는 않다.

경험에 비추어 보면 OOME가 발생하는 시점은 대부분 개발이 대부분 완료된 후 사용자 테스트 혹은 인수 테스트단계에서 많이 발생한다. 즉 개발 단계에서 수행하는 단위 테스트의 경우 목적 기능에 대한 검증 위주로 진행 되기 때문에 식별이 어렵고 가동 초기 단계 혹은 이와 유사한 테스트 환경에서 주로 발생하게 되는 것이다. 때문에 OOME가 발생하는 시점에서는 빠르게 대응해야 하는데 경험에 의한 JVM Option을 통한 처리 방법과 Dump 파일의 분석을 통해 대응을 하게 된다.

JVM Option

Java의 최대 장점은 한번 만들어진 애플리케이션은 JVM이 존재하는 OS/하드웨어에서도 동작을 한다는 것이다. 그러나 이는 동작을 한다는 것이지 성능을 보장하지는 않는다. 때문에 OS/하드웨어별로 JVM의 설정을 변경하여 성능을 보장하기 한 것이 JVM옵션이다. JVM옵션은 JVM표준인 Standard 옵션과 Non-Standard옵션으로 구분 된다.

Standard Option : JVM표준 옵션으로 벤더(JVM을 만드는 회사. Oracle(Sun),IBM,HP,..)와 상관없이 동일한 옵션을 가진다. 32/64Bit,클라이언트/서버 모드 설정과 같이 환경위주의 설정

Non-Standard Option : JVM표준이 아니라 Java의 버전,벤더에 따라 추가되거나 없어 지기도 한다. 그러나 성능에 직접적으로 영향을 주는 옵션들이다. 주로 -X,-XX 로 시작된다.

참조 : http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

Dump 파일 분석

애플리케이션 운영 중 장애 혹은 성능 상 문제가 발생하였을 때 애플리케이션의 상태를 스넵샷 형태로 파일로 저장한 것이 Dump파일이다. 때문에 Dump파일의 분석을 통해 장애 발생 시 애플리케이션이 어떠한 상태 였으며 어떤 점이 문제가 되었는지 확인 할 수 있다.
분석에는 여러 가지 Tool들이 있으며 VisualVM,Eclipse mat 등이 있다. 이를 사용하는 방법은 다음 기회에 설명하도록 하겠다.

아래 내용은 최근 프로젝트를 수행하면서 OOME가 발생하였을 때 오랜만에 다시 공부해본 Java의 메모리 구조에 대해 정리한 내용이다.

Java Virtual Machine의 구조와 JVM Option

OOME는 다양하게 발생하는데 대부분 다음 예외를 많이 접하게 될 것이다.

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap spac
Heap size의 부족으로 Java Object를 Heap에 할당하지 못하는 경우. JVM 옵션 설정을 하지 않은 경우 많이 발생한다.

Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space
Class나 Method 객체를 PermGen space에 할당하지 못하는 경우 발생하며 애플리케이션에서 너무 많은 class를 로드할 때 발생한다. 주로 잘못된 설계/구현에 의해 발생한다.    -XX:PermSize, -XX:MaxPermSize Option을 이용하여 오류를 수정하기도 한다.

Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
사용할 배열의 사이즈가 VM에서 정의될 사이즈를 초과할 때 발생한다.

Exception in thread “main”: java.lang.OutOfMemoryError: request bytes for . Out of swap space?
Java는 런타임시 물리적 메모리를 초과한 경우 가상메모리를 확장해 사용하게 되는데 가용한 가상메모리가 없을 경우 발생한다.

Exception in thread “main”: java.lang.OutOfMemoryError: (Native method)
JVM에 설정된 것 보다 큰 native메모리가 호출 될 때 발생한다.

위와 같은 OOME를 해결하기 위해서 먼저 JVM구조를 이해하고 있어야 한다.

그림-1 Java Virtual Machine Model

JVM은 위에서 보는 것과 같이 여러 영역으로 구성이 되어 있으며 JVM Option을 통해 각 영역별로 메모리 할당이 가능하다. 일반적으로 OOME가 발생하면 각 영역별로 메모리 옵션을 늘려 대응을 하게 된다.

아래 그림은 각 영역들과 JVM option을 통해 관리가 가능한 방식을 설명한 것이다.

그림 2 HotSpot JVM 구조와 영역별 Option

하지만 불행하게도 시스템의 물리적 리소스는 한정이 되어 있어 한 영역의 메모리 사이즈를 늘리게 된다면 다른 영역의 메모리는 줄어 들어 또 다른 문제가 발행할 수도 있다. 예를 들어 전체 2G 메모리 사용 중이고 Heap영역이 1.5G    PermGen space 영역이 256MB를  할당하였다. 이때 PermGen space에서 OOME가 발생하여  516MB로 늘리게 되면 Heap영역은 1G로 줄여들게 된다. 때문에 Dump파일의 분석을 통해 정확한 진단을 한 후 원인을 수정하도록 한다.

잘못된 Application과 OOME

위에서 설명한 영역들에서 발생하는 OOME 중 오늘 이야기 하고자 하는 영역은 Permanent Space에서 발생하는 오류 이다. 보통 잘못된 코딩으로 인해 발생하게 되는 OOME의 대부분은 Permanent Space에서 발생하게 되는데 최근 프로젝트에서 발생한 오류도 Permanent Space에서 발행한 오류였다. 친절하게도 java에서는 OOME 가 발생한 영역을 아래와 같이 설명해 준다.

그림 3 Java Out Of Memory Error 예시

Perm Gen space에서 발생한 오류에 대해 대응하기 전에 Perm Generation 영역에 대해서 알아보면 Permanent Generation은 young과 old를 구분하는 Generational Collector 방식인 HotSpot JVM 중 한 영역으로 객체의 생명 주가기 길다고 판단되는 객체들을 이 영역에 할당하여 GC대상에서 제외를 하기 위해서 만들어진 영역이다.  주로 자바의 Class 객체들이나 문자열에 속한 String 객체들이 위치한다.

일반적으로 Class의 로딩은 시스템의 Class path에 의해서 로드된 Class 객체들과 에플리케이션 내 구현으로 다이나믹하게 로드되는 class들이 있는데 주로 문제는 애플리케이션 내 로직으로 다이나믹 하게 생성되는 Class들에 의해서 발생된다. 최근에 많이 사용되는 Spring, MyBatis등과 같은 프레임워크 등이 이와 같은 방식을 취하고 있다. 때문에 위 프레임워크들을 사용할 경우 OOME발생에 주의를 해야 한다.

최근에 발생한 원인은 이들 프레임워크 중 iBatis(구 MyBtis)를 잘못 사용하여서 발생한 것으로 iBatis를 잘못 사용하여 발생한 경우로 볼 수 있다.

프로젝트에서 iBatis를 사용하기 위한 기본 아키텍처 구조는 다음과 같다.

그림 4 iBatis Dao를 이용한 데이터 접근

iBatis의 dao.xml파일 내 사용할 DAO객체 정보를 설정하고 런타임 시 DaoFactory에서 dao.xml을 읽어 DAO객체를 인스턴스화 하는 구조로 다음 같이 구현하였다.

public class BPSimulationDaoFactory extends DaoFactory {

    /** singleton instance */
    private static BPSimulationDaoFactory daoFactory = new BPSimulationDaoFactory();

    /**
     * {@link BPSimulationDaoFactory} 싱글톤 인스턴스를 리턴한다.
     * 
     * @return {@link BPSimulationDaoFactory}
     */
    public static BPSimulationDaoFactory getInstance() {
        return daoFactory;
    }

    /**
     * 신계약 시뮬레이션 정보 DAO 리턴
     * @return
     */
    public AgrmSimulationDao getAgrmSimulationDao(){
        return (AgrmSimulationDao) daFactory.getDao(AgrmSimulationDao.class);
    }
public InsKindPL getInsKindPL(SearchCond searchCond, List<Dimension> productSearchs)
    	 throws Exception {
        PLStatementsDao dao = 
        	BPSimulationDaoFactory.getInstance().getPLStatementsDao();
        InsKindPL insKindPL = 
        	dao.readSimpleInsKindPL(searchCond.getProjectNo(), searchCond.getVersion());
    }

이 과정에서 iBatis는 내부적으로 DaoManager 클래스를 다이나믹하게 생성하고 로드하는 과정이 존재한다. 이때 단순하게 이 클래스만 로드되는 것이 아니라 이 클래스에 연관된 다른 클래스들(위 그림에서 보면 색으로 표시된 부분)도 같이 로드되고 위에서 설명한 것처럼 Perm Gen spac에 할당된다

때문에 가급적 메모리의 부하를 적게 하게 위해서 설계 원칙 중 하나로 도메인 별로는 하나의DaoFactory만 생성하고 이를 싱글톤 패턴을 적용하여 관리 하고 있으며 이미 여러 프로젝트를 통해 검증된 방식으로 OOME가 발생할 수 없는 구조로 볼 수 있다.

그러나 Dump파일을 분석한 결과 DAOManager클래스 객체가 Perm Gen space에 매우 많이 쌓여져 있는 것으로 분석되었다.

실제 구현 소스를 살펴 보니 어찌된 이유 인지 같은 DB에 접속하는 동일 도메인 내에서 여러 개의 DaoFactory가 구현되어 있다. 원인을 살펴본 결과 각 영역별 담당자별로 DaoFactory를 만들어 사용하고 있던 것이 문제였다.

그림 5 설계 방식과 실제 구현방식의 비교

이를 원래 설계 원칙 대로 수정한 결과 더 이상 OOME에 대한 오류는 발생하지 않았으며 정상 동작이 되었다.  이와 같이 아무리 잘 설계된 아키텍처라 하더라도 잘못된 구현으로 인해 OOME가 발생할 수 있기 때문에 항상 조심하여야 한다.

마치며

Java라는 언어가 이전에 탄생된 언어에 비해서 메모리 관리에 있어 편리한 장점이 있다고 알려져 있고 많이 개발자들이 애플리케이션을 개발하게 되면서 메모리관리에 대해서는 GC가 해결을 해 줄 것이라고 생각하고 JVM의 구조를 이해하거나 메모리 영역에 대해서는 개발 단계에서 많이 고려하지 않는다. 그러나 최근은 고객의 요구사항은 점점 다양해지고 시스템도 같이 복잡해져 가면서 메모리 문제는 언제 어디서나 마주 칠 수 있다. 비록 과거에 비해 시스템의 사양이 좋아져 대부분이 애플리케이션이 많은 메모리를 이용할 수 있다 하더라도 자원은 늘 한정되어 있는 법이니까.

Java의 Reflaction 기능을 이용하거나 이용하거나 Spring이나 MyBatis와 같은 프레임워크를 사용하는 경우는 메모리에 대한 고려를 하여 설계를 하여야 한다. 또한 JVM의 메모리 구조를 알고 OOME가 발생하였을 때 빠르게 대처 할 수 있는 능력을 가져야 하는 것이 개발자로서 필요한 소양이 아닐까 한다

참조 URL