티스토리 뷰

JAVA

[JAVA] JVM의 구조

Odol87 2016. 4. 6. 03:12

1) 개요


 자바가 플랫폼에 종속되지 않을 수 있는 이유는 자바 바이트코드가 JRE(Java Runtime Environment) 위에서 동작하는 방식 때문이다. JRE의 핵심 구성요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java Virtual Machine)이다. 이번 포스팅에선 이 JVM의 구조에 대해 알아보겠다.


2) JVM


 IT관련 공부를 한 사람이라면 익숙한 단어인 VM은 말 그대로 가상 머신이다. 윈도우 환경에서 리눅스를 띄우기 위해 VMware나 Virtual box와 같은 VM을 활용한 경험이 다들 있을 것이다. 위의 두가지 경우 서로 다른 운영체제를 동시에 구동하기 위하여 소프트웨어적으로 운영체제를 구동할 수 있는 환경을 조성한 어플리케이션의 대표적인 예이다. 운영체제 별로 VM 어플리케이션만 준비가 되어 있다면 동시에 다른 운영체제에 수정을 가하지 않은 상태로 실행이 가능하게 된다.


 JVM 역시 같은 목적을 가지고 태어난 것이다. 자바의 원래 목표인 WORA(Write Once Run Anywhere)를 실현하기 위해 물리적인 머신과 별개의 가상 머신을 기반으로 동작하도록 설계한 핵심 기술이 JVM이다. 자바 바이트코드를 실행하기 위해 모든 플랫폼에 맞춰 JVM을 각각 만들어 자바 실행 코드를 변경하지 않고도 다양한 환경에서 실행이 가능하도록   설계된 것이다.


2.1 JVM의 특징


- 스택 기반 : x86 아키텍처나 ARM 아키텍처와 같은 하드웨어가 레지스터 기반으로 동작하는 데 비해 JVM은 스택 기반으로 동작한다.


- 심볼릭 레퍼런스 : 기본 자료형을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스(참조하고자 하는 대상의 이름만으로 참조관계를 구성)를 통해 참조한다.


- 가비지 컬렉션(Garbage Collection) : 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.


- 기본 자료형을 명확하게 정의 : C/C++등의 전통적인 언어는 플랫폼에 따라 int 형의 크기가 변한다. 반면에 JVM은 명확하게 기본 자료형을 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.


- 네트워크 바이트 오더 : 역시 플랫폼 독립성을 유지하기 위하여 빅 엔디안, 리틀 엔디안과같은 바이트오더를 네트워크 전송 시에 사용하는 네트워크 바이트 오더(빅 엔디안)를 사용한다.


자바 바이트코드에 대한 이해가 필요하지만 이는 다음에 다루기로 하고 넘어가겠다. JVM은  자바 실행코드가 컴파일러에 의해 자바 바이트코드로 변환된 것을 각각의 하드웨어에 맞게 실행하는 개념으로 잡으면 나중의 이해에 도움이 될 것이라 생각한다.


2.2 JVM 구조



그림 1 자바 코드 수행 과정


 클래스 로더가 바이트코드를 런타임 데이터 영역에 로드하고, 실행 엔진이 바이트코드를 실행하는 구조이다.


 - 클래스 로더


자바는 동적 로드(레이지 로딩, 인터프리터, JIT)라는 특징이 있다. 컴파일 타임에 필요한 클래스들을 로딩하는 것이 아니라 런타임에 클래스를 처음 참조할 때 해당 클래스를 로드하고 링크하는 방식을 취한다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 


 - 런타임 데이터 영역


그림 2 런타임 데이터 영역 구성


런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. 위의 PC 레지스터, JVM 스택, 네이티브 메서드 스택은 스레드마다 하나씩 생성되며 힘과 메서드 영역(런타임 상수 풀)은 모든 스레드가 공유한다.



  • PC 레지스터: PC(Program Counter) 레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다. 
  • JVM 스택: JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다. 예외 발생 시 printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다. 

