Java

Java 메모리 진단

hongyb 2025. 6. 11. 00:12

 

Java는 Garbage Collector(GC)가 알아서 사용하지 않는 객체를 메모리에서 제거한다. 그래서 메모리 릭 현상이 간간히 발생하고, 원인을 잡기 쉽지 않다. 메모리 릭이 발생하면 원인을 어떻게 찾아야 하는지 알아보겠다.


Java 메모리 영역

자바의 메모리 영역은 다음과 같다.

  • PC 레지스터 : 스레드의 JVM인스트럭션 주소가 저장되어 있다. 스레드별 각각 보유한다.
  • JVM 스택 : 지역 변수와 부분 결과를 저장한다. 메서드 호출 및 리턴 정보 보관된다. 스레드별 각각 보유한다.
  • 힙 : 인스턴스와 배열이 할당된다. GC에 의해 관리된다.
  • 메서드 영역 : 모든 JVM 스레드 공유하며, 클래스 구조 정보 저장한다.
  • 런타임 상수 풀 : 클래스 및 인터페이스에 대한 constant_pool 테이블을 실행 시 참고하기 위한 저장소이다.
  • 네이티브 메서드 스택 : 자바 언어 이외의 네이티브 언어를 호출할 경우 타 언어 스택 정보 저장한다.

OutOfMemoryError란

OutOfMemoryError는 가비지 컬렉터가 새로운 객체를 생성할 공간을 만들지 못하고, 힙 영역 메모리가 증가될 수 없을 때 발생한다. OutOfMemoryError가 왜 발생하는지 원인을 알고 싶으면 에러 로그를 잘 확인해봐야 한다. 에러로그를 살펴보겠다.

 

1. Exception in thread “majava.lang.OutOfMemoryError: Java heap space

  • 메모리 크기를 너무 적게 잡아 놓거나, 메모리 크기를 지정하지 않은 경우 : 자바 옵션 중 -Xms는 JVM메모리 최소 크기를, -Xmx는 최대 메모리 크기를 지정한다.
  • 오래된 객체들이 계속 참조되고 있어서 GC가 되지 않는 경우 : static 잘못 사용하거나 애플리케이션이 객체를 지속해서 참조할 경우 → 메모리 릭 발생
  • 스레드 우선순위 높일 경우 : GC 처리속도보다 우선순위 높은 스레드가 메모리에 생성하는 속도가 빨라 문제 발생
  • 큰 덩어리 객체 여러 개 있을 경우

2. Exception in thread “main”:java.lang.OutOfMemoryError: Metaspace

많은 클래스가 자바 프로세스에 로딩될 경우에 발생한다. 메타 영역 크기보다 크게 지정하면 된다. 옵션은 -XX:MaxMetaspaceSize=128m를 사용하면 된다.

3. Exception in thread “main”:java.lang.OutOfMemoryError: Requested array size exceeds VM limit

배열 크기가 힙 영역 코기보다 크게 지정되었을 때 발생

4. Exception in thread “main”:java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space

네이티브 힙 영역이 부족할 때 발생한다. 즉 OS의 메모리가 부족한 경우이다.

5. Exception in thread “main”:java.lang.OutOfMemoryError: <reason> <stacktrace> (Native method)

네이티브 힙 영역 메모리 할당할 때 발생하는 메시지이다. 앞은 JVM코드에서 발견될 때 발생하고 이 경우 JNI나 네이티브 코드에서 발생한다.

[메모리 릭]
메모리 릭이란 객체가 GC 되지 않아 메모리가 부족해지는 현상이다. 코드나 클래스 로더 문제일 수 있다.

잦은 GC

OutOfMemoryError 이외에도 잦은 GC에 의해 문제 발생할 수 있다. GC는 Minot GC와 Full GC 두 가지가 있으며 보통 Full GC가 많이 발생하면 성능에 영향을 미친다. 다음 규칙을 따르면 GC를 발생시키지 않을 수 있다.

  • 임시 메모리 사용 최소화
  • 객체 재사용
  • XML 처리 시 메모리 점유하는 DOM보다 SAX 사용
  • 많은 데이터 한 번에 보여주는 비즈니스 로직 제거
  • 임시 메모리 많이 사용하는 부분 제거

이 외에도 GC가 자주 발생하는 원인은 여러 가지가 있다. 운영하는 서비스에 동시에 1000명이 넘는 사용자가 접속하는 경우 Full GC가 자주 발생하는 것은 당연한 현상이다.

 


jstat 명령어

메모리 단면(힙 덤프)은 메모리가 부족해지는 현상이 지속될 때와 OutOfMemoryError가 발생했을 때 생성해야 한다. 그러면 메모리가 부족해지는지 어떻게 알 수 있을까? jstat 명령어로 간단하게 확인할 수 있다.

 

다음 명령어를 입력하면 1초 간격으로 어떤 메모리 영역에서 얼마나 메모리를 사용하는지 확인할 수 있다.

jstat -gcutil [pid] 1s

E는 Eden영역, S0과 S1은 Survivor영역이다. 두 영역은 Young으로 묶을 수 있다. O는 Old영역으로 Tenured 영역으로 나뉜다. 이 Tenured영역이 GC이후에도 증가하는지 확인하면 된다.

 

