[Java] JVM 메모리 구조

bien·2025년 1월 22일
0

Java_Spring_Backend

목록 보기
4/10

JVM 구동 방식

JVM 메모리 구조

Method Area

메서드 영역은 보통 정적 영역이라 불린다. 프로그램 실행 중 클래스나 인터페이스를 사용하면, JVM은 클래스 로더를 이용해 클래스와 인터페이스의 메타데이터를 메서드 영역에 저장한다.

  • JVM이 동작해 클래스가 로딩될 때 생성
  • 클래스 로더에 의해 Load된 모든 Type의 메타 정보를 저장하는 메모리 공간
    • Type은 클래스와 인터페이스를 통칭한다.
    • Type 메타데이터
      • 런타임 상수 풀, 멤버 변수(필드), 클래스 변수(static 변수), 상수(final), 생성자(constructor)와 메서드(method)
  • 어느 곳에서나 접근 가능
    • 프로그램 시작부터 종료까지 메모리에 남아있어, 프로그램 종료까지 어디서든 사용 가능하다.
    • static 데이터를 무분별하게 많이 사용하는 경우 부족 현상이 나타날 수 있다.
  • JIT(Just In Time) 컴파일러가 번역한 기계어 코드를 캐싱하기 위한 메모리 공간으로 활용
    • 인터프리팅 기능의 양이 작아저 기능이 향상됨.
  • Java 8부터는 PermGen이 아니라 Metaspace에 속함
    • Metaspace는 JVM 힙이 아니라 네이티브 메모리에서 관리하며 크기가 동적으로 달라질 수 있음.

Runtime constant pool

  • 클래스 버전, 필드, 메서드, 인터페이스 등 클래스 파일에 포함된 정보 및 각종 리터럴, 심벌 참조가 저장되는 영역
    • 상수(클래스 자체에 대한 메타데이터)들이 저장되는 공간.
    • 심볼(특정 클래스가 어떤 클래스를 어떻게 참조해 어떤 메서드를 콜하는지)를 저장하는 정보
    • 해석: 그냥 문자열로 적혀있는게 심볼참조를, 실제 메서드 영역의 자바 바이트 코드 위치의 메모리 주소를 찾아서 그걸 콜하도록 바인딩하는 과정
  • 클래스 로더가 클래스를 로드할 때 상기 정보들을 저장
  • 동적으로 운영되며 런타임에 새로운 상수가 추가될 수 있음
    • 코드 수준에서 상수를 기술, 컴파일 타임에 로딩된다고 기술.

코드 자체는 짧더라도 상수 풀에는 상당량의 데이터가 들어온다.
따라서 클래스 양이 늘어나면, 이 상수풀의 양도 폭발적으로 늘어날 수 있다.

Method 영역 데이터 할당 과정

Stack area

스택은 각 스레드가 실행 중인 메서드 호출 및 반환에 관여하고 지역 변수와 결과들을 저장해, 메서드 호출과 실행에 대한 전반적인 관리를 수행한다.

  • 자바 스택은 스레드별로 1개가 존재하며, 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택이다.
    • LIFO(Last In First Out) 구조로 관리된다.
    • 스택 프레임은 메서드가 호출될 때마다 생성된다.
    • 메서드 실행이 끝나면 스택 프레임은 pop되어 스택에서 제거된다.

스택 프레임(Stack Frame)

  • 하나의 메서드에 필요한 메모리 덩어리

구성요소

  1. 지역 변수 배열 (Local Variable Array)
    • 메서드 내에서 사용되는 모든 지역 변수들이 저장된다.
    • 0부터 시작하는 인덱스를 가진 배열 형태로 관리된다.
    • 다음 데이터들이 포함된다.
      • 메서드 파라미터
      • 지역 변수
      • 인스턴스 메서드의 경우 this 참조 (index 0에 위치)
  2. 피연산자 스택 (Operand Stack)
    • 메서드 실행 중 실제 작업이 이루어지는 공간
    • 다음의 상황에서 사용된다.
      • 연산을 위한 피연산자 보관
      • 메서드 호출 시 전달할 파라미터 저장
      • 메서드의 반환값 임시 저장
  3. 상수 풀 참조 (Runtime Constant Pool Reference)
    • 해당 메서드가 속한 클래스의 상수 풀 참조
    • 다음의 정보들을 포함한다.
      • 리터럴 상수
      • 타입 참조
      • 필드 참조
      • 메서드 참조
  4. 실행 환경 정보
    • 메서드의 실행과 관련된 부가적인 정보들이 저장된다:
      • 메서드 호출자 정보
      • 예외 처리 정보
      • 디버깅 관련 정보

