이번 글은 "JVM 밑바닥까지 파헤치기" 책의 2.4 실전 : OutOfMemoryError 예외에 대한 부분을 정리 및 요약한 글입니다.
이번 절의 목적은 각 런타임 영역에 저장되는 내용을 검증하고, 실제 메모리 오버플로가 일어나는 과정을 경험하는데 초점이 맞춰집니다.
다음 나오는 예제 코드들의 첫 부분에는 예제 실행 시 설정해야 하는 가상 머신 시작 매개 변수가 주석으로 적혀 있습니다.('VM 매개 변
수'로 시작하는 주
석) 이 매개 변수들은 실행 결과에 영향을 줍니다.
실행 및 디버그 구성
-Xms20m
JVM이 실행될 때 초기로 확보할 힙 메모리 크기를 설정합니다.
-Xmx20m
JVM이 사용할 수 있는 힙 메모리의 최대 크기를 지정합니다.
-XX:+HeapDumpOnOutOfMemoryError
OutOfMemoryError(메모리 부족 오류)가 발생했을 때, 힙 덤프 파일을 자동 생성하도록 설정합니다.
힙 덤프 파일 (Heap Dump File)은 말 그대로 JVM 힙 메모리의 스냅샷(snapshot)입니다.
즉, JVM이 사용하는 힙 메모리 안에 어떤 객체들이 어떤 상태로 얼마나 존재하는지를 그대로 저장한 파일입니다.
메모리 누수(Leak) 분석할 때 아주 유용하고 어떤 객체가 계속 살아남고 있는지 추적 가능하고 어떤 클래스가 메모리를 많이 쓰고 있는지 파악 가능하고 GC가 메모리를 수거하지 못하는 이유를 분석 가능합니다.
"-Xms20m -Xmx20m -XX: +HeapDumpOnOutOfMemoryError"은 JVM 힙 메모리를 20MB로 고정하고, 메모리가 부족해 OutOfMemoryError가 나면 힙 상태를 덤프 파일로 저장하라는 뜻을 내포합니다. 또한 힙 자동 확장을 막기 위해서 최소 크기와 최대 크기를 똑같이 설정해주었습니다.
자바 힙 오버 플로
Java 힙은 객체 인스턴스를 저장하는 공간입니다. 객체를 계속 생성하고, 해당 객체들의 접근할 경로(참조)가 살아 있는 경우엔 언젠간 힙의 최대 용량을 넘어서게 됩니다.
힙의 최대 용량을 넘어서게 되면, 메모리 오버플로가 발생합니다.
자바 힙 메모리 오버플로 테스트
package org.example;
import java.util.ArrayList;
import java.util.List;
/**
* VM 매개 변수: -Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
* @author zzm
*/
public class HeapOOM {
static class OOMObject{
}
public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();
while(true) {
list.add(new OOMObject());
}
}
}
실행 결과는 아래와 같습니다.
OutOfMemoryError로 Java heap에서 예외가 발생하게 됩니다.
이 문제를 해결하는 일반적인 방법으로는 먼저 힙 덤프 스냅숏을 분석 해보는 것입니다.
힙 덤프 스냅숏 분석
아래와 같이 힙 상태를 나타내는 덤프 파일을 ./heapdump.hprof파일에 저장하도록 했습니다.
-Xms256m -Xmx256m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heapdump.hprof
가장 많은 메모리를 차지하는 클래스는 org.example.HeapOOM$OOMObject로 객체수가 약 920만개로 148MB의 메모리를 점유하고 있습니다. 또한 GC 루트에 걸려 있습니다. 이는 GC가 수거할 수 없다는 의미이고 객체가 계속 살아있음을 의미합니다.
OOMObject의 객체 수가 너무 많고, GC Root에서 계속 참조 된다는 점을 미루어 봤을때, 해당 클래스로 인해 메모리 누수가 발생하는 것을 확인할 수 있습니다.
GC Root에서 출발해 참조를 따라가면서 도달할 수 있는 객체는 ‘살아있다’고 간주합니다.
즉, GC Root로부터 참조 체인을 따라 도달 가능한 객체는 메모리에서 지우지 않게 되고, GC Root로부터 절대로 도달할 수 없는 객체만 가비지로 간주해서 수거합니다.
만약, 메모리 누수가 아닌, 메모리에 존재하는 모든 객체가 다 살아 있어야 한다면 어떻게 해야할까요?
먼저 자바 가상 머신의 힙 매개 변수 설정과 컴퓨터의 가용 메모리를 비교하여 가상 머신에 메모리를 더 많이 할당할 수 있는지 알아봅니다.
그 다음으로, 코드에서 수명 주기가 너무 길거나 상태를 너무 오래 유지하는 객체는 없는지, 공간 낭비가 심한 데이터 구조를 쓰고 있지 않은지 살펴 프로그램이 런타임에 소비하는 메모리를 최소로 낮춥니다.
가상 머신 스택과 네이티브 메서드 스택 오버플로
핫스팟 가상 머신은 가상 머신 스택과 네이티브 메서드 스택을 구분하지 않습니다.
이로 인해, 네이티브 메서드 스택의 크기를 결정하는 -Xoss 매개 변수를 설정해도 아무런 효과가 없게 됩니다.
즉, 스택 크기는 오직 -Xss 매개 변수로만 변경할 수 있습니다.
가상 머신 스택과 네이티브 메서드 스택은 2가지 예외가 발생하게 됩니다.
- 스레드가 요구하는 스택 깊이가 가상 머신이 허용하는 최대 깊이보다 크면 StackOverflowError가 발생합니다.
- 가상 머신이 스택 메모리를 동적으로 확장하는 기능을 지원하나, 가용 메모리가 부족해 스택을 더 확장할 수 없는 경우 OutOfMemoryError를 던집니다.
핫스팟 가상 머신은 확장을 지원하지 않기 때문에 스레드를 생성할 때 메모리가 부족하여 OutOfMemoryErrorr가 나는 경우를 제외하고는 스레드 실행 중에 가상 머신 스택이 넘칠일이 없습니다. 스택 용량이 부족해 새로운 스택 프레임을 담을 수 없는 경우 StackOverflowError가 발생합니다.
핫스팟 가상 머신의 실험
첫번째 테스트
- -Xss 매개 변수로 스택 메모리 용량을 줄입니다.
- 지역 변수를 많이 선언해서 메서드 프레임의 지역 변수 테이블 크기를 키웁니다.
- StackOverflowError가 발생합니다.
package org.example;
/**
* VM 매개 변수: -Xss180k
*/
public class JavaVMStackSOF_1 {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) {
JavaVMStackSOF_1 oom = new JavaVMStackSOF_1();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("스택 길이: " + oom.stackLength);
throw e;
}
}
}
계속해서 메서드 호출이 일어나기 때문에, 스택 프레임이 계속 쌓이게 됩니다.
출력 결과는 다음과 같습니다.
두번째 테스트
지역 메모리 테이블 공간을 더 많이 점유하기 위해 많은 변수를 선언합니다.
package org.example;
public class JavaVMStatckSOF_2 {
private static int stackLength = 0;
public static void test() {
long unused1, unused2, unused3, unused4, unused5, unused6, unused7,
unused8, unused9, unused10, unused11, unused12, unused13,
unused14, unused15, unused16, unused17, unused18, unused19,
unused20, unused21, unused22, unused23, unused24, unused25,
unused26, unused27, unused28, unused29, unused30, unused31,
unused32, unused33, unused34, unused35, unused36, unused37,
unused38, unused39, unused40, unused41, unused42, unused43,
unused44, unused45, unused46, unused47, unused48, unused49,
unused50, unused51, unused52, unused53, unused54, unused55,
unused56, unused57, unused58, unused59, unused60, unused61,
unused62, unused63, unused64, unused65, unused66, unused67,
unused68, unused69, unused70, unused71, unused72, unused73,
unused74, unused75, unused76, unused77, unused78, unused79,
unused80, unused81, unused82, unused83, unused84, unused85,
unused86, unused87, unused88, unused89, unused90, unused91,
unused92, unused93, unused94, unused95, unused96, unused97,
unused98, unused99, unused100;
stackLength++;
test();
unused1 = unused2 = unused3 = unused4 = unused5 = unused6 = unused7
= unused8 = unused9 = unused10 = unused11 = unused12 = unused13
= unused14 = unused15 = unused16 = unused17 = unused18 = unused19
= unused20 = unused21 = unused22 = unused23 = unused24 = unused25
= unused26 = unused27 = unused28 = unused29 = unused30 = unused31
= unused32 = unused33 = unused34 = unused35 = unused36 = unused37
= unused38 = unused39 = unused40 = unused41 = unused42 = unused43
= unused44 = unused45 = unused46 = unused47 = unused48 = unused49
= unused50 = unused51 = unused52 = unused53 = unused54 = unused55
= unused56 = unused57 = unused58 = unused59 = unused60 = unused61
= unused62 = unused63 = unused64 = unused65 = unused66 = unused67
= unused68 = unused69 = unused70 = unused71 = unused72 = unused73
= unused74 = unused75 = unused76 = unused77 = unused78 = unused79
= unused80 = unused81 = unused82 = unused83 = unused84 = unused85
= unused86 = unused87 = unused88 = unused89 = unused90 = unused91
= unused92 = unused93 = unused94 = unused95 = unused96 = unused97
= unused98 = unused99 = unused100 = 0;
}
public static void main(String[] args) {
try {
test();
} catch (Error e) {
System.out.println("스택 길이: " + stackLength);
throw e;
}
}
}
출력 결과를 통해서 스택 프레임이 너무 커서인지, 아니면 가상 머신이 스택 용량이 부족해서 인지 알 수 있습니다.
새로운 스택 프레임용 메모리를 할당할 수 없는 경우엔 StackOverflowError를 던집니다.
만약 가상 머신이 스택 크기를 동적으로 확장할 수 있다면, OutOfMemoryError가 발생하게 됩니다.
(클래식 VM 모델은 스택 메모리 용량을 동적으로 확장할 수 있었습니다. 따라서, 위 실험시 OutOfMemory 에러가 발생하게 됩니다.)
세번째 테스트
스레드를 계속 만들어 내는 경우, 핫스팟에서도 메모리 오버플로를 일으킬 수 있습니다.
이 오버플로우는 스택 공간이 부족해서가 아니라, 운영체제 자체의 메모리 사용 한계에 영향을 받기 때문입니다.
운영체제는 하나의 프로세스가 사용할 수 있는 메모리 크기에 제한이 있습니다. 예를 들어 32비트 윈도우에서는 하나의 프로세스가 사용할 수 있는 최대 메모리가 2기가바이트입니다. 이 2기가바이트 안에서 자바 힙 메모리와 메서드 영역의 최대 크기를 설정할 수 있고, 나머지 메모리는 스레드별 스택 메모리 등으로 사용됩니다.
각 스레드는 생성될 때마다 스택 메모리를 사용하는데, 이 스택 메모리의 크기를 너무 크게 잡으면 동시에 생성할 수 있는 스레드 수가 줄어듭니다. 따라서 스레드를 많이 생성하려고 하면 전체 메모리 한도를 초과하여 새로운 스레드를 만들 수 없게 되고, 이 경우 OutOfMemory가 발생합니다.
package org.example;
/**
* VM 매개 변수: -Xss2M
*/
public class JavaVMStackOOM {
private void donStop(){
while(true) {}
}
public void stackLeakByThread() {
while(true) {
Thread thread = new Thread(this::donStop);
thread.start();
}
}
public static void main(String[] args) {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread();
}
}
제 맥북 M4 환경에서는 OutOfMemory 오류가 발생하지 않고, 렉만 걸립니다.
만약 현재 코드를 32bit 운영 체제에서 실행을 하면, OutOfMemoryError가 발생하게 됩니다.
참고로 StackOverFlowError가 발생하면 예외를 일으킨 스택 정보가 분석용으로 제공되어 분석이 쉬운 반면,
너무 많은 스레드를 생성해서 발생하는 OutOfMemoryError는 원인 파악이 더 어렵고, 시스템에 따라 발생 조건이 달라 주의가 필요합니다.
특히, 멀티스레드 애플리케이션을 만들 때, 단순히 스레드 개수를 늘리는 것만으로 성능 향상을 꾀하려고 하면 큰 문제에 부딪힐 수 있습니다. 스레드가 차지하는 스택 메모리까지 고려하지 않으면 OutOfMemoryError가 발생할 수 있으므로, 스레드 수,스택 크기,운영체제 리소스 제한 등을 종합적으로 고려해 설계해야 합니다.
메서드 영역과 런타임 상수 풀 오버플로
런타임 상수 풀은 메서드 영역에 속하기 때문에 두 영역의 오버플로 테스트는 함께 수행이 가능합니다.
핫스팟 VM은 JDK 7부터 영구 세대(PerGem)를 점진적으로 없애기 시작해, JDK 8에 와서 메타스페이스(Metaspace)로 완전히 대체했습니다.
메서드 영역을 영구 세대에 구현했는지 메타 스페이스에 구현했는지 테스트 코드를 통해 직접 확인해보고, 실제 프로그램에 어떠한 영향을 주는지 알아보겠습니다.
테스트
JDK 6, JDK 7, JDK 8 버전 별로 테스트 코드를 실행해서 런타임 상수 풀과 메서드 영역이 어떤 메모리 영역에 속해 있는지를 확인하고, 해당 한계를 넘었을 경우 어떠한 문제가 발생하는지 실험을 통해 직접 확인해보겠습니다.
package org.example;
import java.util.HashSet;
import java.util.Set;
/**
* VM 매개 변수 : (JDK 7 이하) -XX:PermSize=6M -XX:MaxPermSize=6M
* VM 매개 변수 : (JDK 8 이하) -XX:MetaspaceSize=6M -XX:MaxMetaspaceSize=6M
*/
public class RuntimeConstantPoolOOM_1 {
public static void main(String[] args) {
// full GC가 상수 풀을 회수하지 못하도록 집합(Set)을 이용해 상수 풀의 참조를 유지
Set<String> set = new HashSet<>();
// short 타입의 범위인 6MB 크기의 영구 세대에서 오버플로를 일으키기 충분한크기
short i = 0;
while(true) {
set.add(String.valueOf(i++).intern());
}
}
}
런타임 상수 풀(Runtime Constant Pool)에 많은 문자열을 저장하여, 특정 메모리 영역(영구 세대 또는 메타스페이스)의 오버플로우(OutOfMemoryError)를 의도적으로 유발하는 실험 코드입니다.
코드에서 사용되는 String.intern() 메서드는 네이티브 메서드로 문자열 상수 풀에 이 메서드가 호출된 String 객체와 똑같은 문자가 이미 존재하는 경우엔, 기존 문자열의 참조를 반환합니다. 만약 같은 문자열이 존재하지 않는다면 현재 String 객체에 담긴 문자열 상수 풀에 추가가되고 이 String의 참조가 반환됩니다.
JDK6에서 해당 코드를 실행시키면, OutOfMemoryError : PerGen space 오류가 발생하게 됩니다.
런타임 상수 풀이 메서드 영역의 한 부분임을 확인할 수 있습니다.(JDK 6의 핫스팟VM에서의 메서드 영역은 영구 세대에 할당됩니다.
JDK7에서 실행하게 되면 결과가 달라집니다. 아무런 예외도 던지지 않고 무한 루프를 돌며 절대 멈추지 않습니다.
이유는 영구 세대에 저장했던 문자열 상수 풀을 JDK7부터 자바 힙으로 옮겼기 때문입니다. 즉, 설정한 메서드 영역 제한의 의미가 없어진 것입니다.
매개 변수를 -Xmx6M으로 바꿔 최대 힙 크기를 6MB로 줄이게 되는 경우, 오버플로가 객체 할당 시 일어나는지 아닌지에 따라 두개의 결과 중 하나를 보게 됩니다.
1번 예외는 숫자 → 문자열 변환 과정 중 String 객체가 힙에 직접 할당되다가 실패한 경우입니다.
즉, 객체가 힙에 들어갈 공간이 부족해서 에러가 발생한 것입니다.(객체 생성 자체가 힙 부족의 원인)
2번 예외는 HashSet 내부의 HashMap이 용량을 늘리기 위해 resize 하려는 시점에 힙이 부족해져서 발생한 경우입니다.
즉, HashSet은 계속 커지고 있는데, 기존 배열의 크기를 늘리기 위한 새로운 배열을 할당하려다가 실패한 것입니다. (저장소의 크기 조절 과정에서 OOM발생)
문자열 상수 풀을 구현한 위치가 바뀌어서 일어나는 흥미로운 변화가 하나 더 있습니다.
package org.example;
public class RuntimeConstantPoolOOM_2 {
public static void main(String[] args) {
String str1 = new StringBuilder("컴퓨터").append(" 소프트웨어").toString();
System.out.println(str1.intern() == str1);
}
}
해당 코드는 JDK 6에서 실행하면 false를 출력하지만, JDK 7에서 실행하면 true를 출력합니다.
그 이유는 JDK 6의 intern() 메서드는 처음 만나는 문자열 인스턴스를 영구 세대의 문자열 상수 풀에서 복사를 한 다음, 영구 세대에 저장한 문자열 인스턴스의 참조를 반환합니다.
반면, StringBuilder로 생성한 문자열 객체의 인스턴스는 자바 힙에 존재합니다. 따라서, 이 두 값은 같은 참조가 될 수 없고, 자연히 false를 반환하게 됩니다.
JDK 7이상에서는 intern() 메서드는 문자열 인스턴스를 영구 세대에 대해 복사할 필요가 없습니다. 문자열 상수 풀 위치가 자바 힙이기 때문에, 그저 풀에 있는 첫 번째 인스턴스의 참조로 바꿔주면 됩니다. 즉, intern()이 반환하는 참조는 StringBuilder가 생성한 문자열 인스턴스와 동일합니다.
메서드 영역
메서드 영역의 주 역할은 타입 관련 정보 저장입니다. 클래스 이름, 접근 제한자, 상수 풀, 필드 설명, 메서드 설명 등이 있습니다.
해당 영역을 테스트하기 위해선 기본적으로 런타임 메서드 영역이 가득 찰 때까지 계속해서 클래스를 생성해야 합니다.
CGLIB는 런타임 바이트코드를 조작해 새로운 클래스를 동적으로 생성합니다.
이 과정을 반복하면 매번 다른 프록시 클래스가 만들어지고, 생성된 클래스들은 JVM의 메서드 영역(Metaspace)에 계속 쌓이게 됩니다.
클래스 수가 많아질수록 메서드 영역에 부담이 가중되고, 결국 OOM 오류로 이어질 수 있습니다.
테스트
package org.example;
/**
* VM 매개 변수 : (JDK 7 이하) -XX:PermSize=10M -XX:MaxPermSize=10M
* VM 매개 변수 : (JDK 8 이상) -XX:MetaspaceSize=10M -XX:MaxMetaspaceSize=10M
*/
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class JavaMethodAreaOOM {
public static void main(String[] args) {
while(true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMObject.class);
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
return methodProxy.invokeSuper(o, objects);
}
});
}
}
static class OOMObject {}
}
이 코드는 CGLIB를 사용해서 지속적으로 새로운 클래스를 생성하면서 메서드 영역(또는 Metaspace)의 메모리를 고갈시켜 OutOfMemoryError를 발생시키는 테스트 코드입니다.
JDK 7에서는 아래와 같은 오류가 발생합니다.
한편, JDK 15에서는 아래와 같은 오류가 발생합니다.
JDK7까지는 영구 세대(PermGen)에 클래스의 메타데이터(클래스명, 메서드 정보, 상속 관계 등)이 저장 되었고, JVM이 시작될 때 고정된 크기로 설정되고, 자동으로 늘어나지 않았습니다. 따라서, 많은 클래스를 동적으로 생성하면 PermGen 공간이 부족해지고, OOM: PerGen space 오류가 발생했습니다.
JDK8부턴 클래스 메타데이터는 Metaspace라는 새로운 영역에 저장됩니다. 특히, Metaspace는 OS의 메모리 영역을 사용하며, 기본적으로 자동으로 커질 수 있습니다.(-XX:MaxMetaspaceSize로 제한하지 않는 이상) 따라서, 기본 설정으로는 일반적인 동적 생성ㅇ으로 오버플로우가 잘 안나게 됩니다.
Metaspace 설정
- -XX:MaxMetaspaceSize : 메타스페이스의 최대 크기를 설정합니다.(기본값은 -1로, 제한 없음 또는 네이티브 메모리 크기가 허용한만큼이라는 의미입니다.)
- -XX:MetaspaceSize : 메타스페이스의 초기 크기를 바이트 단위로 지정하븐 것을 의미합니다. 해당 크기가 가득 차는 경우 가비지 컬렉터가 클래스 언로딩을 시도한 다음 크기를 조정합니다.
- -XX:MinMetaspaceFreeRatio: 가비지 컬렉션 후 가장 작은 메타스페이스 여유 공간의 비율을 정합니다. 해당 값을 조절해 메타스페이스의 공간이 부족해 발생하는 가비지 컬렉션 빈도를 줄일 수 있습니다.
네이티브 다이렉트 메모리 오버플로
Java에서는 보통 JVM의 힙 메모리를 사용하지만, DirectByteBuffer를 통해 힙이 아닌 네이티브 메모리를 사용할 수도 있습니다.
이때 사용하는 메모리는 -XX:MaxDirectMemorySize 옵션을 통해 최대 용량을 지정할 수 있습니다. 별도로 설정하지 않는 경우, 일반적으로 -Xmx 힙 크기와 유사한 크기가 기본값으로 적용됩니다.
DirectByteBuffer 동작 방식
DirectByteBuffer는 내부적으로 sun.misc.Unsafe 클래스의 기능을 활용해 JVM 바깥의 메모리를 직접 할당합니다. Unsafe는 원래 JVM 내부 용도로 만들어진 클래스이기 때문에 일반적인 애플리케이션에서는 직접 사용할 수 없습니다. 그러나 리플렉션을 통해 강제로 인스턴스를 얻어 사용할 수는 있습니다.
JVM 내부 용도 : 해당 클래스가 일반 개발자가 사용하는 것이 아닌, JVM이나 JDK 자체의 내부 동작을 위해 설계 되었다.
오버플로 발생 가능
Unsafe.allocateMemory() 메서드는 JVM 힙이 아닌 OS 메모리 영역을 직접 사용하는 강력한 기능을 제공합니다. 하지만 크기를 잘못 계산하거나 반복적으로 과도하게 메모리를 할당할 경우, Direct Memory 영역이 가득 차면서 오버플로우가 발생하게 됩니다.
운영체제는 네이티브 메모리를 힙처럼 관리해주지 않기 때문에, 적절한 관리 없이 사용하면 OutOfMemoryError가 발생하거나 시스템 전체 안정성에 영향을 줄 수 있습니다.
package org.example;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* VM 매개 변수 : -Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {
private static final int _1MB = 1024 * 1024;
public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while(true) {
unsafe.allocateMemory(_1MB);
}
}
}
해당 코드를 실행하면, 아래와 같은 결과가 발생합니다.
다이렉트 메모리에서 발생한 메모리 오버플로의 특징은 힙 덤프 파일에선 이상한 점을 찾을 수 없다는 것입니다.
메모리 오버플로로 생성된 덤프 파일이 매우 작거나 프로그램에서 DirectMemory를 직접 혹은 간접적으로 사용한 경우, 오류의 원인은 다이렉트 메모리에 있습니다.
정리
- JVM 힙 영역에서는 객체를 계속 생성해 참조를 유지하면 힙이 가득 차면서 OutOfMemoryError가 발생하고, 힙 덤프를 통해 메모리 누수를 분석할 수 있습니다.
- 스택 영역에서는 재귀 호출이나 지역 변수 과다 선언 등으로 StackOverflowError가 발생하며, 스레드를 과도하게 생성할 경우 운영체제 한계로 OutOfMemoryError가 발생할 수 있습니다.
- JDK 7 이전에는 메서드 영역과 런타임 상수 풀이 PermGen에 존재했고, 이후 JDK 8부터는 메타스페이스(Metaspace)로 변경되어 설정 방식과 오버플로 조건이 달라졌습니다.
- 또한, Unsafe 클래스를 이용해 직접 할당하는 다이렉트 메모리에서도 과도한 사용 시 OutOfMemoryError가 발생하지만, 이는 힙 덤프 분석으로는 원인을 찾기 어렵습니다.
'Java' 카테고리의 다른 글
[JVM] 저지연 가비지 컬렉터 : 세넌도어 (0) | 2025.04.21 |
---|---|
[JVM] 핫스팟 VM에서의 객체 돌아보기 (0) | 2025.04.16 |
리플렉션(Reflection) (0) | 2025.03.07 |
ArrayList가 크기를 늘리는 방법 (0) | 2025.02.08 |
[Java] Serialization (2) | 2025.01.27 |