상세 컨텐츠

본문 제목

[JAVA] 자바스터디 1주차 - JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가

개발 공부 (etc)/JAVA

by letprogramming 2021. 1. 24. 06:04

본문

반응형

목표

자바 소스 파일 (.java)을 jVM으로 실행하는 과정 이해하기.


학습할 것

  • JVM이란 무엇인가
  • 컴파일 하는 방법
  • 실행하는 방법
  • 바이트코드란 무엇인가
  • JIT 컴파일러란 무엇이며 어떻게 동작하는지
  • JVM 구성 요소
  • JDK와 JRE의 차이

JVM이란 무엇인가

 

JVM(JAVA Virtual Machine)은 우리나라 말로 자바 가상 머신의 줄임말이다.

JAVA는 프로그래밍 언어라는 것을 알지만 가상 머신에 대한 개념을 알아보면

프로그램을 실행하기 위한 하드웨어를 소프트웨어로 구현한 것이라고 정의할 수 있다

 

자바의 목표는 WORA(Write Once Run Anywhere)이다.

이 목표를 위해 물리적인 머신에 종속되지 않고 가상 머신을 기반으로 동작하도록 설계되었다.

즉, JVM은 이러한 자바의 목표를 실현하기 위한 가상 머신이며 물리적인 하드웨어가 변경되어도 JVM 위에서 동작하기 때문에 코드를 변경하지 않아도 된다.

 

JVM의 다른 특성들은 여러 가지가 있다.

1. 스택 기반의 가상 머신 - 레지스터 기반이 아닌 스택을 기반으로 작동한다.

2. 심볼릭 레퍼런스 - 프리미티브 자료형을 제외한 모든 타입을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.

3. 가비지 컬렉션 - 사용자가 생성한 클래스 인스턴스를 자동으로 가비지 컬렉터가 삭제해준다.

4. 플랫폼 독립성 보장 - C/C++과 다르게 기본 자료형(프리미티브 자료형)을 명확하게 정의하여 호환성과 플랫폼 독립성을 보장한다.

5. 네트워크 바이트 오더 - 자바 클래스 파일은 플랫폼 독립성을 유지하기 위해서 고정된 바이트 오더인 네트워크 바이트 오더를 사용한다. 서로 다른 CPU끼리 데이터를 전송할 때 문제점이 발생할 수 있으므로 일종의 약속을 한 것이다. 네트워크 바이트 오더는 RISC 계열 아키텍쳐가 주로 사용하는 빅 엔디안이다.

 


컴파일 하는 방법, 실행하는 방법

코드를 실행하려면 내가 쓴 자바를 컴퓨터가 이해하고 그대로 실행을 해야한다.

그러나 컴퓨터는 자바라는 언어를 이해하지 못하기 때문에 컴퓨터가 이해할 수 있는 기계어로 변환해야 한다.

이 때 필요한 과정이 컴파일이다.

 

Main.java 라는 .java 파일의 코드를 작성했다고 가정해본다.

이 Main.java를 실행하기 위해서 Build를 하면 Java Compiler는 javac라는 명령어를 사용해서 Main.class파일을 생성한다.

Main.class는 아직 자바 바이트코드이기 때문에 컴퓨터가 이해하고 처리할 수 없다.

 

바이트 코드의 정의는

특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다.

바이트 코드라 불리는 이유는 자바 컴파일러가 변환했을 때 코드의 명령어 크기가 1Byte이기 때문이다.

특정 하드웨어가 아닌 가상 머신(JVM)이 이해할 수 있도록 변환한 코드라고 할 수 있다.

 

Main.class 파일은 JVM내에 있는 클래스 로더(Class Loader)에 의해 JVM으로 로드되고,

실행 엔진(Execution Engine)에 의해 Main.class 파일, 자바 바이트 코드를 실제로 실행할 수 있는 형태로 변경하여 Runtime Data 영역에 배치한다.

 

실행 엔진은 자바 바이트 코드를 실행하기 위해서 두 가지 방식을 사용한다. 인터프리터(Interpreter)와 JIT(Just-In-Time)이다.

 

- 인터프리터 방식은 Main.class파일을 명령어 단위로 읽어서 실행한다. 이러한 방식은 다른 언어들에 비해 훨씬 느린 속도를 보이기 때문에 이를 해결하기 위해 나온 것이 JIT 컴파일러이다.

 

