힙 메모리 오버플로우
힙 메모리에서 OutOfMemoryError (OOME) 가 발생하는 이유는, 객체를 너무 많이 저장해서다.
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class Main {
public static void main(String[] args) {
List<ttt> lt = new ArrayList<>();
while(true) {
ttt t = new ttt();
lt.add(t);
}
}
}
class ttt {
Main stack = new Main();
}
단적인 예시로, 무한히 한 객체를 생성하는 코드.
이 코드를 실행시키면 얼마 안 있..

위와 같은 OOME가 발생하게된다. 콜론(:) 이후에 어디서 OOME가 발생했는지 적혀있는데, 바로 힙메모리 (Java heap space)에서 발생했다고 적혀져있다.
객체들을 무한히 생성되고 그 객체가 여전히 참조되고 있다면 언젠가는 힙메모리에 용량이 가득 차버리게 되고 오버플로가 일어나게 될 것이다.
OOME가 발생했다면 우선 어디서 OOME가 발생했는지 추적해야하는데, 보편적으로 사용되는 도구는 이클립스 MAT.
이클립스 MAT은 자바의 힙 메모리의 현재 활성 중인 모든 객체의 현황들을 보여준다
우선 MAT을 설치해보자면..
Memory Analyzer(MAT) 툴 이용 Heap Dump 분석
운영중이던 작은 서비스에서 그동안 없던 힙덤프가 생성되었다며 분석을 해 볼 기회가 생겼다. 관련 툴을 다운받고 어떻게 힙덤프파일을 보는지 간단히 기록해보고자 한다. Heap Dump 분석하기 생
renee.tistory.com
https://pbg0205.tistory.com/15
eclipse error( Could not create Java Virtual Machine)해결 방법
Cooper's devlog eclipse error( Could not create Java Virtual Machine)해결 방법 본문 eclipse eclipse error( Could not create Java Virtual Machine)해결 방법 cooper_dev 2020. 7. 7. 10:36 1. Error 확인 -오늘 eclipse를 실행하려고 하던 와
pbg0205.tistory.com
MAT을 다운받아서 몇 가지 설정을 한 후에 추가로 IntelliJ에서 힙 덤프를 저장하도록 추가로 설정해야한다. MAT 설정은 위 두 블로그 링크를 보면 되고 힙 덤프 추가 옵션은 쉬운 편이라서 GPT에게 물어물어 설정할 수 있다.

설정을 마치고 실행시키면 dump.hprof 파일이 생기는데, MAT에서 이걸 불러오면 된다.
보면 hprof 크기가 6GB가 되는데ㄷㄷ.. 이게 오버플로우 에러가 생겨서 힙 메모리에 객체들을 꽉꽉 눌러담다보니 이정도로 용량이 커졌다. MAT에서 불러오는데도 거의 5분 정도 걸린다.
-Xms20m -Xmx20m
이 vm options을 통해서 힙메모리의 크기를 20mb로 조정할 수 있다.
그리고 다시 힙 메모리 오버플로우를 일으키면 MAT을 통해서 누수된 메모리가 어느 객체 때문에 누수되었는지, 어느 스레드에서 누수되었는지 등을 알 수 있다.


이렇게 MAT의 분석자료에 출력된다.
20MB의 크기의 힙메모리의 96%가 점유되어 더 이상 객체를 저장할 수 없는 상태임을 나타낸다.

이외에도 어느 클래스의 개수가 가장 많은가도 명시된다. 반복적으로 생성한 ttt 클래스가 50만 개가 생성되었고, 그 ttt 클래스를 호출하는 Main 클래스도 50만 개 생성되고 하는 등의 정보를 알 수 있다.

