자바의 구동원리와 JVM

mallin·2022년 2월 3일
1

JAVA

목록 보기
1/6
post-thumbnail

자바 코드는 어떻게 동작될까 ?
아래와 같은 순서에 따라서 실행된다.

① 소스 코드 (.java 확장자) 를 작성한다.
② 컴파일러 (Javac.exe) 가 자바 소스 코드(.java) 를 바이트 코드(.class) 로 변환한다.
③ 런처 (java.exe) 로 자바 가상 머신을 구동시킨다.
④ 자바 가상 머신 (JVM) 이 바이트 코드를 해석하여 자바 프로그램을 실행한다.

.java 파일이란 ❓

  • java 규칙에 맞게 작성한 모든 소스코드 파일
  • 사람이 읽을 수 있는 text 로 구성

.class 파일이란 ❓

  • 컴파일러에 의해 생성된 bytecode 로 구성된 파일
  • JVM 을 위한 코드
  • 자바를 실행할 수 있는 모든 장치에서 실행 가능

위 과정 중 자바 가상 머신 (JVM) 이 동작하는 과정 및 내부 로직에 대해서 알아보자

자바 가상머신 (JVM) 이란?

본격적으로 알아보기 전에 JVM 이 만들어진 이유와 알아야 하는 이유에 대해서 먼저 살펴보고 넘어가자.

JVM 이 만들어진 이유

일반 프로그램의 경우에는 하드웨어 > 운영체제 > 일반 프로그램 의 구조로 운영체제가 프로그램을 실행시킨다. 그렇기 때문에 컴파일 플랫폼과 타겟 플랫폼이 다른 경우 프로그램이 동작하지 않는다.
EX) 리눅스에서 컴파일 된 프로그램은 윈도우 에서는 정상적으로 동작하지 않는다.

이런 경우 타겟 플랫폼에 맞춰 컴파일 하는 크로스 컴파일로 해당 문제를 해결했지만 자바의 경우에는 하드웨어 > 운영체제 > JVM > 자바 프로그램 의 구조로 한 번 만들기만 하면 어느 운영체제에서 실행할 수 있도록 했다.

JVM 을 알아야 하는 이유

JVM 을 알아야 하는 이유로는 성능이 있는데
동일한 기능의 프로그램이라고 해도 메모리 관리가 되지 않으면 속도 저하 현상이나 튕김 현상이 일어날 수 밖에 없다. 자바 가상 머신의 구성과 메모리를 알면 관리하는 것에 따라 프로그램의 성능이 달라진다.

JVM 의 구조

그러면 JVM 은 어떤 구조를 가지고 있을까 ?

큰 단위로 봤을 때 Class Loader / Execution / Rumtime Data Area 로 나눌 수 있다.

간단하게 설명했을 때

클래스 로더가 클래스 파일을 동적으로 로드 해 런타임 데이터 영역에 배치하면
👉 런타임 데이터 영역은 파일들을 각각의 위치에 저장하고
👉 실행엔진이 런타임 데이터 영역에 배치되어 있는 바이트 코드를 해석하여 실행한다.

클래스 로더 (Class Loader)

클래스 로더는 .class 파일을 동적으로 로드하고, 해당 바이트 코드들을 JVM 의 메모리 영역인 Runtime Data Areas 에 배치한다.

동적으로 로드한다는 건 한번에 메모리에 올리지 않고, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재한다는 뜻이다.

클래스 로더의 로딩 순서는 Loading -> Linking -> Initialization 순서로 이뤄진다.

1. Loading

.class 파일을 읽고 그 코드에 따라 적절한 바이너리 데이터를 만든 다음 메소드 영역에 저장한다.
내부적으로는 Bootstrap -> Extension -> Application 로 동작한다.

① Bootstrap 는 최상위 클래스 로더다.

  • JVM 시작할 때 가장 최초로 실행되며, 모든 클래스 로더의 부모
  • JAVA_HOME/jre/lib 디렉토리에 존재하는 핵심 java API 를 로드
  • 최소한의 자바 클래스 로더 (java.lang.Object, Class, Classloader)

② Extension 는 확장 클래스 로더다.

  • 당연하게 Bootstrap 클래스 로더를 부모로 갖는 클래스 로더
  • JAVA_HOME/jre/lib/ext 혹은 java.ext.dirs 시스템 속성으로 명시된 다른 특정 디렉토리인 확장 디렉토리 안에 존재하는 클래스들을 로드

③ Application 는 어플리케이션 레벨 클래스 로더다.

  • Extension 클래스 로더의 자식이고,
  • 모든 어플리케이션 레벨의 클래스 로드
  • 우리가 작성한 클래스는 이 Application 클래스 로더에 의해 로드된다.

로딩의 동작 원리는 다음과 같다.
1. 클래스를 읽어달라고 요청이 들어오면
2. 제일 부모인 Bootstrap 클래스 로더에게 클래스를 읽어달라고 요청을 하고
3. 만약 못 읽은 경우 다음 로더인 Extension 클래스 로더에게 읽어달라고 요청을 하고
4. 못 읽은 경우 Application 클래스 로더가 읽어 온다.
5. 만약 위의 모든 경우에도 못 읽으면 ClassNotFoundException 이 발생한다.

2. Linking

클래스 파일을 사용하기 위해서 검증하는 과정이다.
내부적으로 Verify > Prepare > Resolver 로 동작한다.

① Verify 는 유효성을 확인한다.

  • 클래스(.class) 파일의 정확성을 보장하기 위한 단계
  • 읽은 클래스의 바이너리 데이터가 유효한 것인지 확인

② Prepare 은 메모리를 준비한다.

  • 클래스의 static 변수와 기본값에 필요한 메모리 공간을 준비
  • 클래스 및 인터페이스의 static 필드를 생성하고 기본값으로 초기화