- JIT 컴파일러

이름이 Just-In-Time인 것처럼 런타임(실행을 하면서 도중에)에 바이트 코드를 기계어로 변환시켜 주기 때문에 인터프리터에 비해 속도가 빠르다는 장점이 있다.

하지만 JIT 컴파일러가 변환하는 과정에서도 비용이 발생하므로 인터프리터 방식으로 실행하다가 적절한 시기에 JIT 컴파일러를 이용해 직접 실행한다. 또한 JIT 컴파일러는 캐시를 사용하기 때문에 자주 나오는 명령어는 빠르게 실행할 수 있다는 장점이 있다.

반대로 한 번만 실행하는 명령어는 JIT보다 인터프리터 방식이 더 유리하다.

 

실행 엔진의 동작 방식에 대한 명세는 따로 정해져 있지 않기 때문에 다양한 기법으로 실행 엔진을 이용한다.

대분의 JIT 컴파일러는

IR 변환 -> 최적화 -> 네이티브 코드 생성의 과정을 거친다.

IR(Intermediate Representation)은 바이트 코드의 중간 단계 표현이다.

 

정리하자면

.java 파일 -> 자바 컴파일러 -> .class(자바 바이트 코드) -> 클래스 로더(Class Loader) -> JVM 메모리에 로드 ->

실행 엔진(인터프리터, JIT 컴파일러)

순서로 자바 코드가 실행된다.

 


JVM의 구성요소

 

  • 클래스 로더(Class Loader)

  • 실행 엔진

  • 런타임 데이터 영역(Runtime Data Areas)

클래스 로더

클래스 로더는 자바의 동적 로드를 담당한다. 컴파일 타임이 아닌 런타임에 클래스를 로드하고 링크한다.

클래스 로더의 특징은 다음과 같다.

- 계층 구조 : 로더끼리 부모 - 자식 관계를 이루어 생성된다. 최상위 클래스 로더는 브트스트랩 클래스 로더이다.

- 위임 모델 : 계층 구조이기 때문에 상위 클래스의 로더를 이용할 수 있다. 로더가 클래스 로드를 요청받으면 먼저

상위 클래스 로더를 확인하여 해당 클래스를 찾는다. 상위 클래스에 있다면 사용하고 없다면 요청받은 클래스 로더가 로드를 한다.

- 가시성 제한: 클래스 로더를 참조하는 것은 하위 -> 상위는 가능하지만, 상위 -> 하위는 불가능하다.

- 언로드 불가: 클래스 로더는 클래스를 로드할 수 있지만 언로드를 할 수는 없다.

대신에 현재 클래스 로더를 삭제하고 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

 

클래스 로더의 로드를 더 자세하게 살펴보겠다.

클래스 로더가 클래스 로드를 요청 받는다.

클래스 로더 캐시 -> 상위 클래스 -> 최상위 클래스 -> 요청받은 자신 순서로 해당 클래스가 있는지 확인한다.

캐시와 최상위 클래스까지 없다면 요청받은 자신이 파일 시스템에서 해당 클래스를 찾는 것이다.

 

클래스 로더의 종류는 다음과 같다.

- 부트스트랩 클래스 로더 : JVM 기동할 때 생성, 최상위 클래스 로더, Object 클래스들과 자바 API들을 로드, 자바가 아닌 네이티브 코드(다른 언어)로 구현.

- 익스텐션 클래스 로더(Extension Class Loader) : 기본 자바 API를 제외한 확장 클래스들을 로드.

- 시스템 클래스 로더(System Class Loader) : 애플리케이션의 클래스들을 로드. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드

- 사용자 정의 클래스 로더(User-Defined Class Loader) : 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더

 

클래스 로드 단계

위의 과정을 거쳐서 드디어 클래스를 찾으면 클래스 로더는 찾은 클래스를 로드 -> 링크 -> 초기화 한다.

- 로드: 로드는 클래스를 파일에서 가져와서 JVM 메모리에 올리는 것이다.

- 검증: 찾은 클래스가 자바 언어 명세와 JVM명세에 맞게 잘 구성되어 있는지 검사하는 과정이다.

- 준비: 클래스가 필요로 하는 메모리를 할당하고 클래스에 정의되어 있는 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.

- 분석: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.