어느 스레드에서 점유율이 가장 높은지도 명시한다.
이처럼 힙메모리 OOME가 발생하면 먼저 어느 객체 때문에 OOME가 발생했는지 살펴봐야한다. 만약 OOME의 원인이 참조값을 해제하지 않아 생긴 메모리누수라면, 메모리 누수가 일어나는 부분의 코드를 바꿔야한다.
메모리 누수가 아니라면, 위의 VM options 명령어를 통해 힙메모리의 가용공간을 조절할 수 있다. 당연한 얘기지만 조절범위는 컴퓨터의 실제 가용 메모리를 고려해야한다. 가령 위의 예시에서 메모리 누수가 일어나지 않고, 모든 ttt 객체가 필요하다면 힙메모리의 크기를 20MB에서 100MB로 조정할 수 있다. 그리고 코드에서 수명주기가 너무 길거나 너무 오래 할당되는 객체를 찾아서 런타임 가용 메모리를 최대한으로 늘려야한다.
가상 머신 스택 | 네이티브 메서드 스택 오버플로
https://dev-dx2d2y-log.tistory.com/84
[CS] 자바의 정석 독서 #6 - 클래스와 메서드
생각해보니까 다음주부터 시험공부도 시작하면서수요일에 발표과제도 해야하고금요일에 C프 과제제출도 해야하고 주말에 알바도 가야하고다음주까지 이팩티브 자바 읽어야하는데 그러기 위
dev-dx2d2y-log.tistory.com
1. 스택에서 메서드 하나가 실행되면 실행되는 스택 프레임 위로 새로운 스택 프레임이 생긴다. 이 스택 프레임들의 깊이가 허용범위를 넘어서면 오버플로우가 생긴다. 위의 글에서도 다뤘던 StackOverflowError 다.
2. 스택 메모리를 동적으로 확장할 수 있으나, 더 이상 확장할 메모리가 부족한 경우 OutOfMemoryError가 발생한다.
다만 핫스팟 가상머신은 스택 메모리의 동적 확장 기능을 지원하지 않는다. 따라서 2번의 사례는 스레드가 생성될 때 메모리 용량이 부족한 경우에만 OOME가 발생하고, 스택 용량이 부족할 경우에는 동적으로 스택 메모리를 확장하지 않고 그대로 StackOverflow 에러가 발생한다.
public class Main {
public static void main(String[] args) {
try {
ttt t = new ttt();
t.go();
} catch (Throwable e) {
System.out.println("스택 깊이 : " + ttt.gip);
throw e;
}
}
}
class ttt {
Main stack = new Main();
static int gip = 0;
public void go(){
gip++;
go();
}
}

가장 대표적인 StackOverflowError를 발생시키는 방법은 무제한적인 재귀함수
위를 보면 메서드가 약 2만 번 호출되고 나서 더 이상 스레드에 스택프레임을 올릴 수 없어 에러가 발생했다.
-Xss180k
이 VM options을 통해서 스택 메모리의 용량을 줄일 수 있다. 180kb로 스택 메모리의 용량을 설정한다는 의미이며, 64비트 윈도우 환경과 JDK17을 사용한다면 180k 아래로 스택 메모리의 용량을 줄일 수 없다.

