JVM 아키텍처 - 1

이주오·2022년 2월 3일
1

java

목록 보기
4/6

JVM이란 무엇인가

JVM은 Java virtual machine을 줄인 것으로 자바를 실행하기 위한 가상 컴퓨터이다.

자바로 작성된 애플리케이션은 모두 JVM에서만 실행되기 때문에, 자바 애플리케이션을 실행하기 위해서는 JVM이 반드시 필요하다. 컴파일러는 Java 파일을 .class 파일로 컴파일한 다음 해당 .class 파일을 JVM에 입력하여 클래스 파일을 로드하고 실행한다.

WORA(Write once, run anywhere)

이 과정을 통하여 Java 는 높은 이식성이라는 큰 장점을 얻을 수가 있었다. 어느 기기나 운영체제에 상관없이 JVM 이 설치 및 구동될 수 있는 환경이라면 Java 로 작성한 프로그램은 실행이 가능하기 때문에 다른 운영체제에 맞춰서 컴파일을 해줘야 하는 다른 언어보다 높은 이식성을 가질 수 있게 되었다.

단, JVM은 OS에 종속적이기 때문에 해당 OS에서 실행가능한 JVM이 필요하다.

JVM은 바이트 코드를 이해하는 것이지 자바 코드를 이해하는 것이 아니다. 코틀린 또한 코틀린 코드를 바이트 코드로 컴파일해서 JVM 위에서 동작한다.

JVM의 특성

  • 스택 기반의 가상 머신
  • 단일 상속 형태의 객체 지향 프로그래밍을 가상 머신 수준에서 구현
  • 포인터를 지원. 단, C와 같이 주소 값을 임의로 조작이 가능한 포인터 연산은 불가능
  • Garbage collection 수행
  • 플랫폼의 독립성 보장
  • Data Flow Analysis에 기반한 자바 바이트코드 검증기를 통해 문제를 실행 전에 검증하여 실행 시 안전을 보장하고 별도의 부담을 줄여줌

바이트코드란 무엇인가

바이너리 코드

CPU가 이해하기 위한 기계어는 0과 1로 구성된 바이너리 코드(이진 코드)이다. 기계어가 이진 코드로 이루어졌을 뿐 모든 이진 코드가 기계어인 것은 아니다.

바이너리 코드 != 기계어

바이트 코드

0과 1로 이루어진 이진 코드이지만 바이너리 코드와 달리 가상머신이 이해할 수 있는 코드이다. 사람에게 친숙한 고급 언어보다는 덜 추상적이지만 기계어보다는 추상적이다.

고급언어로 작성된 코드를 가상머신이 이해할 수 있도록 컴파일한 것이다. CPU에게 넘어가기 전에 실시간 번역기 또는 JIT(just-in-time) 컴파일러에 의해 바이너리 코드로 변환된다.

정리

Java는 OS와 직접적으로 대화할 수 없다. 오로지 JVM하고만 상호작용을 한다. 자바는 JVM을 거쳐야만 OS와 대화할 수 있다.

바이너리 코드와 바이트 코드 둘 다 0과 1로 이루어져 있다. 바이너리 코드는 CPU가 이해할 수 있는 언어, 바이트 코드는 가상 머신이 이해할 수 있는 언어이다.

그 중에 JVM을 위한 바이트 코드를 “자바 바이트코드”라고 한다.


JVM 구성 요소

JVM은 크게 세 가지 구성요소로 볼 수 있다.

  • Class Loader
  • Runtime Data Area
  • Execution Engine


출처: https://dzone.com/articles/jvm-architecture-explained

Class Loader

JDK 에서 개발하고, JRE 를 통해서 환경을 제공받은 JVM 은 compile 된 바이트 코드를 탑재하여 로직을 실행하게 됩니다. 그렇다면 JVM 에 Class 는 어떻게 로드되는 것일까??

바로 그 역할을 하는 것이 자바 클래스로더이다. 클래스 로더는 자바 클래스를 JVM으로 동적 로드하는 JRE(자바 런타임 환경)의 일부이다. 클래스 파일을 로드하는데 사용되는 하위 시스템이다.

Compile time이 아닌, Runtime시 처음으로 한 번만 동적으로 클래스를 로드하며, jar 파일 내에 저장된 클래스들을 JVM 위에 탑재하고 사용하지 않는 클래스들은 메모리에서 삭제한다.변환된 바이트 코드 파일(.class)을 JVM이 운영체제로부터 할당 받은 메모리 영역인 Runtime Data Area로 “적재”하는 역할을 한다.

ClassLoader 는 클래스 파일을 찾아서 탑재하는 역할뿐만이 아니라 jvm 에 관련된 다른 일들도 같이 한다.

