들어가며
최근 자바 AOP에 관심이 생겨 수강했던 백기선의 더 자바 - 바이트코드 조작의 마법 강의 내용 중 바이트코드를 설명하기 전, JVM과 그것과 관련된 것들에 대한 내용을 접하게 되었다. 입사 후 자바를 사용한지 2년 가까이 되었지만 JVM과 관련된 지식은 학부생때 잠깐 배웠던 지식이 전부라 관련 내용을 정리해보고 싶어서 간단하게 정리하였다.
자바와 JVM, JRE, JDK
학부생 시절에 자바 수업을 위해 인터넷에서 자바를 다운로드받은적이 있다. 당시 다운로드할 때 jre, jdk 등 차이를 알수없는 요소들이 있던것으로 기억한다. JVM 구조에 대해 정리하기 전, 자바와 JVM, JRE, JDK가 어떻게 다른지 정리하려고 한다. 이 챕터를 읽고 나면 자바를 다운로드했다 라는 말에서 어색함을 느낄 수 있을것 같다.
JVM
JVM은 Java Virtual Machine 의 약자이며 자바의 바이트코드(.class) 파일을 OS에 특화된 코드(네이티브)로 변환하여 실행하는 역할을 한다. 여기서 중요한 점은 .java 파일을 바이트코드로 변환하는 것은 JVM의 역할이 아니라는 것이며 JVM은 이미 변환된 코드를 실행만 한다는 점이다.
추가로, 정확히 말하자면 JVM은 바이트 코드를 실행하는 표준이며 여러 밴더사에서 이 JVM의 구현체를 개발하고 개발자들은 이 구현체를 사용한다. JVM 밴더사로는 오라클, 아마존, Azul 등이 있으며, JVM은 바이트 코드를 네이티브 언어로 변환하여 실행하기 때문에 특정 플랫폼에 종속적이다.
JRE
JRE는 Java Runtime Environment 의 약자이며 앞서 언급한 JVM에 자바 애플리케이션에 필요한 라이브러리가 추가된 배포판이다. 자바 런타임 환경에서 사용되는 프로퍼티 세팅, 리소스 파일을 가지고 있기때문에 바이트코드를 실행할 수는 있지만, .java → .class 파일로 변환해주는 javac 와 같은 개발도구는 포함되지 않아 컴파일은 할 수 없다.
JDK
JDK는 Java Development Kit 의 약자이며 JRE에 개발자가 개발할 때 필요한 개발툴을 포함한다. 컴파일에 사용되는 javac과 같은 툴들이 여기 포함된다.
이제 우리는 자바를 다운로드한다 가 어색한 표현임을 알 수 있고, 자바 개발 혹은 실행을 위해 jre 혹은 jdk를 다운로드한다 고 표현할 수 있게 되었다.
추가로 몇년 전, 오라클에서 자바를 유료화 한다는 뉴스가 돌았고 당시 자바를 몰랐던 나는 앞으로는 자바가 개발 시장에서 사라지고 무료인 코틀린이 떠오를것이라고 생각해서 코틀린을 공부했던 적이 있다. 당시 오라클에서 발표한 바는 오라클에서 만든 JDK 11 버전부터 상용으로 사용할 시 유료 이다. 앞에서 언급한 조건 중 하나라도 해당되지 않으면 여전히 우리는 무료로 사용할 수 있다. 예를 들어 오라클에서는 일반 JDK 뿐만 아니라 Open JDK도 제공하는데 해당 JDK를 사용하면 무료, 밴더사는 오라클뿐만 아니라 아마존도 있으므로 아마존의 Corretto를 사용하면 무료이다. 이에 더해, 코틀린을 사용하더라도 오라클의 jdk11 버전을 사용한다면 이는 유료이다.
JVM 구조
Class Loader(클래스 로더)
클래스 로더는 바이트 코드를 읽어 메모리에 적재하는 역할을 하며 로딩, 링크, 초기화 단계로 이루어져 있다.
로딩 단계
로딩 단계에서는 클래스 로더가 .class 파일을 읽고 내용에 따라 바이너리 데이터를 만들어 Runtime Data Area의 Method Area에 저장한다. 이 때 저장되는 데이터는 클래스, 인터페이스, enum, 메소드, 변수등이 있다. 로딩이 끝나면 각 클래스 타입의 Class 객체를 생성하여 Heap Area에 저장한다.
클래스 로더에는 Bootstrap, Extension(Platform), Application 로더가 있으며 최초 로딩시에는 나열한 순서대로 로딩하고 만약 찾을 수 없다면 그 다음 로더에 로딩을 위임한다. 만약 맨 마지막의 Application 로더가 클래스를 찾지 못하면 ClassNotFoundException 이 발생한다.
링크 단계
링크 단계는 내부적으로 Verify, Prepare, Resolve의 세 단계로 이루어져 있으며 Verify 단계에서는 .class 파일의 형식이 유효한지 확인하며 다음 Prepare 단계에서는 static 변수와 기본값에 필요한 메모리를 준비한다.
마지막 Resolve 단계는 Optional한 단계이며, 심볼릭 메모리 레퍼런스를 Method Area의 실제 레퍼런스로 교체한다. A 객체가 B 객체를 참조한다고 했을 때, 최초 클래스 로드 시에는 B 객체의 참조가 아닌 논리적 참조가 선언된다. 이후 링크 단계 실행 혹은 실제 B 객체 참조시에 논리적 레퍼런스를 제거하고 앞서 로딩단계에서 Method Area에 적재한 실제 클래스로 레퍼런스를 변경하는 단계이다.
초기화 단계
마지막 초기화 단계는 링크의 Prepare 단계에서 준비한 메모리를 통해 static 변수의 값을 할당한다. 많이 사용하는 static 변수나 static 변수에 대한 처리가 이 때 이루어진다.
Runtime Data Area(메모리)
메모리 영역에 있는 다섯 가지 요소는 크게 두 유형으로 나눌 수 있는데, 프로세스 전역으로 공유되는 것과 쓰레드에서만 유효한 영역이다. 먼저 프로세스에서 공유되는 요소는 Method, Heap 이며 그 외 세 요소는 쓰레드별로 존재한다.
Process 단위 공유 요소
- Method
메소드 영역에는 클래스로더가 바이트코드를 통해 읽어들인 클래스 이름(풀 패키지 정보), 부모 클래스 이름, 메소드, 변수가 저장된다. - Heap
힙 영역에는 new 키워드 등을 통해 생성된 객체 인스턴스들이 저장된다.
Thread 단위 공유 요소
- Stack
스택은 쓰레드마다 런타임 스택이 생성되며, 메소드 호출 시 호출에 대한 스택 프레임 블럭이 쌓이게 된다. 개발중 에러 발생 시 에러 로그가 여러 줄 쌓인 모습을 볼 수 있는데, 해당 로그들이 stack에 쌓인 스택 프레임을 pop하여 찍힌 로그들이다. - Pc register
Program counter register의 약자이며 쓰레드마다 현재 실행할 스택 프레임을 가리키는 포인터이다. - Native Method Stack
쓰레드마다 네이티브 메소드를 호출할 때 사용하는 스택이며 위에서 언급한 Stack과 별도이다. 네이티브 메소드란 Java로 구현되지 않고 C 혹은 C++로 구현된 메소드들을 뜻하는데, Thread.currentThread() 메소드 등이 이에 해당된다. 아래 사진에서 확인할 수 있듯이 네이티브 메소드는 메소드 앞에 native 키워드가 붙은것을 확인할 수 있다.
Execution Engine
실행 엔진은 인터프리터와 JIT(Just In Time) 컴파일러로 이루어져 있으며, 이 두 요소와 메모리를 통해 코드를 읽고 실행한다.
흔히 자바를 컴파일 언어라고 하지만 실제 실행 엔진에서는 인터프리터를 통해 바이트코드를 읽어 네이티브 코드로 변환하여 한줄씩 실행한다. 이 경우, 중복되는 코드를 한줄씩 계속 읽으면 성능에 저하가 오기 때문에 JIT 컴파일러를 통해 최적화한다.
JIT 컴파일러는 인터프리터가 반복되는 코드를 발견했을 때, 해당 코드를 모두 네이티브 코드로 바꿔둔다. 그 이후부터는 인터프리터가 동일한 코드를 발견할 경우, JIT 컴파일러가 미리 네이티브 코드로 컴파일해둔 것을 사용함으로써 성능상의 이점을 가질 수 있게 한다.
정리
JVM, JRE, JDK가 무엇인지부터 시작해서 자바가 실행되는 과정에서 어떠한 요소들을 거치는지 정리해보았다. 실제 개발시에 이러한 요소들을 하나하나 생각하면서 개발하지는 않겠지만, 이 지식들을 알고 있다면 운영상 발생되는 이슈 혹은 개발중 발생하는 이슈들에 대한 원인 추적이 더욱 쉬울 것같다.
JVM 구성 요소 중 가장 중요한 GC는 그 자체만으로도 공부할 양이 많기 때문에 추가로 공부해서 다른 포스트를 통해 정리해보면 좋을 것 같다.
참조
- 백기선 - 더 자바: 코드를 조작하는 다양한 방법
'Java' 카테고리의 다른 글
Jackson 라이브러리의 직렬화/역직렬화 (0) | 2022.09.21 |
---|---|
제네릭을 이용한 마이바티스 쿼리 유틸 만들어보기 (0) | 2022.09.13 |
GC 개념 및 동작 원리 (0) | 2022.09.13 |
[Effective Java] Item02. 생성자에 매개변수가 많다면 빌더 패턴을 고려하라 (0) | 2022.01.14 |
[Effective Java] Item01. 생성자 대신 정적 팩토리 메서드를 고려하라 (0) | 2022.01.10 |
댓글