이걸보면 같은 코드임에도 불구하고 스택 메모리의 용량을 180kb로 설정하니 약 1500번 정도 메서드가 불러와진다.
다만 핫스팟 자바머신을 단일스레드에서 구동할 경우, 스택 프레임이 너무 커서 메인 스레드에 지정된 용량을 벗어난 경우만 StackOverflow가 발생하고, OOME는 발생하지 않는다, 그래서 StackOverflow 에러가 발생해도 스택 프레임이 너무 큰지, 아니면 가상 머신 스택의 용량이 부족한지 알 수 없다.
(사실 핫스팟 가상머신에서는 가상 머신 스택의 용량이 부족한 것이 결국 현재 환경에서 스택 프레임이 너무 큰 것이기에 둘을 구분하지는 않는다.)
만약 핫스팟 가상머신이 아니라 스택 메모리 확장을 지원하는 다른 가상머신을 사용했으면 스택 프레임이 너무 커서 스레드에 지정된 용량을 벗어났다면 자연스럽게 스택 메모리를 확장하면 되므로 계속 메모리를 확장하다가 어느 순간 확장할 수 없게되어 OOME가 발생했을 것이다. 다만 스택 메모리 확장을 지원하는 가장머신은 JDK1.0.2 이전의 핫스팟 머신 등장 이전의 클래식 VM에 해당되고, 요즘 가상머신들은 거진 스택 메모리 확장을 지원하지 않는단다.
그래서 핫스팟 가상머신에서 스택 메모리의 OOME를 볼 수 있는데, 바로 스레드를 여러 개 만드는 것이다. 핫스팟 가상머신에서는 스레드를 여러 개 만들다가 더 이상 스택 메모리에서 스레드를 저장할 수 없다면 OOME가 발생하기 때문이다. 이것도 시연해보면 좋겠지만 자바 스레드를 운영 체제의 커널 스레드와 매핑시키기 때문에 스레드를 많이 만들면 컴퓨터가 힘들어한다. 그래서 굳이... 너무 많이 스레드를 만들면 운영 체제가 뻗어버릴 수도 있다.
암튼 StackOverflowError가 발생했다면 어디서 예외가 일어났는지 정보가 제공되므로 문제되는 부분으로 가면 된다. 그리고 대부분의 경우도 스택 깊이가 20000 수준으로 깊어지는 경우도 적으므로 StackOverflowError가 발생했다면 재귀탈출부분을 작성하지 않았거나 어딘가에 실수가 있는 경우가 있다고한다.
다만 진짜 문제는 OOME가 발생했는데 더 이상 줄일 스레드도 없고 64비트 가상머신도 사용할 수 없는 경우라면, 결국에는 힙 메모리의 용량과 스택의 크기를 줄여야한다.
힙 메모리의 경우에는 운영체제에서는 프로세스 하나 당 최대 가용용량을 지정하는데, (32비트 윈도우에서는 2GB가 한 프로세스가 사용할 수 있는 최대용량) 이 가용용량에서 힙 메모리의 최대 크기, 메서드 영역의 최대 크기, 가상머신이 동작하며 소비하는 메모리를 뺀 나머지 부분이 스택 영역으로 지정된다.
하지만 VM설정으로 다룰 수 있는 부분은 힙 메모리의 최대 크기 (-Xms, Xmx), 스택의 최대크기(-Xss) 뿐이다. 따라서 힙메모리의 용량을 줄이면 스택 영역으로 지정되는 용량이 그만큼 많아지기 때문에 스택 영역에 들어갈 수 있는 스레드의 수가 많아진다.
스택의 크기는 왜 줄여야하냐면, (-Xss)로 스택의 크기를 다룰 수 있는데, 스택의 크기라는 것이 결국에는 스레드의 전체크기이자 부수적인 구조체의 집합이기 때문이다. 그래서 스택의 크기를 줄인다는 것이 스레드 한 개의 크기를 줄이는 것이고, 스레드 한 개에 들어가는 크기를 줄여서 스택 영역에 들어가는 스택의 양을 늘릴 수 있기 때문이다.
다만 32비트 JVM에서 이러한 스택 오버플로우를 주의해야하는 것이고, 64비트 JVM으로 넘어오면서 저장가능한 용량이 크게 늘어났기 때문에 64비트 JVM을 쓴다면 굳이 신경쓸 필요까지는 없을 것이다.
메서드 영역 | 런타임 상수 오버플로
메서드 영역의 주 역할은 타입 관련 정보의 저장이다. 클래스의 상속정보, 인스턴스 정보, 상수 풀, 필드 설명, 메서드 설명 등이 메서드 영역에 저장된다.
리플랙션 API나 동적 생성 시나리오(런타임에 클래스를 반복적으로 호출할 수 있도록 함)를 통해서 메서드 영역에서 오버플로를 일으킬 수 있으나, 영구 세대가 메타플레이스로 바뀐 JDK8부터는 잘 일어나지 않는다.
다만 여전히 메서드 영역의 오버플로를 막기위해
-XX:MaxMetaspaceSize (메타스페이스의 최대 크기를 설정. 기본값은 -1. 제한없음.)
-XX:MetaspaceSize (바이트단위로 메타스페이스의 초기 크기 설정. 크기가 가득차면 MaxMetaspaceSize 이내 범위에서 가비지 컬렉터가 크기를 조정함)
-XX:MinMetaspaceFreeRtio (가비지 컬렉션 후 최소한 이정도의 비율은 빈 공간으로 남겨두어야한다고 설정함. 부족할 경우 자동으로 메서드 영역 크기 조정)
여담으로 예전에는 String 클래스의 intern() 메서드를 사용하면 해당 문자열으 메서드 영역에 문자열 풀에 저장했다. 그래서 JDK6까지는 intern() 메서드를 반복적으로 사용한다면 메서드 영역에서 OOME가 발생했다. 에러메시지는 힙메모리에서의 OOME 메서지가 PermGen space (영구세대)로 바뀐 것. 그러나 JDK7부터는 문자열 상수 풀이 힙메모리로 옮겨갔기 대문에 .intern() 메서드를 무한히 사용해도 힙메모리의 용량에 손을 대지 않으면 무한루프를 돌 것이고, 힙메모리의 용량에 손을 대면 힙메모리 오버플로우가 일어난다.
JDK6까지는 String 클래스의 .intern() 메서드를 사용하면 해당 문자열은 메서드 영역 내의 '영구세대'의 문자열 상수 풀에 문자열을 저장하고 그곳의 참조값을 String 변수에 담아 힙메모리에 저장하지만, StringBuilder를 사용하면 문자열 객체의 인스턴스는 힙메모리에 저장되고 그 문자열을 참조하는 변수도 힙메모리에 저장되기 때문에 같은 문자열이더도 String과 StringBuilder의 값이 다른 경우가 종종 있었다.
public static void main(String[] args) {
System.out.println(new StringBuilder("컴퓨터")
.append("자바")
.toString()
.intern() == "컴퓨터자바");
}
그러니까 이런 코드를 실행했을 때 JDK6 이전에는 false가 출력되었다. 주소값이 String은 해당 문자열이 저장된 메서드 영역 영구세대 내의 문자열 풀의 주소를 나타내고, StringBuilder는 힙메모리의 주소를 나타내기 때문이다.
JDK7이후부터는 문자열 상수 풀이 힙메모리로 옮겨갔기 때문에 intern() 메서드를 사용하면 힙메모리 내의 상수 풀에 문자열을 저장하면 되고, 참조값도 힙메모리의 상수 풀에 참조값을 저장하면된다. StringBuilder도 같은 공간에서 참조값을 반환하면 되므로 같은 문자열에 대해서 String.intern() 과 Stringbuilder의 참조값이 같아지는 것, JDK7이후에서 저 코드를 실행하면 true가 출력된다.
이렇게 각 메모리영역마다 어떤 에러가 발생할 수 있는지에 대해 알아봤다.
- 힙메모리: 객체를 저장 가능한 영역이 더 이상 없는 경우 OOME 발생.힙 메모리 추적 프로그램을 이용하여 메모리 누수 여부를 판별하며, 메모리 누수가 없지만 OOME가 발생한 경우 힙 메모리의 크기를 조절해야하고 너무 오래 살아있는 객체는 한 번 할당을 해제하는 등 메모리를 최소로 사용해야한다.
- 스택: 핫스팟 머신에서는 하나의 스레드에 스택 프레임들이 쌓여 지정된 스레드에 용량을 벗어나면 StackOverflow 에러가 발생하며, 스레드를 너무 많이 생성하여 스택메모리에 여유공간이 없을 경우 OOME 발생. StackOverflow 에러는 평소에는 잘 발생하지 않으나 스레드가 너무 많아 OOME가 발생한 경우 힙메모리의 용량과 각 스레드에 할당되는 메모리 공간을 줄여야한다.
- 메서드영역: JDK8 이후로는 잘 일어나지 않으나 VM option을 통하여 메서드영역의 크기를 조정할 수 있다.
정도가 되겠다.
'CS > JVM' 카테고리의 다른 글
| [JVM] GC의 객체회수과정은 어떻게 일어나는가? - 마크-스윕, 마크-카피, 마크-컴팩트 (0) | 2025.12.26 |
|---|---|
| [JVM] 참조 카운팅 알고리즘과 파이썬의 순환 검출 알고리즘에 대해서 (0) | 2025.12.25 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #3 - 자바는 어떻게 객체를 불러오는가? (0) | 2025.12.05 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #2 - 자바는 어떻게 객체를 저장하는가? (0) | 2025.11.27 |
| [JVM] JVM 밑바닥까지 파헤치기 독서 #1 - 자바 런타임 메모리 영역 (0) | 2025.11.26 |
