반응형
JVM 아키텍처 및 동작 방식
https://www.geeksforgeeks.org/java/how-jvm-works-jvm-architecture/
JVM은 크게 세 가지 하위 시스템으로 구성:
- 클래스 로더 서브시스템 (Classloader Subsystem)
- 런타임 데이터 영역 (Runtime Data Areas)
- 실행 엔진 (Execution Engine)
1. 클래스 로더 서브시스템 (Classloader Subsystem)
자바 프로그램이 실행될 때 .class 파일을 읽어와 JVM 메모리에 로드하는 역할을 합니다. 클래스 로더는 세 단계로 작동함.
- 로딩 (Loading):
- .class 파일을 읽어와 바이너리 데이터를 생성하고, 이 데이터를 메소드 영역에 저장.
- 클래스 이름, 부모 클래스 이름, 인터페이스 이름, 필드, 메소드 등 클래스에 대한 모든 정보가 로드됨.
- 로딩 시, JVM은 로드된 클래스의 .class 파일에 대한 바이너리 표현을 나타내는 java.lang.Class 타입의 객체를 힙(Heap) 영역에 생성.
- 링크 (Linking):
- 로드된 클래스 파일을 실행하기 위해 준비하는 단계입니다.
- 검증 (Verification): 로드된 .class 파일이 유효하고 안전한지 확인합니다. (예: 바이트코드의 형식이 올바른지, 보안상 문제가 없는지 등) 이 단계에서 문제가 발견되면 VerifyError가 발생할 수 있습니다.
- 준비 (Preparation): 클래스 변수(static 변수)에 필요한 메모리를 할당하고 기본값으로 초기화합니다. (예: int는 0, boolean은 false, 참조 타입은 null 등). 정적 변수의 실제 초기화(사용자가 지정한 값)는 초기화 단계에서 일어납니다.
- 해결 (Resolution): 심볼릭 참조(Symbolic References)를 직접 참조(Direct References)로 변환합니다. 심볼릭 참조는 클래스나 메소드의 이름과 같은 추상적인 참조를 말하며, 이를 실제 메모리 주소와 같은 직접적인 참조로 바꾸는 과정입니다.
- 초기화 (Initialization):
- 이 단계는 클래스 로딩 과정의 마지막 단계입니다.
- 클래스 변수(static 변수)에 명시적인 값을 할당하고, 정적 초기화 블록 (static { ... })을 실행합니다.
- JVM은 스레드로부터 안전한 방식으로 클래스를 초기화합니다 (여러 스레드가 동시에 초기화를 시도하지 않도록 보장).
클래스 로더의 종류:
- 부트스트랩 클래스 로더 (Bootstrap Classloader): JVM의 핵심 클래스(예: rt.jar에 있는 java.lang.* 등)를 로드합니다. $JAVA_HOME/jre/lib 디렉토리에 있는 클래스들을 로드합니다.
- 확장 클래스 로더 (Extension Classloader): 확장 디렉토리(예: $JAVA_HOME/jre/lib/ext)에 있는 클래스들을 로드합니다.
- 애플리케이션/시스템 클래스 로더 (Application/System Classloader): 클래스패스(Classpath)에 지정된 애플리케이션 클래스들을 로드합니다. 이것은 개발자가 작성한 클래스들이 로드되는 주된 로더입니다.
2. 런타임 데이터 영역 (Runtime Data Areas)
JVM이 프로그램을 실행하면서 사용하는 메모리 영역입니다. 각 스레드마다 고유한 영역과 공유 영역 존재
- 메소드 영역 (Method Area):
- 모든 JVM 스레드가 공유하는 영역
- 클래스 구조(런타임 상수 풀, 필드 및 메소드 데이터, 메소드 및 생성자의 코드 등)와 정적 변수가 저장됨.
- 하나의 JVM 당 하나의 메소드 영역만 존재합니다.
- 힙 영역 (Heap Area):
- 모든 JVM 스레드가 공유하는 영역
- new 키워드를 사용하여 생성된 모든 객체(Object)와 배열(Array)이 저장됨.
- 가비지 컬렉터(Garbage Collector)의 주된 대상이 되는 영역
- 스택 영역 (Stack Area):
- 각 JVM 스레드마다 고유하게 생성되는 영역
- 메소드가 호출될 때마다 스택 프레임(Stack Frame)이 생성되어 이 영역에 푸시(push)됩니다.
- 각 스택 프레임은 다음을 포함합니다:
- 지역 변수 배열 (Local Variable Array): 메소드 내의 지역 변수와 매개변수를 저장
- 피연산자 스택 (Operand Stack): 연산을 위한 피연산자를 임시로 저장하는 스택
- 프레임 데이터 (Frame Data): 메소드 호출에 필요한 기타 정보(예: 상수 풀 참조, 예외 처리 정보 등)를 저장함.
- 메소드 실행이 완료되면 해당 스택 프레임은 팝(pop)되고 제거됨.
- PC 레지스터 (PC Registers):
- 각 JVM 스레드마다 고유하게 생성
- 현재 실행 중인 JVM 명령어의 주소를 저장합니다.
- 메소드가 JVM 메소드인 경우 명령어의 주소를 저장하고, native 메소드인 경우 그 값을 정의하지 않음.
- 네이티브 메소드 스택 (Native Method Stacks):
- 각 JVM 스레드마다 고유하게 생성됩니다.
- JVM이 Java 코드가 아닌 C/C++과 같은 네이티브 메소드를 호출할 때 사용되는 스택입니다. (JNI - Java Native Interface를 통해 호출될 때 사용)
- 인터프리터 (Interpreter):
- 바이트코드를 한 줄씩 읽어와 기계어로 번역하고 바로 실행
- 장점: 빠른 시작 시간.
- 단점: 동일한 코드가 반복적으로 호출될 때 매번 번역해야 하므로 전체 실행 속도는 느릴 수 있음.
- JIT (Just-In-Time) 컴파일러:
- 인터프리터의 단점을 보완하기 위해 도입되었습니다.
- 자주 사용되는(핫 스팟) 바이트코드 블록을 통째로 읽어와 한 번에 네이티브 기계어 코드로 컴파일하고 캐싱합니다.
- 이렇게 컴파일된 코드는 다음에 동일한 블록이 실행될 때 다시 번역할 필요 없이 바로 실행될 수 있어 성능을 크게 향상시킵니다.
- JIT 컴파일러는 HotSpot이라는 기술을 사용하여 자주 실행되는 코드를 식별합니다.
- 가비지 컬렉터 (Garbage Collector):
- 힙 영역에서 더 이상 참조되지 않는(사용되지 않는) 객체를 자동으로 찾아 메모리에서 제거하여 메모리 누수를 방지하고 자원을 효율적으로 관리함.
- 개발자가 명시적으로 메모리를 해제할 필요가 없습니다.
- 런타임 데이터 영역에 로드된 바이트코드(.class 파일)를 실행하는 역할
참조 타입(reference type)
- 객체의 번지를 참조하는 타입
- 참조 타입으로 선언된 변수(a.k.a 참조변수) 는 객체가 생성된 메모리 번지를 저장
- 내부 포인터로 JVM이 메모리 주소를 변경할 권한을 가짐. 프로그래머가 핸들링할 필요 없음. 위임한 것임.
- . : 주소값을 참조한다. Ex. i.cv : 참조변수 i의 주소값을 참조해서 cv 라는 변수에 접근
- 책 : JVM 밑바닥까지 파헤치기
- JVM은 운영체제에서 할당받은 메모리 영역을 메소드, 힙, 스택 영역으로 구분해서 사용
- 메소드 영역 : 바이트코드 파일을 읽은 내용이 저장되는 영역, static 으로 만들어지는 영역.
- 힙 영역 : 객체가 생성되는 영역. 객체의 번지는 메소드 영역과 스택 영역의 상수와 변수에서 참조
- 스택 영역 : 메소드를 호출할 때마다 생성되는 프레임(main(), println() 등의 메소드 프레임 안의 지역변수, 매개변수 등)이 저장되는 영역
- 위 코드의 수행 과정 정리
- InitTest 가 메모리에 로드되면서 클래스 정적 변수 cv가 메소드 영역에 생성되고, 기본 값인 0으로 자동적으로 초기화된다. (InitTest 클래스가 메모리 주소 0xaaaa에 생성되었다고 하자)
- 명시적 초기화에 의해 cv가 1로 초기화된다.
- static 초기화 블럭에 의해서 cv 에 2가 저장된다. 이처럼 클래스 변수의 명시적 초기화와 클래스 초기화 블럭은 클래스가 메모리에 로드될 때 단 한번만 수행된다.
- 클래스가 메모리에 로드된 다음, java.exe는 InitTest 클래스의 main 메소드를 호출한다. 호출 스택에 main 메소드를 위한 공간이 마련된다. (메인 메소드를 위한 스택 프레임(Stack Frame))
- 참조변수 i와 InitTest 인스턴스가 생성된다. 이때, 인스턴스 변수 iv가 생성된다. iv는 기본값인 0으로 자동적으로 초기화 된다. 모든 인스턴스는 자신을 생성한 클래스의 주소를 갖고 있다.
- 명시적 초기화에 의해 iv 가 1로 초기화 된다.
- 인스턴스 초기화 블럭에 의해 iv 에 2가 저장된다.
- 생성자가 호출되어 iv 에 3이 저장된다. 인스턴스 초기화블럭이 생성자보다 먼저 수행된다는 것을 기억하자.
- 대입연산자에 의해 생성된 InitTest 인스턴스의 주소가 i에 저장된다.
- cv 의 값을 화면에 출력한다. (2)
- iv 의 값을 화면에 출력한다 (3)
- 메인 메소드의 모든 문장에 수행되었으므로 전체 프로그램이 종료된다.
- class InitTest { static int cv = 1; int iv = 1; static { cv = 2; } InitTest() { iv = 3; } { iv = 2} public static void main(String[] args) { InitTest i = new InitTest(); System.out.println(cv); System.out.println(i.iv);
- JVM이 객체를 생성할 때 다음과 같은 순서로 초기화를 진행.
- 기본값 초기화: 모든 인스턴스 변수는 기본값(int의 경우 0)으로 초기화됩니다.
- 명시적 초기화: int iv = 1;과 같이 선언 시 할당된 값이 초기화됩니다. (iv는 1이 됨)
- 인스턴스 초기화 블록 실행: { iv = 2; } 블록이 실행됩니다. (iv는 2가 됨)
- 생성자 실행: InitTest() 생성자가 실행됩니다. 생성자 내부의 iv = 3;이 실행됩니다. (iv는 최종적으로 3이 됨)
- itar 을 넣으면 for문 자동 완성 : Iterate elements of Array
- iter 을 넣으면 향상 된 for문 으로 자동 완성 : Iterate Iterable or Array
- 향상된 for문 (for-each): 배열의 모든 원소를 읽기 전용으로 순회할 때 편리합니다. 각 원소의 값을 읽어와 처리하는 용도
- 향상된 for문은 내부적으로 **반복자(Iterator)**를 사용하거나, 배열의 경우 인덱스를 사용하여 각 원소의 '복사본'을 가져와서 처리
- 스택 영역의 참조 변수(Ex. int[] arr 에서 arr)에는 힙 영역에 있는 배열 객체(Ex. new int[5])의 '시작 메모리 주소'(Ex. 주소 0xABC123) 가 저장됩니다.
- System.out.println(arr)와 같이 배열 객체를 직접 출력할 때 나타나는 값은 해당 배열 객체의 해시 코드(hashCode)를 16진수로 변환한 값입니다.
- 정확히 말하면, Object 클래스의 toString() 메소드가 호출되고, 이 메소드는 기본적으로 getClass().getName() + "@" + Integer.toHexString(hashCode()) 형식의 문자열을 반환합니다.
- 예시: [I@1b6d3586
- [I: 배열의 타입을 나타냅니다. [는 배열을, I는 int 타입 배열임을 의미합니다. (참고: [Ljava.lang.String;는 String 배열을 의미합니다)
- @: 구분자입니다.
- 1b6d3586: 해당 배열 객체의 해시 코드를 16진수로 표현한 값입니다.
반응형