* C/ C++ 개발자는 객체의 소유권, 탄생부터 죽음까지 관리하는 책임을 진다
* 자바 개발자는 VM이 제공하는 자동 메모리 관리 메커니즘으로 누수 / 오버플로 문제를 거의 겪지 않음
* 의존성이 높기 때문에, 문제 발생시 파악에 어려움이 있음
JVM 런타임 데이터

* PC 레지스터 - 현재 실행 중인 스레드의 줄 번호 표시기 (바이트 코드 줄 번호). 인터프리터는 이 카운터의 값을 바꿔서 다음에 실행할 바이트코드 명령어를 선택. 스레드별로 존재하며, CPU에게 멈춘 지점을 명확하게 복원시켜 주는 값 저장 담당.
* 자바 가상 머신 스택 - 스레드별로 존재. 스레드 내에서 메서드 호출시 스택에 지역 변수 테이블 (32비트 슬롯들 모음집), 피연산자 스택 (CPU의 계산용 임시공간), 동적 링크 (의존하는 메서드 호출 네비게이션), 메서드 반환값 등이 저장된 스택 프레임을 쌓음, FILO.
스택 메모리 영역에서는 두가지 오류가 정의
- StackOverflow : 스레드가 요청한 스택 깊이가 VM이 허용하는 깊이보다 큰 경우
- OutOfMemory : VM에서 동적으로 스택 용량을 확장하려는 시점에 여유 메모리가 충분하지 않을 경우
* 네이티브 메서드 스택 - 네이티브 메서드를 실행할 때 사용되는 스택. 일반 스택과 하나로 합쳐져 있는 경우도 많음
* 자바 힙 - 모든 스레드가 공유, 자바의 가장 큰 메모리, VM 구동시 생성. 자바 세계의 거의 모든 객체 인스턴스가 할당되는 영역. 가비지 컬렉터가 관리하는 메모리 영역. 스레드 로컬에 할당되는 버퍼 여러 개로 나뉨 (효율 높이기 위함). 디스크에 파일이 저장되듯이, 메모리 내에서 물리적으로는 떨어져도 논리적으로 연속되어야 함. Xmx, Xms 매개변수를 활용하여 VM 들은 힙 크기를 제어할 수 있다. 힙을 확정할 수 없다면 OOM 이 발생한다.
* 메서드 영역 - 모든 스레드가 공유. 정적 변수, 상수, (JIT 컴파일러가 컴파일한) 코드 캐시 등을 저장. GC 대상이긴 하지만 회수 효과가 상대적으로 적음. 메서드가 꽉 차서 메모리 할당 할 수 없다면 OOM 이 발생한다
메서드에는 다음과 같은 정보들이 저장됨
- 클래스 메타데이터, 필드 정보, 메서드 정보
- 런타임 상수 풀 (.class 에는 클래스 파일 상수 풀이 있는데, JVM 이 로딩시 런타임 상수 풀로 변경, )
- static 변수
- 문자열 상수 풀
* 런타임 상수 풀 - 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 정보 저장. 이 때, [.class] 파일에 있는 정보들은 정적인 테이블인 클래스 파일 상수 풀에 저장되는데, 클래스가 JVM에 로딩되면 클래스 파일 상수 풀이 런타임 상수 풀로 복사된다. 단, 이 런타임 상수 풀은 동적이다. 메서드 실행 중 new String("hello").intern() 을 수행시 상수풀에 추가된다 (그냥 String a = "hello" 는 이미 클래스 파일 상수 풀에 있는 상태라고 함)
* 다이렉트 메모리 - JVM에 속하지 않지만 자주 쓰이고 OOM의 원인이 되기도 함. 성능을 위해 힙을 거치지 않고 OS 메모리를 직접 사용하는 메모리로, 사용예시 ex) ByteBuffer buffer = ByteBuffer.allocateDirect(1024).
핫스팟 VM의 객체 살펴보기
객체 생성 과정
* JVM의 new 객체() 라는 바이트 코드를 만나면, 매개 변수가 상수 풀 안의 클래스를 가리키는 심벌 참조인지 확인. 그 다음 이 심벌 참조가 뜻하는 클래스가 로딩, 해석, 초기화되어 있는지 확인.
어떤 클래스의 객체를 만들라는건지 아까 클래스 상수 풀 (.class 의 이름, 변수, 상수 등) 에서 확인하는데, 이 때 심벌 참조 (new "이름")를 사용한다는 뜻. 그리고 메모리에 있는지 확인, 실제 메모리 참조하여 링크, 힙에 객체 공간 할당 등을 진행한다는 뜻
* 로딩이 완료된 클래스면 새 객체를 담을 메모리 할당 (힙에서 특정 크기의 메모리 블록 잘라 주는 일).
이 때, 자바 힙이 물리적으로 규칙적이라고 한다면, 새 객체의 크기만큼 포인터를 밀친 뒤 밀쳐진 공간을 주면 되는데, 이걸 "포인터 밀치기"라 함. 하지만, 당연히 뒤섞여있기 때문에 JVM은 가용 메모리 블록을 따로 관리, 공간을 찾아 할당 후 목록 refresh. 이걸 "여유 목록" 방식이라 함
(추가- 스레드가 가용 공간을 어떻게 할당할까)- 멀티 스레딩 환경에서는 여유 메모리의 시작 포인터 위치를 수정하는 단순한 일도 non-thread-safe. 여러 스레드가 동시에 객체를 생성하려 할 때 문제가 생길 수 있음
한 스레드가 요청한 객체 A를 위해 공유 공간인 힙 메모리를 할당하는 과정에서, 포인터의 값을 수정하기 전에 다른 스레드가 B를 요청할 때, 할당된 메모리가 A 공간 포인터 수정 전일 경우 문제가 발생한다
해법은 CAS, 혹은 ThreadLocal. 둘다 존재함. ThreadLocal 할당 버퍼 (TLAB) 방식은 스레드가 각각 힙 내의 작은 크기의 전용 공간을 미리 할당받아 놓는 방식을 말한다 (버퍼가 부족해지면, 그 때 동기화를 통해 새로운 버퍼를 할당받음). -XX:+/-UseTLAB 매개 변수를 JVM에게 전달하면 스레드 로컬 할당 버퍼를 사용하는 실행으로 인지한다.
* 아무튼 이후 JVM은 정보들을 객체 헤더에 저장한다 (해시코드 정보, GC 세대 정보 등). 이 시점에서 JVM은 객체 생성을 완료 했으나, 프로그램 관점에서는 클래스의 <init> 이 실행되지 않은 상태.
자바 컴파일러가 new 를 발견하면 바이트코드 명령어인 [new] 와 [invokespecial] 을 수행하는데, 후자가 <init> 메서드 호출을 담당. 둘은 연이어 수행된다.
객체의 메모리 레이아웃