크게 LoadingLinking, 그리고 Initialization 3가지 역할을 맡게 된다.

  • Loading 은 클래스 파일을 탑재하는 과정
  • Linking 은 클래스 파일을 사용하기 위해 검증하고, 기본 값으로 초기화하는 과정
  • Initialization 은 static field 의 값들을 정의한 값으로 초기화를 하는 과정

Runtime Data Area

이렇게 탑재하는 클래스 파일들은 JVM 에서 어떤 영역을 차지하고 있을까? JVM 의 Run-Time Data Area는 프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간이며, 크게 Method Area , Heap , Java Stacks , PC registers 그리고 Native Method Stacks 가 존재한다.

출처: https://tecoble.techcourse.co.kr/post/2021-08-09-jvm-memory/

Method Area

  • Method Area 에는 인스턴스 생성을 위한 객체 구조, 생성자, 필드 등이 저장된다. Runtime Constant Pool 과 static 변수, 그리고 메소드 데이터와 같은 Class 데이터들도 이곳에서 관리 된다.
  • 즉, 정적 변수를 포함하여 모든 클래스 수준 데이터가 여기에 저장된다.
  • JVM당 하나의 메소드 영역만 있으며 공유 자원입니다.다른 스레드에서도 활용 가능한 공유자원이다.
  • 다중 스레드에 대한 메모리를 공유하므로 저장된 데이터는 스레드에 안전하지 않다

Heap

  • 모든 객체와 해당 인스턴스(instance) 변수 및 배열, String pool이 여기에 저장됩니다.
  • JVM 당 역시 하나만 생성이 되고, 해당 영역이 가진 데이터는 모든 Java Stack 영역에서 참조되어, Thread 간 공유가 됩니다.
  • 다중 스레드에 대한 메모리를 공유하므로 저장된 데이터는 스레드에 안전하지 않다
  • GC의 주 대상

Native Method Stack

  • 순수하게 Java 로 구성된 코드만을 사용할 수 없는 시스템의 자원이나 API 가 존재합니다.
  • 다른 프로그래밍 언어로 작성된 메소드들을 Native Method 라고 합니다.
  • 일반적인 메소드를 실행하는 경우 JVM Language Stack에 적재되지만, 네이티브 메소드 스택은 네이티브 라이브러리에 따라 네이티브 코드 명령(C언어와 같이 네이티브 방식으로 작성된 메소드)을 보관한다.

PC Register

  • Java 에서 Thread 는 각자의 메소드를 실행하게 됩니다. 이때, Thread 별로 동시에 실행하는 환경이 보장되어야 하므로 최근에 실행 중인 JVM 에서는 명령어 주소값을 저장할 공간이 필요합니다.
  • 이 부분을 PC Registers 영역이 관리하여 추적해주게 됩니다. Thread 들은 각각 자신만의 PC Registers 를 가지고 있습니다.
  • 만약 실행했던 메소드가 네이티브하다면 undefined 가 기록이 됩니다. 실행했던 메소드가 네이티브하지 않다면, PC Registers 는 JVM 에서 사용된 명령의 주소 값을 저장하게 됩니다.

Stack

  • 프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위한 영역이다.
  • 각종 형태의 변수나 임시 데이터, 스레드나 메소드의 정보를 저장하고 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산 시 일어나는 값들을 임시로 저장한다.
  • 각 스레드에는 자체 JVM 스택이 있고, 스레드가 생성될 때 동시에 생성된다.
  • 각 Thread 별로 따로 할당되는 영역이므로 Heap 메모리 영역보다 비교적 빠르다는 장점이 있다. 또한, 각각의 Thread 별로 메모리를 따로 할당하기 때문에 동시성 문제에서 자유롭다.

Execution Engine

런타임 데이터 영역에 할당된 바이트코드는 실행 엔진에 의해 실행된다. Execution Engine은 바이트코드를 읽고 자바 바이트 코드를 JVM 내부에서 컴퓨터가 실행할 수 있는 형태인 바이너리 코드로 변경하며 하나씩 실행한다.

변경하는 방식은 두가지가 있는데, 인터프리터 방식과 JIT 방식이 있다.

인터프리터 방식

  • 기본 바이트 코드를 실행하는 방법은 인터프리터 방식이 기본이다. 자바 바이트 코드를 명령어 단위로 읽어서 실행하기 때문에 느리다.

JIT(just-in-time) Compiler

  • JIT 컴파일러는 인터프리터의 단점을 해결한다. 실행 엔진은 바이트 코드를 변환하는 데 인터프리터의 도움을 사용할 것이지만 반복되는 코드를 발견하면 전체 바이트코드를 컴파일하여 네이티브 코드로 변경하는 JIT 컴파일러를 사용한다. 이 네이티브 코드는 반복 메서드 호출에 직접 사용되어 시스템 성능을 향상시킨다.

Garbage Collector

  • 참조되지 않은 객체를 수집하고 제거한다. JVM의 가비지 컬렉션은 생성된 객체를 수집한다.

