일을 하면서
JVM은 구체적으로 메모리 관리를 어떻게 할까?
,JAVA의 GC는 어떻게 정리 대상 메모리인걸 판단할까?
에 대한 내용이 궁금했는데 찾아 보다가 적합한 도서로 보이는 JVM 밑바닥까지 파헤치기라는 책을 발견했고, 읽었고 내가 이해한 부분의 내용을 정리해봐야겠다.
class Student {
String name = "크리링";
public Student(){}
public void updateName(String name){
this.name = name;
}
}
Student student = new Student();
이 코드에서 JVM은 어떻게 동작할까?
클래스 로딩시 Student 클래스 정보 메서드 영역에 저장
크리링
문자열 String Constant Pool에 저장, name의 필드의 초기값으로 사용
클래스 로딩 여부 확인
클래스 메타정보를 기반으로 필요한 힙 메모리 크기 계산
힙에 메모리 할당 (TLAB 사용 등)
0으로 초기화
객체 헤더 설정 (class pointer, hash code, GC 정보 등)
인스턴스 필드 초기화 (name은 크리링
을 가리키게 됨)
생성자 호출
JVM의 메모리 영역에 대해서 알아보자
JVM은 자바 프로그램을 실행하는 동안 필요한 메모리를 몇 개의 데이터 영역으로 나눠 관리한다. 이 영역들은 각각 목적과 생성/삭제 시점이 있다.
(이미지 참고 : [Java] 런타임 데이터 영역(Runtime Data Area)에 대해)
현재 실행중인바이트코드 줄 번호 표시기
자바 가상 머신에서의 멀티스레딩은 CPU 코어를 여러 스레드가 교대로 사용하는 방식으로 구현되기 때문에 특정 시각에 각 코어는 한 스레드의 명령어만 실행
스레드 전환 후 이전에 실행하다 멈춘 지점을 정확하게 복원하려면 스레드 가각에는 고유한 프로그램 카운터 필요
각 스레드의 카운터는 서로 영향을 주지 않는 독립된 영역에서 저장
자바 메서드를 실행하는 스레드의 메모리 모델을 설명한다.
(연결된 스레드와 운명을 같이 한다.)
각 메서드가 호출될 때마다 자바 가상 머신은 스택 프레임을 만들어 지역 변수 테이블, 피연산자 스택, 동적 링크, 메서드 반환값 등의 정보 저장
그런 후 스택 프레임을 가상 머신 스택에 푸시하고, 메서드 끝나면 팝하는 일을 반복
데이터 공간은 컴파일 시에 할당
가상 머신을 어떻게 구현하느냐에 따라 슬롯 하나가 32비트, 64비트일 수 있다.
JVM 가상 머신 스택과 비슷한 역할
자바 애플리케이션이 사용할 수 있는 가장 큰 메모리
모든 스레드가 공유하며 가상 머신이 구동될 때 만들어진다.
객체 인스턴스 저장이 목적, 거의 모든 객체 인스턴스가 이 영역에 할당
GC 관리하는 메모리 영역 (GC 힙이라고 불리기도 함)
객체 할당 효율을 높이고자 스레드 로컬 할당 버퍼 여러개로 나뉜다.
모든 스레드가 공유
가상 머신이 읽어 들인 타입 정보, 상수, 정적 변수, JIT 컴파일러가 컴파일한 코드 캐시 등을 저장
public static void main(String[] args) {
List<Class> list = new ArrayList<>();
while(true) {
list.add(new Class());
}
}
static class Class {
private int x;
public Class() {
}
}
필요없는 객체가 원인이라면 메모리 누수
public class Main {
public static void main(String[] args) {
try {
Test2.test();
} catch (Exception e) {
e.printStackTrace();
}
}
static class Test2 {
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;
test();
}
}
스택 프레임
에 계속된 변수 할당으로 스택 프레임
메모리 초과class Student {
String name;
public Student(String name){
this.name = name
}
public void updateName(String name){
this.name = name;
}
}
Student student = new Student("크리링");
student.updateName("크리링크리링");
클래스 로딩시 Student 클래스 정보 메서드 영역에 저장
크리링
문자열 String Constant Pool에 저장, name의 필드의 초기값으로 사용
클래스 로딩 여부 확인
클래스 메타정보를 기반으로 필요한 힙 메모리 크기 계산
힙에 메모리 할당 (TLAB 사용 등)
0으로 초기화
객체 헤더 설정 (class pointer, hash code, GC 정보 등)
인스턴스 필드 초기화 (name은 크리링
을 가리키게 됨)
생성자 호출
크리링크리링
String Constant Pool
확인 및 생성
메서드 호출 및 name
참조 변경
name
파라미터에 크리링크리링
전달)this.name = name
실행class Student {
List<Integer> scores = List.of(100, 90, 80);
public Student(){}
public void updateScores(List<Integer> scores){
this.scores = scores;
}
}
Student student = new Student();
student.updateScores(List.of(100, 90, 80, 70, 60, 50, 40, 30, 20, 10));
클래스 로딩 여부 확인
클래스 메타정보를 기반으로 필요한 힙 메모리 크기 계산
힙에 메모리 할당 (TLAB 사용 등)
0으로 초기화
객체 헤더 설정 (class pointer, hash code, GC 정보 등)
인스턴스 필드 초기화 (scores = List.of(100, 90, 80)
실행됨)
List.of(...)
호출로 불변 리스트 객체 생성 → 힙에 저장scores
필드는 이 리스트의 참조값을 저장생성자 호출
List.of(100, 90, 80)
생성 후 scores
는 참조 값 저장List.of(100, 90, 80, 70, 60, 50, 40, 30, 20, 10
힙 영역에 생성 this.scores = scores
실행으로 참조값 변경응용 부분의updateName
과 updateScores
예시에서 더는 참조하지 않는크리링
값과 List.of(100, 90, 80)
값은 GC로 제거될까?
정답은 크리링은 아니다
와 List.of(100, 90, 80)은 맞다
이다.
이유는 String
은 메서드 영역의 String Constant Pool
에서 관리하여 GC 대상이 아니고
리스트
는 힙 영역
에서 관리하기 때문에 GC 대상이 되기 때문이다.
(다만 "크리링"도 new String("크리링")처럼 만들면 GC 대상이 될 수 있다)
그렇다면 GC는 어떻게 동작할까?
다음 글에...