* 핫스팟 VM은 객체를 세 부분으로 나눠 힙에 저장 (헤더, 인스턴스 데이터, 정렬 패딩)
* 객체 헤더
> 마크워드: 런타임 데이터 저장 (해시 코드, GC 세대, 현재 모니터 락 상태, 자주 쓰는 스레드 ID 등)
>> 매우 많은 정보들이 담겨야 하기 때문에, 효율적으로 사용해야하고 플래그를 통해 정보를 저장하기도 함
> 클래스워드: 클래스 포인터 저장
> 배열 길이: 배열 객체일 경우 길이도 저장 (int [10] : JVM은 이도 객체라고 판단함).
* 인스턴스 데이터
> 정의된 다양한 타입의 필드들, 부모 클래스 유모, 부모 클래스로 상속받은 필드 등 여기에 기록됨
* 정렬 패딩
> 없을 수도 있음. 객체의 크기는 8바이트의 N(정수)배. 조건을 충족하지 못하는 경우 해당 패딩을 통해 채움.
객체에 접근하기
* 객체들은 서로 연관되어 있으며, 스택내 참조 데이터를 통해 힙에 있는 객체들에 접근해서 조작함
* 힙에서 객체의 정확한 위치를 알아내어 접근하는 방식은 보통 핸들 / 다이렉트 포인터를 사용해 구현함