JVM 동작 간단 정리

JVM 구성 요소는 다음과 같다.

  1. 클래스 로더 컴파일러가 내부에 만든 .class(바이트 코드)를 런타임 데이터 공간에 “적재”한다.
  2. 런타임 데이터 공간 OS로부터 메모리를 할당받은 공간으로 스택, 힙, 메소드, 네이티브 메소드, PC 레지스터가 있다.
  3. 실행 엔진인터프리터 방식 또는 JIT 컴파일러를 이용하여 데이터 영역에 배치된 바이트 코드를 실행한다.
  • JIT 컴파일러는 바이트 코드를 바이너리 코드로 변환하는 속도가 느린 인터프리터 방식을 보완하기 위해 나온 것이다.
  • 인터프리터 방식으로 기계어 코드를 생성하면서 그 코드를 캐싱하여, 같은 함수가 여러 번 불릴 때 매번 기계어 코드를 생성하는 것을 방지한다.
  • JVM 내부에서는 자바 컴파일러가 자바 프로그램 코드를 바이트 코드로 변환시킨 후 실제 바이트 코드가 실행하는 시점에서 JIT 컴파일러를 통해 기계어로 변환한다.
  1. GC는 JVM 상에서 더 이상 사용되지 않는 데이터가 할당되어있는 메모리를 해제시킨다.

컴파일 하는 방법

컴파일이란?

컴파일러는 특정 프로그래밍 언어로 쓰여 있는 문서를 다른 프로그래밍 언어로 옮기는 프로그램을 말한다. 기존 문서를 소스 코드 혹은 원시 코드라고 부르고, 출력된 문서를 목적 코드라고 부른다.원시 코드에서 목적 코드로 옮기는 과정을 컴파일이라고 한다.

자바 컴파일 과정

  1. 소스 파일 생성한다. (Hello.java)
  2. 자바 컴파일러(javac.exe)를 사용하여 컴파일한다. $ javac Hello.java
  3. 컴파일이 정상적으로 완료되면 클래스 파일 생성된다. (Hello.class)

실행하는 방법

자바 인터프리터(java.exe)로 실행한다. $ java Hello실행 시에는 확장자를 붙이지 않는다.

내부적인 진행순서는 다음과 같다.

  1. 프로그램의 실행에 필요한 클래스(*.class파일)을 로드한다.
  2. 클래스파일을 검사한다.(파일형식, 악성코드 체크)
  3. 지정된 클래스(Hello)에서 main(String[] args)을 호출한다.

JDK와 JRE의 차이

JDK란?

JDK는 Java Development Kit으로 자바 프로그래밍 시 필요한 컴파일러 등을 포함한다. JDK는 JRE를 포함하며, 개발을 위해 필요한 도구(java, javac 등)를 포함한다.

JRE란?

JRE는 Java Runtime Enviroment로 컴파일된 자바 프로그램을 실행시킬 수 있는 자바 환경을 말한다. JRE는 JVM의 실행환경을 구현했다고 볼 수 있으며, JVM이 자바 프로그램을 동작시킬 때 필요한 라이브러리 파일들과 기타 파일들을 가지고 있다.


javac 옵션

더 자세한 javac의 standard options은 공식 문서에서 볼 수 있다.

$ javac <options> <source files>

-cp path or -classpath path

  • 컴파일러가 참조할 클래스 파일들을 찾기 위해서 컴파일 시 파일경로를 지정해주는 옵션이다.해당 옵션을 쓰지 않는 경우(classpath가 지정되지 않는 경우) 사용자 클래스 경로는 현재 디렉터리가 된다.

-directory

  • 클래스 파일의 대상 디렉터리를 설정한다. javac가 별도의 디렉터리를 만들지 않기 때문에 디렉터리는 미리 만들어둬야 한다.

-deprecation

  • 사용되지 않는 멤버 또는 클래스의 사용 또는 오버라이드에 대한 설명을 표시한다.해당 옵션이 없는 javac는 사용되지 않는 멤버나 클래스를 사용하거나 재정의하는 소스 파일의 요약을 보여준다.

-g

  • 로컬 변수를 포함한 모든 디버깅 정보를 생성한다.g:none : 디버깅 정보를 생성하지 않는다.g:{source, lines, vars} : 소스파일 정보, 라인 정보, 지역변수의 디버깅 정보를 생성한다.

-source release

  • 소스 코드의 버전을 지정한다.

-target version

  • 가상 시스템의 지정된 릴리스를 대상으로 하는 클래스 파일을 생성한다. 클래스 파일은 지정된 대상 및 이후 릴리스에서 실행되지만 이전 릴리스의 JVM에서는 실행되지 않는다.

참고 출처

profile
동료들이 같이 일하고 싶어하는 백엔드 개발자가 되고자 합니다!

0개의 댓글