잘못된 정보를 발견하셨다면 지적과 수정요청 언제나 환영입니다 감사합니다 ! ! !
문득 우리가 항상 사용하고 있는 new 연산자를 통해 생성자를 호출했을때
선언한 참조변수와 생성된 인스턴스가 어디에 저장되고 어떤식으로 만들어지는지 궁금했다.
약간 의식의 흐름 느낌이긴 하지만, 궁금해서 알아본 김에 글로 정리해보고자 한다.
0. 이 과정에 관여되는 메모리 영역들
JVM 은 프로그램을 실행하기 위해 메모리를 여러 영역으로 나누어 사용하는데,
new 연산자를 통한 생성자의 실행 과정에 관여되는 메모리 영역들 중심으로 알아보자
- 스택(Stack) 영역
스택 영역에는 메소드의 호출 정보, 지역 변수, 기본형 변수가 저장된다.
각 스레드마다 별도로 스택이 생성되고, 각 스택마다 메소드가 호출되면 스택 프레임(Stack Frame) 이 쌓이게 되며, 이는 메소드가 종료되면 사라진다. 구조는 스택이기에 LIFO(Last-In, First-Out) 구조이다.
- 힙(Heap) 영역
new 키워드를 통해 생성된 인스턴스와 배열 등이 저장되는 공간이다.
모든 스레드가 이 영역을 공유하며, 가비지 컬렉터(Garbage Collector) 에 의해 관리되는 영역이기도 하다.
- Metaspace 영역

이 부분이 특히 생소했는데, 주의깊게 볼만한 점은
Java 7 까지 Permanent Generation (PermGen) 이라는 영역이 있었지만
Java 8 이후로 Metaspace 영역이 도입되어 사용되었는데, 여기에는 클래스에 대한 정보들(이름, 부모 클래스 이름, 필드 정보, 메소드 바이트코드, 생성자 바이트코드 등)과 static 변수, 상수 등이 저장된다.
모든 스레드가 공유하는 특징이 있고, 프로그램의 시작 시 생성되어 종료 시까지 유지된다.
위 사진과 아래 비교정리를 통해 PermGen과 Metaspace의 차이점에 대해 알아보고 넘어가자.
PermGen 과 Metaspace 의 차이점
PermGen 과 Metaspace 의 가장 큰 차이점은, 바로 해당 영역의 관리 주체다.
Java 7 까지 사용되던 PermGen은 JVM이 관리하였고, Java 8 이후 이를 대체한 Metaspace 는 운영체제(OS)가 관리한다.
PermGen 은 JVM 이 관리하기 위해 크기가 고정되었기에, 클래스 로딩이 많아지면 OutOfMemory 에러가 많이 발생하고, Full GC 대상이 아니라서 GC 빈도가 낮아 메모리 누수가 발생할 수 있다고. 이 문제를 해결하기 위해 Metaspace는 네이티브 메모리 영역에 할당되어서 크기가 동적으로 조정되고, Metaspace에 저장된 클래스 메타데이터는 GC 의 대상이 되며, 클래스로더가 종료되면 Metaspace 의 메모리가 해제되는 등 유연한 메모리 관리가 가능한 점에서 PermGen 에서 발생했던 문제를 해결할 수 있다.
여기서 다루지 못한 여러 개념들은 이후 JVM 과 GC 관련 포스팅을 따로 준비할 듯 하다..!
1. 생성자를 호출했을 때 생기는 일
이제 어떤 영역들이 존재하는지 알아봤으니, 생성자의 호출과 실행과정을 통해서
여러 변수들이 어떤 메모리 공간에 저장되고, 어떤 정보를 담고 있는지에 대해 알아보자.
아래와 같은 클래스가 있다.

이제 아래와 같이 new 연산자를 통해 생성자 호출 코드를 실행한다.