- 초기화: 클래스 변수들을 초기화 한다.

 

런타임 데이터 영역(Runtime Data Areas)

 

런타임 데이터 영역은 JVM이 OS에게 할당받은 메모리 영역이다. JVM도 소프트웨어이기 때문에 운영체제로 부터

메모리를 할당받아 사용하는 것이다.

런타임 데이터 영역은 6개의 영역으로 나뉜다.

 

1) PC 레지스터: PC(Program Counter) 레지스터는 스레드마다 하나씩 존재하며 현재 실행 중인 JVM 명령의 주소를 갖는다.

 

2) JVM 스택: 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임이라는 구조체를 저장하는 스택이다.

JVM 스택은 스택 프레임을 push, pop만 수행한다.

 

- 스택 프레임: JVM내에서 메서드가 수행될 때마다 생성 -> 스택에 push -> 종료 -> pop 과정을 거친다.

각 스택 프레임은 지역 변수 배열, 피연산자 스택, 런타임 상수 풀에 대한 레퍼런스를 갖는다. 컴파일 시에 스택 프레임의 크기는 메서드에 따라 고정된다.

- 지역 변수 배열: 0번에는 this, 이후에는 메서드 파라미터, 지역 변수 순서로 저장된다.

- 피연산자 스택: 메서드의 실제 작업 공간이다. 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드의 결과를 push, pop하면서 작업을 수행한다. 피연산자 스택도 컴파일 시에 공간이 고정된다.

 

3) 네이티브 메서드 스택: 자바 이외의 언어로 작성된 네이티브 코드를 위한 스택이다.

 

4) 메서드 영역: 모든 스레드가 공유하는 영역이다. JVM이 시작될 때 생성되며 클래스, 런타임 상수 풀, 필드, static 변수 등을 보관한다.

 

5) 런타임 상수 풀: 각 클래스와 인터페이스의 상수뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스도 담고 있는 테이블.

메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 거쳐야지만 실제 메모리 주소를 참조할 수 있다.

 

6) 힙: 인스턴스 또는 객체를 저장하는 공간으로 가비 컬렉션의 대상이다. 힙 구성 방식이나 가비지 컬렉션 방법은 JVM 벤더의 재량이고

JVM 성능 이슈에 가장 많이 언급되는 메모리 공간이다.


JDK와 JRE의 차이

JRE(Java Runtime Environment)는 자바 프로그램을 실행시킬 수 있는 환경이다.

JVM이 자바 프로그램을 동작시킬 때 필요한 파일들을 가지고 있다고 생각하면 된다.

즉, 자바 프로그램을 실행하기 위해서는 JVM 실행환경을 구현한 JRE가 반드시 필요하다.

 

JDK(Java Development kit)은 자바 프로그래밍을 위한 도구들이 들어있다고 생각하면 된다.

자바를 이용한 개발을 위해서 javac, java와 같은 도구들이 포함되며,

당연히 JDK를 설치하면 JRE가 같이 설치된다.

 

결론은 JDK = JRE + @라고 생각하면 된다.


마무리

JAVA의 기본적인 원리를 사실 대충 알고 있었다. 2학년 전공 선택으로 수강했었지만

이론적인 부분은 소홀히 하고 자바의 문법이나 실제 프로그래밍, 프로젝트에 더 집중했었던 것 같다.

생각해보니 이후에도 인턴 생활을 하면서 자바를 이용해 안드로이드 개발을 했었는데 정작 자바의 기본 작동 원리는 잘 몰랐다.

 

자료를 조사하면서 느낀 점은 결국 한 발자국을 더 안 갔었던 것 같다.

아는 이야기들도 있었지만 결국 전반적인 내용이었다. 자바 가상 머신이 어떻게 작동하는지, 

왜 자바는 여러 하드웨어에서도 동작할 수 있는지, 내가 작성한 코드의 변수들과 객체들은 어디에 저장되고, 어떻게 실행이 되는건지 등

당연하다고 사용해왔던 것들이 복잡한 과정을 거치고 있었다.

 

이것저것 많이 경험하는 것이 중요하다고 생각했었는데 물론 다양한 경험도 중요하지만

경험을 할 때 최소한의 깊이라는 것이 존재한다는 생각이 들었다. 항상 기본을 잘 다지고 새로운 기술에 접근해야겠다.

반응형

관련글 더보기