스택의 push와 pop

스택이 프레임을 push하고 pop하는 과정은 아래와 같다.

public class Example {
	public static void main(String[] args) {
    	int result = methodA(); // methodA의 결과물을 받음
        System.out.println(result);
    }
    
    public static int methodA() {
    	int value = methodB(); // methodB의 반환값을 받음
        resurn value + 10; // methodB의 결과에 10을 더해서 반환
    }
    
    public static int methodB() {
    	reuturn 5;
    }

}

스택의 push 과정

  1. main() 호출
    • 스레드가 시작되면 main()이 호출된다. main()의 프레임이 스택에 push된다.
    • 이 프레임에는 mina()의 파라미터, 지역변수, 연산결과를 위한 피연산자 스택 정보들이 포함된다.
  2. main()에서 methodA() 호출
    • methodA()의 프레임이 main()의 프레임 위 스택에 push된다.
    • methodA()의 프레임도 역시 파라미터, 징겨변수, 피연산자 스택이 포함된다.
  3. methodA()에서 methodB() 호출
    • 위와 마찬가지로 methodB()의 프레임이 스택에 push된다.

스택의 pop 과정

반대 순서대로 (methodB 프레임 -> methodA 프레임 -> main 프레임) 스택에서 pop된다.
반환되는 과정에서 반환된 값이 있다면, 이전 프레임의 피연산자 스택에 push된다.
예를들어, methodB()에서 반환된 5는 methodA()의 프레임의 피연산자 스택에 push되어 사용되어 진다.

스택의 예외처리

  • StackOverflowError
    • 스레드의 계산이 허용된 JVM 스택 크기보다 큰 스택을 필요로 하는 경우, JVM은 StackOverflowError를 던진다.
  • OutOfMemoryError
    • JVM 스택이 동적으로 확장될 수 있는데 확장 시도 시 충분한 메모리를 확보할 수 없거나, 새 스레드를 위한 초기 JVM 스택을 생성할 때 충분한 메모리를 확보할 수 없는 경우, JVM은 OutOfMemoryError를 던진다.

Stack 영역 데이터 할당 과정

Heap Area

  • 모든 JVM 스레드간에 공유되는 공간.
    • 동기화 이슈가 수반된다.
    • stack은 스레드 갯수마다 각각 생성되지만, heap은 몇 개의 스레드가 존재하든지 상관없이 단 하나의 heap 영역만 존재한다.
    • 멀티스레딩 환경에서 스레드 간 공유되므로 자원을 효율적으로 사용할 수 있다는 장점이 있지만, 데이터의 일관성과 스레드의 안정성을 보장하기 위한 추가적 처리가 필요하다.
  • 참조형(Reference Type) 데이터 타입을 갖는 객체(인스턴스, Instance)와 배열(Array)이 저장되는 공간
    • 단, Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수는 stack에 적재
  • JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역
  • Heap 영역은 Stack 영역과 다르게 보관되는 메모리가 호출이 끝나더라도 삭제되지 않고 유지된다.
    • 그러다 어떤 참조 변수도 Heap 영역에 있는 인스턴스를 참조하지 않게 된다면, GC(가비지 컬렉터)에 의해 메모리에서 청소된다.

Heap 영역의 세부 구조

  1. Young Generation
    • Eden Space
      • 새로 생성된 객체가 배치되는 공간. 대부분의 객체가 여기에 생성되고, 일정 시간이 지나지 않고 사용되지 않는다면 소멸된다.
    • Survivor Space (S0 and S1)
      • Eden 영역에서 살아남은 객체들이 이동하는 곳.
      • 객체들은 S0과 S1 사이에서 이동하며, 여러 번의 가비지 컬레션(GC) 사이클을 거치면서 살아남은 객체는 Old Generation으로 이동한다.
  2. Old Generation
    • 장시간 동안 사용되는 객체들이 이동하는 영역.
    • Young Generation에서 살아남은 객체들이 이곳으로 이동하며, 가비지 컬렉션이 발생하는 빈도가 상대적으로 낮다.

📍 Metaspace

  • Java8버전 이후의 Heap 영역의 구조는 Young Generation, Old Generation만 포함한다.
    • Java8 버전 이전: 클래스의 메타데이터가 Heap영역에 저장(이 공간을 PermGen 영역이라 함) 되었다.
    • Java8 버전 이후: Metaspace라는 공간이 운영체제가 관리하는 네이티브 메모리 영역으로 분리되면서 메모리 관리 전력이 진화함
  • 이전(Java8이전)에는, JVM 시작 시 설정된 고정된 크기에 많은 클래스 로드와 많은 정적 데이터를 사용하는 경우 PermGen 영역이 고갈되면서 java.lang.OutOfMemoryError:PermGen space오류로 이어지곤 했다.
    • Metaspace라는 네이티브 메모리 영역을 사용함으로써 JVM 메모리 요구사항에 따라 필요한만큼 동적으로 메모리를 할당받을 수 있게 되었다.