그렇다면 다음과 같은 타임라인으로 진행된다.
T0: 실행 전 상태
T1: 클래스 로딩
T2: 힙 메모리 할당 및 기본값 초기화
T3: 명시적 초기화
T4: 생성자 실행 시작
T5: 생성자 코드 실행
T6: 생성자 완료 및 참조 할당
객체 생성 완료
이제 타임라인에 따라가며 Stack, Heap, Metaspace 영역들이 어떻게 변하는지 살펴보자.
T0: 실행 전 상태

코드 실행 직전, JVM은 main 메소드를 위한 스택 프레임을 스택 영역에 생성한다.
이 시점에는 아직 객체가 생성되지 않았으므로 힙 영역은 비어있는 상태이다.
메타스페이스 영역에는 프로그램 실행에 필수적인 JDK 핵심 클래스들(java.lang.Object, java.lang.String) 이 JVM 시작 시점에 미리 로드되어 있다.
T1: 클래스 로딩

JVM이 'new MyClass()' 구문을 만나면, 메타스페이스에서 MyClass 의 클래스 메타데이터를 탐색한다.
만약 로드되어 있지 않은 클래스라면, 클래스 로더가 해당 .class 파일을 읽어 클래스 구조, 필드 정보, 메소드 바이트코드 등의 정보를 메타스페이스에 적재한다.
그리고 static 변수 type 도 초기화된다.
static 변수 자체는 클래스 메타데이터의 일부로서 메타스페이스에 위치하고, 이 변수가 참조하는 진짜 데이터인 "MyType" 문자열 리터럴 객체는 힙 내부의 String Constant Pool (SCP) 에 생성된다.
String Constant Pool (SCP) ?
자바에서는 문자열 리터럴 객체에 대한 저장공간을 힙 영역 내부에 마련했는데, 바로 String Constant Pool 이다.
만약 String 을 'new String' 키워드로 생성했다면 그 문자열은 SCP 에 들어가지 않지만, 큰 따옴표(" ") 로 초기화한 String 은 따옴표의 내용을 SCP 에 저장하고, 오로지 내용을 기준으로 저장하기 때문에, 똑같은 내용의 String 타입 변수 두 개를 따옴표로 초기화 했다면 두 변수는 SCP에 저장된 동일한 문자열 리터럴 객체를 참조하게 된다. 이러한 전략을 통해 메모리 낭비를 줄일 수 있다.
T2: 힙 메모리 할당 및 기본값 초기화 & T3: 명시적 초기화


클래스 로딩이 완료되면, JVM은 new 키워드를 통해 힙 영역에 MyClass 인스턴스를 위한 공간을 할당한다.
할당 직후에 이 공간의 모든 인스턴스는 기본값으로 초기화된다. (null, 0)
이어지는 명시적 초기화 과정에서, name 는 SCP에 존재하는 "기본값" 문자열 객체의 참조를 갖게 된다.
T4: 생성자 실행 시작

드디어 인스턴스 초기화의 마지막 단계인 생성자가 호출된다.
이제 스택 영역에는 'MyClass("홍길동")' 을 실행하기 위해 새로운 스택 프레임이 push 된다.
이 스택 프레임 내부에는 힙 영역에 생성된 인스턴스의 참조인 this 와 인자로 전달된 "홍길동" 문자열의 참조가 지역 변수로 저장된다.
T5: 생성자 코드 실행

이젠 생성자 내부의 코드를 실행할 차례다.
this.name = name; 구문에 의해 인스턴스 변수 name의 참조가 인자로 받은 "홍길동" 문자열 객체로 변경되고,
this.age = age; 구문은 age 값을 25로 변경한다. 이로써 힙에 생성된 인스턴스는 모든 초기화 과정을 마쳤다.
T6: 생성자 완료 및 참조 할당

생성자의 모든 코드가 실행을 마쳤기 때문에, 스택 영역 최상단에 있던 생성자 스택 프레임은 제거(pop) 된다.
그 후, 드디어 new 연산의 최종 결과물인 힙 영역의 객체 주소(참조)가 main 메소드의 스택 프레임에 있는 지역 변수인 obj 에 할당된다.
이 시점부터는 obj 변수를 통해 해당 인스턴스에 접근하고 메소드를 호출할 수 있게 된다.