[Java] IBM JDK에서 native out of memory가 발생하는 이유와 해결책

IBM JDK를 사용할 때 (gc policy는 보통 gencon) 물리 메모리가 많이 남아있는 경우에도 native out of memory (NOOM)가 발생하는 경우가 있다.
NOOM이 발생하지 않았더라도 global gc 가 자주 발생하여 성능에 영향을 주기도 한다.
verbose gc 로그를 보면 sys-start reason="native out of memory" 라는 이유로 global gc가  실행되는 것을 볼 수 있다.

이렇게 global gc가 자주 발생하면 자바의 가장 큰 골치거리인 Stop-The-World 현상이 발생하여 순간적으로 멈춰서게 되어 매우 불안정하게 되는데 이 문제의 원인은 무엇인지 알아보자.

이 문제가 발생하는 경우는 IBM JDK (버전 6 이후)를 사용하고 64비트 JVM, 전체 Java Heap 크기를 4GB 이상 25GB 이하로 줬을 때이다.

-Xmx 값이 4GB~25GB이면 IBM 64bit JVM은 기본값으로 -XcompressedRefs 옵션을 켠 것과 동일한 방식으로 동작한다.
자바의 객체들은 JVM 내부에서 2-word 크기의 헤더를 가지는데 32bit JVM에서는 2-word는 8byte이며, 64bit JVM에서는 2-word가 16byte가 된다.

64bit 방식으로 자바 객체를 addressing하게 되면 사용할 수 있는 메모리 영역은 훨씬 더 커지겠지만 메모리를 과다 사용하고 OS의 page cache fault 등의 문제로 성능적으로도 느려질 수밖에 없다.

그래서 64bit JVM에서도 32bit 방식으로 자바 객체 주소를 addressing하는 방식이 바로 compressed reference라는 개념이다.

compressed reference 방식으로는 최대 32GB까지 주소를 나타낼 수 있다.
이 경우 Java Heap에 있는 자바 객체들에 대한 주소는 shift 연산을 통해 32bit 주소의 한계인 4GB를 넘어서 최대 32GB까지 참조할 수 있게 되지만, Java Heap 밖에 (즉, native 영역에) 존재하는 클래스 객체, 모니터 객체, 쓰레드 객체 등은 여전히 4GB 주소 안에 있어야 한다. 자바 객체 헤더에서 참조하는 클래스, 모니터, 락 등에 대한 포인터가 32비트로 표현되기 때문이다. (테스트를 해보면 live class 객체들만 4gb 크기에 들어있으면 된다. Unloading을 안해서 전체 클래스 객체가 8~9gb인 경우에도 live 객체들 크기가 4gb 범위 안에 있으면 noom warning이 나질 않는다. 아마도 live pointer들을 간접 포인팅하는 식으로 구현된 것 같다.)

native OOM이 발생하는 원인 중 하나는 주로 클래스 객체가 너무 많이 사용되어 이 영역이 4GB를 다 차게 되는 경우이다.
일반적으로 복잡한 자바 업무들을 구현하면 자바 코드들이 1GB를 차지하는 경우는 종종 볼 수 있다.
좀 지나치게 많은 업무 코드들을 모두 하나의 JVM에서 올리려고 하면 2.5GB를 넘어서게 될 수 있는데 이렇게 되면 일반적으로 다른 모니터와 쓰레드가 차지하는 영역까지 합쳐서 4GB를 육박하게 된다.




<그림 : JVM 프로세스 메모리 맵> "Using -Xgc:preferredHeapBase with -Xcompressedrefs"에서 인용
이런 경우에는 코드들을 효과적으로 서로 다른 JVM으로 분리하여야 불필요한 global gc를 막을 수가 있다.

참조 :


댓글

이 블로그의 인기 게시물

[Java] Java G1 GC의 특성에 따른 Full GC 회피 튜닝 방법

일론 머스크의 First Principle Thinking (제1원리 기반 사고)

엄밀한 사고(Critical Thinking)란 무엇일까