보통 Tenured영역은 애플리케이션 동작중에 계속 증가하는 것이 기본이다. 하지만 Full GC이후에도 메모리 사용량이 증가하고 있다면 메모리 릭이 발생하고 있다는 증거이다.

이런 경우에 메모리 단면은 확인해야 하지만 한 가지 주의할 점이 있다. 메모리 사용량 분석할 때 성능에 많은 영향을 주기 때문에 운영 서버에서 확인하면 안 된다. 개발자의 PC나 개발 서버에서 문제점을 확인해야 한다. 메모리 덤프 시 다음 현상이 발생한다.

  • 덤프 파일 생성하는 동안 서비스 불가능
  • 덤프 생성 시 많은 시간 소요
  • 큰 파일이 생성

메모리 단면은 JVM에 있는 객체 하나하나에 대한 정보를 담고 있다. 그래서 생성하는데 오래 걸리고 시스템 운영에 문제가 발생한다.


메모리 단면 생성

jdk에서 지원하는 jmap 명령어로 힙 덤프를 생성할 수 있다. jmap 필요 옵션은 다음과 같다.

  • -dump:[live,] format=b, file=<filename> : 메모리 단면 생성. live옵션은 살아있는 객체만 덤프 하라는 뜻. format을 b로 지정 시 바이너리 형태 파일 생성. file옵션에 지정된 파일 이름으로 생성. 예시) jmap -dump:format=b, file=goldmem.bin [pid]
  • -finalizerinfo : GC가 되려고 기다리고 있는 객체 정보 출력
  • -clstats : 클래스 로더의 통계 정보를 제공
  • -histo[:live] : 힙 영역의 메모리 점유 상태를 가장 많이 점유한 객체부터 출력
  • -F : -dump와 -histo 옵션과 같이 사용. 덤프가 발생되지 않을 경우 강제 발생시킬 때 사용

histo 옵션으로 실행하면 결과는 다음과 같다.

jmap -histo [pid]

메모리를 가장 많이 점유한 객체부터 데이터를 출력한다. 순번, 인스턴스 수 바이트 크기, 클리스 이름 순서로 나열되고 있다. 대문자로 시작하는 클래스는 배열 형태의 클래스를 의미하고 <>로 둘러싸인 클래스들은 JVM에서 사용하는 것이다.

 

몇몇의 JDK에서 jmap을 두 번 이상 사용할 수 없다. 리눅스에서 그런 경우가 발생하면, gcore로 코어 덤프를 발생시키고 그 덤프에 jmap을 사용하면 된다.

gcore -o holdmem.core [pid]

위와 같이 명령어를 입력하면 holdmem.core.[pid] 라는 파일이 생성된다. 파일이름뒤에 .pid 형식으로 프로세스 id가 붙는다. 이 파일을 jmap을 활용하여 메모리 덤프를 생성할 수 있다.

jmap [옵션] [자바 실팽 파일 위치] [코어덤프 파일]
ex) jmap -dump:format=b, file=holdmem.dump /usr/local/java/bin/java holdmem.core.1975

 

자동으로 힙 덤프 생성

다음 옵션을 주면 OutOfMemoryError발생 시 힙 덤프가 생성된다.

-XX:+HeapDumpOnOutOfMemoryError

 

힙을 원하는 정보에 저장하기 위해 다음 명령어를 입력한다.

-XX:HeapDumpPath=[경로]

 

[예시]

java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp

 

다음 옵션은 OutOfMemoryError발생 시 실행되는 스크립트를 명시할 수 있다.

-XX:OnOutOfMemoryError="명령어"

 

다음과 같이 활용하면 된다.

java -XX:+OnOutOfMemoryError="gcore -o gcore.dump %p" 클래스 이름

%p는 현재 실행 중인 자바 프로세스 아이디를 의미한다.

 


메모리 문제 Case

1. 시스템이 느릴 경우

시스템이 느리다고 항상 메모리 단면을 사용하는 것은 아니다. 그래도 원인을 찾지 못할 경우 메모리 분석 단계로 넘어가야 한다. 메모리 때문에 시스템이 느릴 경우에 다음을 의심해야 한다.

  • 메모리를 작게 잡아 GC가 자주 발생하는 경우
  • 임시 메모리 사용하여 GC가 자주 발생하는 경우

이 경우 jstat을 사용하자. 메모리 크기와 관련됐으면 -gccapacity 옵션을, 사용량과 관련됐으면 -gcutil 옵션을 사용하자.

 

2. 애플리케이션 응답하지 않을 경우

메모리 릭이 발생하면 GC 작업만 실행하고, 애플리케이션이 응답하지 않을 수 있다. 메모리 릭일 때 다음 절차를 따르자.

  1. 메모리 사용량 확인한다. 메모리 사용량 95% 이상인 경우 메모리 릭 문제 원인일 수 있다.
  2. 메모리 사용량 지속해서 높을 경우 메모리 단면 생성
  3. apm 사용해 어떤 객체가 죽지 않고 점유되는지 확인한다.

'Java' 카테고리의 다른 글

리눅스 진단 명령어  (0) 2025.06.17
[자바 트러블 슈팅] 스레드 진단  (0) 2025.05.26