③ Resolve 는 심볼릭 메모리 레퍼런스를 교체한다.

  • 심볼릭 메모리 레퍼런스를 메모리 영역에 존재하는 실제 레퍼런스로 교체
    EX) Test test = new Test() 에서 new Test() 부분은 실제 레퍼런스를 가리키지 않음. 그렇기 때문에 실제 힙에 들어있는 인스턴스를 가리키는 작업을 Resolve 시점에 해준다.

3. intialization

링크 단계의 preapre 에서 확보한 메모리 영역에 클래스의 static 값들을 할당한다.


Runtime Data Area

Runtime Data Area 는 쉽게 메모리 영역이라고 생각하면 된다.
즉, 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.

위 그림처럼 크게 Method / Heap / PC Register / Stack / Native Method Stack 으로 나눌 수 있다.

여기서 Method Area 와 Heap Area 는 모든 스레드에서 공유하고,
PC Register 와 Stack Area 그리고 Native Method Stack 은 각 스레드 별로 생성한다.

1. Method Area

사용기간스레드 공유
JVM 시작시 생성 ~ 프로그램 종료시모든 스레드 공유

바이트 코드 를 처음 메모리 공간에 올릴 때 초기화 되는 대상을 저장하기 위한 공간이며, 클래스 수준의 정보를 저장한다.

저장되는 정보
클래스 로더에 의해 로딩된

  • 클래스
  • 메서드
  • 클래스 변수 (static)
  • 전역 변수
    가 저장된다.

Runtime Constant Pool
메서드 영역에 존재하는 별도의 관리영역으로 말그대로 '상수' 정보가 저장되는 공간이다.

  • 각 클래스, 인터페이스 마다 별도의 constrant pool 테이블이 존재하는데, 클래스 생성 시 참조해야할 정보들을 상수로 가지고 있는 영역
  • JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조

➡️ 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 수행한다.

2. Heap Area

사용기간스레드 공유
JVM 시작시 생성 ~ 프로그램 종료시모든 스레드 공유

데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다.

저장되는 정보

  • 런타임 시 결정되는 참조형 데이터타입(Reference Type)
  • new 연산자를 통해 생성된 객체

가 저장된다.

참조하는 변수나 필드가 없다면 의미 없는 객체가 되어 가비지 컬렉터 (GC) 의 대상이 되어 회수된다.

➡️ Eden, Survivor1, Survivor2, Old, Permanent 로 이루어져 있다.

3. Java Stack Area

사용기간스레드 공유
{} 나 메소드가 끝났을 때까지각 스레드별 생성

임시로 사용되는 데이터들을 저장하기 위한 영역으로 마지막 들어온 값이 먼저 나가는 LIFO 의 구조로 되어 있다.

저장되는 정보

  • 컴파일 시 결정되는 int, long, boolean 등 기본형 데이터타입
  • 지역변수, 매개변수, 리턴값, 참조변수

동작 방식
메소드 호출
-> 각각의 스택 프레임 (메소드 만의 공간) 생성
-> 메서드 안에서 사용되는 값들을 저장
-> 호출된 메서드의 매개변수, 지역 변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장
-> 메소드 수행이 끝나면 프레임별 삭제

4. PC Register

사용기간스레드 공유
스레드 시작시 생성각 스레드 별 생성

현재 수행중인 JVM 명령어 주소를 저장하는 공간이다.

자바는 OS 나 CPU 입장에선 하나의 프로세스 이기 때문에 가상 머신(JVM) 의 리소스를 이용해야 한다. 그렇기 때문에 자바는 CPU 에 직접 연산을 수행하도록 하는게 아니라 현재 작업하는 내용을 CPU 에 연산으로 제공 해야 하며, 이를 위한 버퍼 공간 으로 PC Register 라는 메모리 영역을 가지고 있다.

만약 스레드가 자바 메소드를 수행ㅇ하고 있으면 JVM 명령의 주소를 PC Register 에 저장한다.

5. Native Method Stack

사용기간스레드 공유
native interface 호출시 생성, native interface 종료시 생성각 스레드 별 생성

바이트 코드가 아니라 실제 기계어로 작성된 프로그램을 실행시키는 영역이다.
또한, 자바 이외의 언어로 작성된 네이티브 코드를 실행시키기 위한 메모리 영역이기도 하다.

일반적으로 메소드를 실행할 때에는 JVM 스택에 쌓이다가 해당 메소드 내부에 네이티브 방식을 사용하는 메소드가 있으면 해당 메소드는 네이티브 스택에 쌓이고 수행이 끝나면 다시 자바 스택으로 돌아와서 작업을 수행한다.


실행 엔진 (Execution)

실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.

이때 인터프리터와 JIT 컴파일러 방식을 혼합해서 바이트 코드를 실행한다.

1. 인터프리터

자바 바이트 코드를 명령어 단위로 하나하나씩 읽어서 바로 해석하고 실행한다.
JVM 안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
반복되는 코드인 경우에도 무조건 실행하기 때문에 속도가 느릴 수 있다는 단점이 있는데 그건 JIT 컴파일러가 해결해준다.

2. JIT (Just-In Time) 컴파일러

인터프리터 방식으로 바이트 코드를 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 네이티브 코드로 직접 실행하는 방식

컴파일 된 코드를 수행하기 때문에 전체적인 속도가 빠르다. 하지만, 네이티브 코드로 변경하는데에 훨씬 많은 비용이 소요되기 때문에 한번만 실행되는 코드라면 인터프리터 방식을 사용하는게 더 효율적이다.

🙇🏻‍♀️ 레퍼런스

0개의 댓글