* 핸들 방식은 자바 힙에 핸들 저장용 Pool 을 두어서, 인스턴스 데이터, 타입 데이터 등의 정확한 주소 정보를 담아두는 방식
자바 A 객체에서 B 객체를 참조할 때, 핸들 방식이란 해당 B 객체에 대해 핸들 풀 참조를 들고 있는 것. GC 과정에서 객체 이동은 매우 흔한데, A 객체에 참조는 손댈 필요 없이 핸들 풀의 인스턴스 데이터 포인터만 바꾸면 됨
* 다이렉트 포인터은 힙에 인스턴스 데이터 뿐 아니라 타입 데이터 포인터도 제공해야 하고, 참조 데이터에도 힙 내 실제 객체의 인스턴스 주소가 저장되어 있음
핸들을 경유하지 않기 때문에 매우 빠른 접근 속도. 실행 시간에 실제로 유의미한 영향을 보인다
OutOfMemory 예외 살펴보기
* 자바 힙 OOM - 객체를 무한정 생성시 생성만 빠르게 지속하면 OOM이 발생한다. 힙의 OOM은 진짜 메모리 부족, 객체를 너무 많이 생성, 힙 크기 제한, 메모리 누수의 이유로 발생한다. (메모리 누수는 메모리 부족과 마찬가지긴 함)
* 스택 OOM 과 오버 플로우 - 함수를 무제한 호출 loop 을 돌리면, 스택 크기 제한으로 인해 StackOverflow 에러가 발생한다. 그리고 지역변수 테이블을 크게하여 스택 크기 자체를 크게 해서 (지역변수 800 Byte 추가) 스택이 빨리 가득 차게 하면 더 빠르게 발생한다. 하지만 이 부분은, 스택의 크기를 동적으로 늘릴 수 있는 VM이라면 OOM 을 발생시킬 수도 있다.
* 스레드로 인한 스택 OOM - 스레드를 생성하면 JVM 이 OS에게 직접 "스택 메모리 주세요" 한다. 이 때 OS 에서 "이 프로세스에게는 더 못준다"고 하면 발생함.
프로세스 전체 메모리 한도
├─ Heap (-Xmx)
├─ Thread stacks (-Xss × 스레드 수)
├─ Metaspace
├─ Direct/native memory
└─ JVM 내부 영역
- 스택이 1MB 인데 2000개의 스레드가 돌고 있다면, Thread stacks 는 총 2GB 를 가져간다. 이 때, Heap 이 4GB 라면 6GB 이상을 프로세스가 점유하고 있는 것
- Xmx 크기를 줄이고, -Xss 크기를 줄이면, 프로세스에게 "더 많은 스레드" 를 위한 공간이 확보되기 때문에, 오히려 위와 너무 많은 스레드로 인한 OOM 일 경우는 힙 크기 줄이기와 스택 크기 줄이기가 해결책이 되기도 한다.
* 메서드 영역 (런타임 상수 풀)의 OOM
> (무한루프) hashSet.add(String.valueOf(i++).intern()); 을 수행하면, JDK 6까지는 PermGen (핫스팟의 메소드 영역) 에서 OOM 이 발생한다.
intern 을 사용하면 JVM 이 관리하는 문자열 상수 풀에 등록하게 된다. 따라서 메서드 영역을 잡아 먹는 것. 문자열 상수 풀은 JVM이 공유 / 관리 하는 것으로, 우리가 실제 쓸 일은 없다. JVM의 static String 들이라고 보면 편하다 (특정 문자열 비교가 굉장히 잦을 때, intern 을 사용하여 최적화 할 수도 있다)
> JDK 7 을 넘어서면서는 문자열 상수 풀 + static 영역의 PermGen 이 힙으로 넘어왔기 때문에, 힙 영역 오류가 발생한다.
String a = new StringBuilder("hello").append("world").toString();
a.intern() == a;
를 수행했을 때, JDK7 이후는 true 가 발생한다. JDK 6에서 문자열에 대한 intern()은 문자열 상수 풀에 복사한 다음 문자열 인스턴스의 참조를 반환한다. StringBuilder 를 통해 생성되면 일반 Heap 에 존재하고 문자열 상수 풀에 복사시키지 않기 때문에, 다른 주소를 참조. 따라서 둘이 다름.
JDK 7 이후는 문자열 상수 풀이 자바 힙에 있으므로, intern 을 수행해도 첫 번째 인스턴스의 참조로 바꿔주면 된다 (JVM 입장에서는 공유중이기 때문)
------ 문자열 상수 풀이란 개념, 공유된다는 개념이 잘 잡히지는 않음. static 느낌과는 다름? String 은 자동 static 도 아닌데.
> 메서드 영역의 메타스페이스에 대한 OOM 도 테스트 하는데, 이 부분도 잘 이해가 안된다. 객체를 무한정 생성하는데, 메서드 영역의 OOM 나는 모습이 어떤 충돌이 일어난건지 잘 모르겠음
* 네이티브 다이렉트 메모리 OOM
> Unsafe 라는 객체 인스턴스를 사용하면, 직접 운영 체제 단에서 메모리를 할당하는 방식을 사용하게 된다. unsafe.allocateMemory(1024*1024) 를 통해 1MB 씩 지속 할당받으면, OOM 이 발생하는데, 이는 운영체제에서 직접 더 이상 할당하지 않는 모습
> 힙 덤프에서는 이상한 점을 찾을 수 없다는 점이 다이렉트 메모리의 OOM 특징
