자바 프로그램이 실행되기 위해선 다음과 같은 과정을 거친다.
- 인텔리제이 같은 editor로 자바 소스 코드를 작성한다. (Simple.java 작성)
- 프로그램을 컴파일러(javac)가 바이트코드로 컴파일한다. (Simple.class 파일 생성)
- JVM의 실행 엔진이 바이트코드를 기계어로 변환시켜 실행한다.
- 실행 엔진이 필요한 클래스를 요청하면 클래스 로더가 클래스를 로드한다. (동적 로딩)
JVM이란
JAVA Virtual Machine은 자바 바이트코드를 실행하는 가상 머신이다. 이 바이트코드는 .java 파일을 컴파일한. class파일로부터 얻을 수 있다.
JVM은 운영 체제에 의존하지 않는 인터페이스를 제공하기 때문에 가상이라고 한다. 이러한 OS 및 하드웨어로부터의 독립성은 java 프로그램의 write-once-run-anywhere 가치의 초석이다.
JVM은 GC, adaptive optimizer를 사용해 java 애플리케이션을 최적화할 수 있다.
JVM 구조
클래스 로더
클래스 로더는 클래스 파일을 로드하는 데 사용되는 하위 시스템이다. 로드한다는 것은 클래스가 요청될 때 javac가 컴파일한 .class 파일을 읽고 바이너리 데이터를 생성하여 메모리에 저장하는 것을 의미한다.
한 번에 모든 클래스를 다 로드하지 않고 요청이 오면 로드한다.
로딩, 링크, 초기화 순으로 진행된다.
Loading
클래스 파일(.class)을 바이트코드로 읽어 메모리로 불러오는 과정이다.
아래의 클래스 수준의 정보를 메모리의 메소드 영역에 저장한다.
- 풀 패키지 경로 포함 클래스명
- " .class" 파일이 Class 또는 Interface 또는 Enum인지에 대한 정보
- 수정자, 변수 및 메서드 정보 등
.class 파일을 로드한 후 JVM은 힙 메모리에 이 파일을 나타내기 위해 Class 타입의 객체를 생성한다. 이 객체는 java.lang 패키지에 미리 정의된 클래스 타입이다.
이 클래스 객체는 클래스 이름, 부모 이름, 메서드 및 변수 정보 등과 같은 클래스 수준 정보를 얻기 위해 개발자가 사용할 수 있다.
Link
링크는 세 단계에 걸쳐 이루어진다.
- Verify : .class 파일의 정확성을 검증한다. 올바른 형식인지, 유효한 컴파일러에서 생성되었는지를 확인한다. 검증이 끝나면 클래스 파일을 컴파일할 준비가 된 것이다.
- Preparation : 클래스에 필요한 메모리 양을 할당하고 기본값으로 초기화한다.
- Resolve : 심볼릭 레퍼런스를 메모리 영역에 있는 실제 레퍼런스로 바꾸는 과정이다.
- 어떤 객체가 다른 객체의 참조를 가지고 있는 경우 링크가 일어나기 전에는 힙에 있는 참조 객체의 실제 주소를 가지고 있지 않는다. 링크의 Resolve 과정에서 힙에 있는 객체를 가리키게 된다.
- 로딩 단계에서 일어날 수도 있고 실제 레퍼런스를 참조할 때 이뤄질 수도 있다.
Initialization
static 변수의 값을 할당하고 static 블록이 있다면 이때 실행된다.
세 가지 클래스 로더
- 부트 스트랩 클래스 로더 : HOME/lib에 있는 코어 자바 API를 제공. 최상위 우선순위를 가진 클래스 로더
- 플랫폼(Extention) 클래스 로더 : JAVA_HOME/lib/ext 폴더 또는 java.ext.dirs. 시스템 변수에 해당하는 위치의 클래스를 읽는다.
- 애플리케이션 클래스 로더 : 애플리케이션 클래스 패스에서 클래스를 읽는다.
자바 클래스로더의 세 가지 원칙
- Delegate(위임) - 하위 클래스 로더는 상위 클래스 로더에게 로딩을 위임한다. 최상위 클래스부터 하위 클래스 로더가 차례로 읽고 클래스가 로더에 의해 발견되지 않으면 ClassNotFoundException이 발생한다.
- Visibility(가시성) - 클래스 로더들은 계층구조이기 때문에 하위 클래스 로더는 상위 클래스 로더가 로드한 클래스를 알 수 있다. 하지만 상위 클래스 로더는 하위 클래스 로더가 로드한 클래스를 알지 못한다.
- Uninqueness(유일성) - 하위 클래스 로더가 상위 클래스 로더에서 로드한 클래스를 다시 로드하지 않아야 한다.
JVM 메모리 영역
- 메소드 영역에는 클래스 수준의 정보가 저장된다. (클래스명, 상속 관계, 메서드, 변수) 모든 스레드에서 공유하는 자원이다.
- 힙 영역에는 애플리케이션 실행 중에 생성되는 모든 객체를 저장한다. 모든 스레드에서 공유하는 자원이다.
- 스택은 지역 변수와 중간 결과를 저장한다. 이러한 모든 변수는 만들어지는 스레드에 대해 로컬이고 각 스레드에는 자체 JVM 스택이 있다. 스레드마다 런타임 스택을 만들고 그 안에 메서드 호출을 스택 프레임이라 부르는 블록으로 쌓는다. 스레드가 종료되면 런타임 스택도 사라진다.
- PC 레지스터는 현재 실행 중인 명령문의 물리적 메모리 주소를 저장한다. java에서 각 스레드에는 별도의 PC 레지스터가 있다.
- java는 네이티브 코드도 지원한다. (C, C++) 네이티브 메서드 스택은 네이티브 코드를 실행하기 위한 스택이다.
JVM 실행 엔진
JVM에 할당된 모든 코드는 실행 엔진에 의해 실행된다. 실행 엔진은 바이트코드를 읽고 하나씩 실행한다. 내장된 interpreter와 JIT 컴파일러를 사용해 바이트코드를 기계어로 변환하고 실행한다.
JVM을 사용하면 인터프리터와 컴파일러 모두 네이티브 코드를 생성한다. 차이점은 코드를 생성하는 방법, 최적화 방법, 최적화 비용에 있다.
- 인터프리터: 바이트 코드를 (네이티브 코드로 변환) 한 줄씩 실행.
- JIT 컴파일러: 인터프리터 효율을 높이기 위해 런타임에 상호작용을 하여 컴파일한다. 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바꿔둔다. 그다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용한다.
- JIT 컴파일러는 기본적으로 활성화되어 있다. 비활성화할 수도 있는데 JIT의 문제점을 발견하거나 해결하는 경우를 제외하고는 하지 않는 것이 좋다.
- GC(Garbage Collector): 더 이상 참조되지 않는 객체를 모아서 정리한다.
참고 링크
'JAVA' 카테고리의 다른 글
테스트 더블(Test Double) (0) | 2022.04.09 |
---|---|
상태 패턴 (in 체스 미션) (0) | 2022.04.04 |
IllegalArgumentException vs IllegalStateException (0) | 2022.03.31 |
[JAVA] Enum으로 if문 제거하기 (0) | 2022.03.16 |
옵저버 패턴(Observer Pattern) 살펴보기 (0) | 2022.03.05 |