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개의 댓글

Powered by GraphCDN, the GraphQL CDN