2.4.1 자바 힙 오버플로
package org.fenixsoft.jvm.chapter2;
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());
}
}
}> Task :jvm.HeapOOM.main() FAILED
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid12938.hprof ...
Heap dump file created [30015047 bytes in 0.110 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
- 자바 힙에서 오버플로 발생시 문제를 해결하는 방법은
- 첫째, 오버플로를 일으킨 객체가 꼭 필요한 객체인가 확인
- 다시 말해 메모리 누수인지, 오버플로인지 확인
- 필요없는 객체가 원인이라면 메모리 누수임
- 누수라면 도구를 이용해 누수된 객체로부터 GC루트까지의 참조 사슬을 살펴본다. 누수된 객체 까지 어떤 참조 경로가 있고 어느 GC루트와 연결되어 있기에 가비지 컬렉터가 회수 못했는지 찾아보는 것
- 누수가 아니라면? 즉, 모든 객체가 다 살아있어야 한다면?
- 힙 매개 변수 설정(-Xms, -Xmx)와 컴퓨터의 가용 메모리 비교하여 가상 머신에 메모리 더 할당할 수 있을지 확인
- 코드에서 수명 주기 너무 길거나 상태 너무 오래 유지하는 객체 없는지 확인
- 공간 낭비 심한 데이터 구조 쓰고 있지 않은지 확인 등
- 프로그램이 런타임에 소비하는 메모리를 최소로 낮춘다!
- 첫째, 오버플로를 일으킨 객체가 꼭 필요한 객체인가 확인
2.4.2 가상 머신 스택과 네이티브 메서드 스택 오버플로
- 핫스팟은 가상 머신 스택과 네이티브 메서드 스택 구분 안한다
- 따라서 네이티브 메서드 스택의 크기를 설정하는 -Xoss 매개 변수를 설정하더라도 아무런 효과 없다(그럼 왜 있음)
- 스택 크기는 오직 -Xss 매개 변수로만 변경 가능. <명세> 따르면 가상 머신 스택과 네이티브 메서드 스택에서는 다음 두 경우 예외가 발생함
-
- 스레드가 요구하는 스택 깊이가 가상 머신이 허용하는 최대 깊이보다 크면
StackOverflowError던짐
- 스레드가 요구하는 스택 깊이가 가상 머신이 허용하는 최대 깊이보다 크면
-
- 가상 머신이 스택 메모리를 동적으로 확장하는 기능을 지원하지만, 가용 메모리가 부족해 스택을 더 확장할 수 없다면
OutOfMemoryError던짐
- 가상 머신이 스택 메모리를 동적으로 확장하는 기능을 지원하지만, 가용 메모리가 부족해 스택을 더 확장할 수 없다면
-
- <명세>에서는 분명 스택을 동적으로 확장할 수 있는 여지 주었지만 핫스팟 가상 머신은 확장을 지원안함
- 따라서 스레드를 생성 시 메모리가 부족하여
OutOfMemoryError나는 경우 제외하고는 스레드 실행 중 가상 머신 스택이 넘치는 일 없음(나머지 영역들은 미리 정해져 있으니까 그런듯! 스래드는 생성되면 새로 그 고유한 영역이 생겨버리니깐) - 즉, 스택 용량이 부족하여 새로운 스택 프레임을 담을 수 없을 때만 `StackOverflowError 발생
- 따라서 스레드를 생성 시 메모리가 부족하여
- 이를 검증해보기 위한 실험 ㄱㄱ
- 먼저 실험 범위를 단일 스레드로 한정하고 다음 두 동작 수행해 핫스팟이
OutOfMemoryError던지는지 실험
- -Xss 매개 변수로 스택 메모리 용량 줄인다. 결과는
StackOverflowError발생. - 지역 변수를 많이 선언하여 메서드 프레임의 지역 변수 테이블 크기 키운다. 결과는
StackOverflowError발생.
첫째 상황
package jvm;
/**
* VM 매개변수:-Xss180k
*-Xss 매개 변수로 스택 메모리 용량 줄인
* @author zzm
*/public class JavaVMStackSOF_1 {
private int stackLength = 1;
public void stackLeak() {
stackLength++;
stackLeak();
}
public static void main(String[] args) throws Throwable {
JavaVMStackSOF_1 oom = new JavaVMStackSOF_1();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("스택 길이:" + oom.stackLength);
throw e;
}
}
}
> Task :jvm.JavaVMStackSOF_1.main() FAILED
스택 길이:35468
Exception in thread "main" java.lang.StackOverflowError
둘째 상황
package jvm;
/**
* @author zzm */public class JavaVMStackSOF_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;
}
}
}> Task :jvm.JavaVMStackSOF_2.main() FAILED
스택 길이: 10384
Exception in thread "main" java.lang.StackOverflowError
셋째 - 메모리 오버플로 by 스레드 생성
- 스레드 계쏙 만들면 당연히 메모리 오버플로 memory
- 이건 32비트 운영체제환경에서 하는거라 책 참고
- 스택 공간과 노상관이 되는데 오히려 운영체제 자체의 메모리 사용상태에 영향 크게 받음
- 심지어 이 경우 스레드별 스택을 크게 잡을수록 메모리 오버 플로 쉽게 일으킬수있음
- 이유 이해 어렵지는 않은데
- 바로 운영체제가 각 프로세스에 할당하는 메모리 크기가 제한적이라서. 예컨데 32비트 윈도우에서 프로세스 하나가 쓸 수 있는 최대 메모리는 2GB.
- 핫스팟에서는 자바 힙과 메서드 영역의 최대값을 매개변수로 설정 가능. 이렇게 하고 남은 메모리는 운영체제 한계인 2GB - 힙의 최대 크개 - 메서드 영역 최대 크기 이렇게 한 것. PC 가 차지하는 메모리 작아서 무시함
- 다이렉트 메모리와 자바 가상 머신 프로세스가 자체적으로 소비하는 메모리 제외한 나머지 메모리가 가상 머신 스택과 네이티브 메서드 스택에 할당됨.
- 따라서, 각 스레드에 스택 메모리 많이 할당하면 생성가능한 스레드수 작아짐
- 즉, 새로운 스레드 생성하려 할 때 메모리가 고갈될 가능성이 커지는 것
2.4.3 메서드 영역과 런타임 상수 풀 오버플로
- 런타임 상수 풀은 메서드 영역 소속
- 이 두 영역의 오버플로 테스트는 함께 수행 가능
- 앞서 이야기했듯이 핫스팟은 JDK 7부터 영구 세대를 점직적으로 없애기 시작하여 JDK 8에 와서 메타스페이스로 완전 대체
- String::intern()은 네이티브 메서드
- 문자열 상수 풀에 이 메서드가 호출된 String 객체와 똑같은 문자열이 이미 존재한다면 풀에 있는 기존 문자열 참조를 반환
- 같은 문자열 없다면 현재 String 객체에 담긴 문자열이 상수 풀에 추가되고 이 String의 참조가 반환
- JDK 6까지 핫스팟은 상수 풀을 영구 세대에 할당.
- 영구 세대 크기는 -XX:PermSize, -XX:MaxPermSize 로 조절 가능
- 이는 상수 풀 용량에도 간접적으로 영향 줌
package org.fenixsoft.jvm.chapter2;
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
*
* @author zzm
*/
public class RuntimeConstantPoolOOM_1 {
public static void main(String[] args) {
// Full GC가 상수 풀을 수거하지 못하도록 집합(Set)을 이용해 상수 풀의 참조를 유지
Set<String> set = new HashSet<String>();
// short 타입의 범위면 6MB 크기의 영구세대에서 오버플로를 일으키기 충분함
short i = 0;
while (true) {
set.add(String.valueOf(i++).intern());
}
}
}JDK 6 결과
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern (Native Method)
이를 통해 런타임 상수 풀이 정말 메서드 영역의 한 부분임을 알 수 있음
하지만 같은 코드를 JDK 7이상에서 실행시 결과가 달라짐. 아무런 예외 던지지 않고 무한 루프 돌며 절대 안멈춤. 영구 세대에 저장했던 문자열 상수 풀을 자바 힙으로 옮겼기 때문! 따라서 이 테스트에서 설정한 메서드 영역 제한의 의미가 없어진 것
이번에는 매개 변수를 -Xmx6M 으로 바꿔 최대 힙 크기를 6MB 로 줄여보자. 결과로
- OOM: Java heap space
- OOM: Java heap space
구글 검색
자바의 영구 세대(PermGen)은 JVM 메모리 영역 중 클래스 및 인터페이스 정보가 저장되는 곳으로, Java 8부터 메타스페이스로 대체되었습니다.
자세한 내용:
-
영구 세대(PermGen)의 역할:
클래스 로더가 로드한 클래스, 인터페이스, 메서드 정보 등 애플리케이션 실행에 필요한 메타데이터를 저장하는 공간입니다.
-
메타스페이스의 역할:
Java 8부터 영구 세대를 대체한 메타스페이스는 클래스, 인터페이스, 메서드와 관련된 데이터를 저장합니다. 하지만 영구 세대와 달리 힙 영역에 위치하며, 메모리 할당에 대한 제약이 줄어들었습니다.
-
가비지 컬렉션 대상:
영구 세대는 가비지 컬렉션 대상이 아니었습니다. 하지만 메타스페이스는 힙 영역에 위치하므로 가비지 컬렉션의 영향을 받습니다.
-
Java 8 이전:
영구 세대는 주로 JVM의 초기화 단계에서 클래스가 로드될 때 데이터를 저장하고, 애플리케이션 실행 중에도 사용되던 클래스 정보 등을 관리했습니다.
-
Java 8 이후:
메타스페이스로 대체되면서, 클래스 정보를 힙 영역에 저장하고, 메모리 사용량과 가비지 컬렉션에 대한 제약이 완화되었습니다.
요약:
Java 8 이전까지는 영구 세대(PermGen)가 JVM에서 클래스 및 인터페이스 정보를 저장하는 역할을 했지만, Java 8부터는 메타스페이스(Metaspace)로 대체되었습니다. 메타스페이스는 영구 세대와 달리 힙 영역에 위치하며, 메모리 관리 측면에서 더 많은 유연성을 제공합니다.
책 내용이 잘 이해안가서 정리는 다 안함. 내용을 다시 볼 것
2.4.4 네이티브 다이렉트 메모리 오버플로
- 다이렉트 메모리 용량은 -XX:MaxDirectMemorySize 로 설정. 디폴트는 -Xmx로 설정한 자바 힙의 최대값과 같음
- 밑의 코드는 NIO의 DirectByteBuffer 클래스를 건너뛰고 리프렉션 이용해 Unsafe 인스턴스를 직접 얻어 메모리를 할당받고 있음
- DirectByteBuffer 통해 메모리를 할당해소 오버플로는 될 수 있지만, 이 경우는 운영 체제 단에서 메모리르 할당하느라 나는 예외가 아님. 하지만 Unsafe 이용하면 할당할 수 없는 크기를 계산해 오버플로를 수동으로 일으킬 수 있다
- Unsafe::allocateMemory()가 메모리를 할당하는 메서드
package org.fenixsoft.jvm.chapter2;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* VM 매개변수:-Xmx20M -XX:MaxDirectMemorySize=10M
*
* @author zzm
*/
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);
}
}
}결과
~OutOfMemoryError: Unable to allocate 1048576 bytes
~
- 다이렉트 메모리에서 발생한 메모리 오버플로의 특징은 힙 덤프 파일에서는 이상한 점을 찾을 수 없는 것
- 메모리 오버플로로 생성된 덤프 파일이 매우 작다면 그리고 프로그램에서 DirectMemory를 직접 또는 간접적(보통 NIO) 사용했다면, 다이렉트 메모리에서 원인을 찾는데 집중해야함