Heap 영역 데이터 할당 과정

  1. 생성자 new Counter()를 호출한다.

    • 생성자를 호출하면 heap 영역에 Counter 클래스 인스턴스 변수들이 저장되게 되고, stack 영역의 지역변수 sub에 주소값으로 연결되게 된다.
  2. twice(sub) 메소드를 실행한다.

    • 새로운 메서드를 실행하는 것이니, stack 영역에 새로운 스택 프레임이 생기게 된다.
    • 그리고 아규먼트로 클래스를 전달했기 때문에 twice()의 매개변수 c는 주소값으로 같은 힙 영역을 가리키게 된다.
  3. 객체의 plus() 메소드를 실행한다.

    • 객체 Counter에 정의된 plus() 메소드를 호출하게 되는데, 이 역시 메소드이므로 스택 영역에 새로운 스택 프레임으로 생성되게 된다.
    • 다만 여기서 this라는 암묵적인 변수가 자동 생성되게 되는데, 이 this 변수는 자동으로 힙 영역에 있는 Counter 객체를 기리키게 된다.
    • 따라서 plus() 메소드 안의 코드 state += n이 동작하면서 힙 영역에 있는 인스턴스 변수 state가 값이 변하게 된다.
  4. 객체의 state 인스턴스 변수를 가져오는 get() 메소드를 호출한다.

    • 모두 실행되어 볼일이 끝난 plus() 스택 프레임은 제거된다.
    • 마찬가지로 sub 객체변수의 메소드 get()을 호출하면 스택 영역에 새로운 스택 프레임이 생기고, this 변수가 힙 영역의 객체를 가리키게 된다. 그리고 힙 영역의 변수를 반환하게 된다.
    • 마지막으로 할일을 마친 get() 스택 프레임은 스택 영역에서 제거되고, main 스택 프레임에 result2 지역변수가 추가된다.

📌 주의사항

  • 이 예제에서 강조되는 부분은 호출되는 메서드가 파라미터로 객체값을 전달받아 객체의 상태를 변경하게 되면, 메서드 종료(스택 제거) 이후에도 힙 영역에 있는 객체의 상태는 쭉 유지된다는 점이다.
  1. 마지막 코드가 실행되면 main 스택 프레임은 스택 영역에서 제거된다.

    • 스택 영역은 메서드의 끝을 알리는 닫는 중괄호 }를 만나면 자동으로 메모리에서 제거된다.
    • 그러나 힙 영역에서는 여전히 객체 데이터가 메모리에 상주하게 된다.
  2. 가비지 컬렉터(GC)가 힙 영역을 청소한다.

    • 가비지 컬렉터는 힙 영역에 참조되지 않고 남아버린 고아 객체들을 식별해 힙 영역을 청소해주는 역할을 한다.
    • 추가로 코드 실행이 모두 끝나면 Method(Static) 영역도 비워지게 된다.

PC(Program Counter) Registers

JVM은 멀티스레드 환경을 지원하며, 각 스레드는 독립적인 PC Register를 가진다. PC Register는 다음과 같은 핵심 특징을 가진다.

  • 명령어 위치 추적
    • 현재 실행 중인 JVM 명령어의 주소를 저장한다.
    • 명령어 실행이 완료되면 다음 명령어의 주소를 자동 갱신한다.
  • 스레드별 할당
    • 각 스레드는 자신만의 PC Register를 보유한다.
    • 스레드는 독립적인 호출 스택을 가지며 한 번에 하나의 메서드만 실행한다.
  • 실행 흐름 관리
    • PC Register는 Java Byte Code레벨에서 실행 흐름을 괸리한다.:
      • 현재 실행 중인 작업의 위치
      • 다음 실행할 작업의 위치
      • 전체적인 실행 흐름의 제어

Native method stack

자바 외의 언어로 작성된 네이티브 코드를 위한 스택.

  • JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택. 언어에 맞게 C 스택이나 C++ 스택이 생성된다.
    • 자바 메서드가 호출될 때에는 스택 프레임이 스택에 추가되지만, native 메서드가 호출될 때는 새로운 스택 프레임이 추가되는 것이 아니라, 동적으로 native 메서드를 연결한다.

Reference

profile
Good Luck!

0개의 댓글