JVMInternal5

  • 스택 프레임 : JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다. 각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다. 
  • 지역 변수 배열 : 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다. 
  • 피연산자 스택 : 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다. 

  • 네이티브 메서드 스택 : 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다. 

  • 메서드 영역 : 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다. 
  • 런타임 상수 풀 : 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 메서드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다. 
  • 힙 : 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다. 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더의 재량이다.

 - 실행 엔진


실행 엔진은 클래스 로더가 JVM 내의 런타임 데이터 영역에 배치한 바이트코드를 실행하는 역할을 한다. 자바 바이트코드는 완벽한 기계어가 아니라 실행 엔진에 의해 기계어로 변경되는데 그 방법은 두 가지가 있다.


  • 인터프리터 : 바이트코드 명령어를 하나씩 읽어 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠르나 결과의 실행은 느리다는 단점이 있다. 바이트코드라는 언어는 기본적으로 인터프리터 방식으로 동작한다. (한 줄씩 해석한다는 식의 표현을 하곤 하는데 자바의 실행 엔진은 바이트코드를 명령어 단위로 읽어서 실행하기 때문에 이 표현은 이 포스팅엔 적절치 않다고 생각한다)
  • JIT(Just In Time) 컴파일러 : 인터프리터 방식의 단점을 보완하기 위해 도입된 것이다. 인터프리터 방식으로 실행하다가 적절한(?) 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하여 캐시에 저장한 후 직접 실행하는 방식이다.
JIT 컴파일러가 컴파일 하는 과정은 인터프리팅하는 것보다 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅 하는것이 효율적이다. 그래서 JVM 벤더들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘는 경우에만 컴파일을 수행한다.


대표적으로 오라클 핫스팟 VM은 핫스팟 컴파일러라는 JIT 컴파일러를 사용한다. 이름과 같이 가장 컴파일이 필요한 부분인 '핫스팟'을 내부적으로 프로파일링을 통해 찾아 네이티브 코드로 컴파일하기 때문이다. 핫스팟 VM은 한번 컴파일된 바이트코드라도 해당 메서드가 더 이상 자주 불리지 않는다면 캐시에서 코드를 덜어내고 다시 인터프리터 모드로 동작한다. 또한 서버 VM과 클라이언트 VM으로 나뉘어 있고, 각각 다른 JIT 컴파일러를 사용하는데 이는 다음에 기회가 된다면 알아보도록 하겠다.


3) 정리 

 

JVM의 개략적인 내용을 네이버 D2 헬로월드의 내용을 정리해 보았다. "자바에서 Static 변수가 어디에 저장되는가?" 에 대한 답변을 준비하기 위해 참고했던 글이었다.


추가적으로 안드로이드의 Dalvik VM은 스택 기반이 아닌 레지스터 기반이고 컴파일 방식은 2.2부터 JIT 방식을 지원하며 롤리팝(안드로이드 5.0) 부터는 달빅 VM을 완전히 폐지하고 ART를 새로운 런타임으로 완전히 대체했다. ART에는 AOT(Ahead Of Time) 컴파일러가 포함되어 있는데, 이는 기계어 번역을 실행 전에 미리 완료하는 방식을 취하는데 한정적인 베터리와 램을 사용하는 스마트폰이라는 한계를 해결하기 위한 조치로 보인다. 실행 속도면에선 긍정적인 효과를 얻을 수 있었지만 길어진 설치 시간이나 기계어로 번역해 놓기 위한 2배정도의 설치 용량 등의 단점도 있다.


다음 포스팅은 자바의 Gargabe Collection에 대해 작성할 예정이다.


4) 출처


http://d2.naver.com/helloworld/1230

https://namu.wiki/w/안드로이드%20런타임




공유하기 링크
TAG
,
댓글
댓글쓰기 폼