JVM

2021. 7. 7. 19:18Java

JVM이 왜 등장했을까

Java는 네트워크에 연결된 모든 디바이스에서 작동하는 것이 목적이었다. 즉, 디바이스마다 운영체제와 하드웨어가 다르기 때문에 플랫폼에 의존적이지 않는 언어를 만들고 싶었다고 한다. 그래서 Java 바이트 코드를 실행시킬 수 있는 JVM이 등장했다.

주요 용어

  • 환경, 플랫폼 = 운영체제 + CPU 
    ex) 내 컴퓨터의 운영체제는 Linux야. CPU는 AMD 라이젠.
  • 컴파일 플랫폼 : 프로그램을 만들고 있는 컴퓨터 (개발자 컴퓨터)
  • 타겟 플랫폼 : 프로그램을 사용할 컴퓨터 (사용자 컴퓨터)

환경이 다를 경우 Java와 C/C++ 대응방식

  • C/C++ : 크로스컴파일; 타겟 플랫폼에 맞춰 컴파일

Java : 자바 바이트 코드플랫폼에 상관없이 / 플랫폼 독립적으로 JVM 위에서 동작
주의! JVM은 타겟 플랫폼에 의존한다

Java를 사용해서 프로그램을 작성하려면 JDK를 설치해야 한다.
여기서 JDK는 운영체제별 종류가 다르다. JDK안에는 JRE, API, JVM이 포함되어 있다.
운영체제마다 다른 JVM이 제공되기 때문에 독립적일 수 있다.

 

JVM은 무엇일까

JVM(자바가상머신)은 Java의 바이트 코드를 운영체제가 이해할 수 있는 기계어(명령어)로 해석한다. JVM은 Main method가 없으면 실행되지 않는다.

  1. Java로 작성한 소스코드(.java)가 컴파일
  2. Java 바이트코드(.class)파일 생성
  3. 바이트 코드는 JVM을 거쳐서 기계어로 변환
.java 파일 :  java 규칙에 맞게 작성한, 사람이 읽을 수 있는 소스코드 파일이다
.class 파일 :  컴파일러에 의해 생성된 java bytecode로 구성된 파일이다.
컴파일러 : .java파일에 오류가 있는지 검사하고 JVM을 위한 Java 바이트 코드를 생성한다.

Java 소스코드가 실행되기까지

  • Java Compiler
  • JIT Compiler
  • Java Virtual Machine(JVM)

 

JVM 구조

  • class loader subsystem
  • runtime data area
  • execution engine
  • native method interface - JNI
  • native method libraries

1. Class Loader Subsystem

클래스 로더(Class Loader)

  • Runtime 시에 .jar 파일 내 저장된 클래스 파일(Java 바이트코드)을 JVM에 로드하고 사용하지 않는 Class는 메모리에서 삭제한다.
  • 자바는 동적 로딩(Dynamic Loading) 을 지원한다. Class Loader는 컴파일 타임에 모든 클래스가 로딩되지 않고 필요한 시점에 (런타임 도중) 해당 클래스를 실행하고 로딩한다.

Java 8 기준 ClassLoader 계층구조

1) Bootstrap ClassLoader

  • 최상위 클래스
  • <JAVA_HOME>/jre/lib에 담긴 자바 라이브러리를 로딩한다.
  • Native C로 구현됨.

2) Extension ClassLoader

  • <JAVA_HOME>/jre/lib/ext 폴더에 담긴 자바의 확장 클래스파일을 로딩한다.
  • Java로 구현됨.

3) Application ClassLoader

  • -classpath(또는 -cp)폴더에 있는 클래스를 로딩한다.
  • Java로 구현됨.
  • 개발자가 직접 작성한 코드를 로딩한다.

위의 클래스들은 아래 3가지 원칙에 따라 동작한다.

  • Delegation Principle
    클래스 로더는 상위 클래스 로더로 로딩요청을 위임한다. 최상위 클래스로더, 부트스트랩 로더에서부터 로딩요청을 수행하고 하위 클래스로더로 요청을 넘긴다.
  • Visibility Principle
    범위 규칙을 적용한다. 하위 클래스로더는 상위 클래스로더의 클래스를 찾을 수 있다. 하지만 상위클래스 로더는 하위클래스 로더가 로딩한 클래스를 사용할 수 없다.
  • Uniqueness Principle
    하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 다시 로딩하지 않는다.

2. Runtime Data Area ⭐ 

JVM 이 Java 바이트코드를 실행하기 위해 운영체제로부터 할당받은 메모리 공간이다. Thread끼리 공유하는 영역인지 아닌지 여부에 따라 Runtime Data Area에 있는 주요 구성요소를 정리해보았다.

2-1 Thread가 공유하지 않는 영역

1) PC Register

  • 스레드가 시작될 때 함께 생성되는 공간이다. 스레드마다 1개씩 있다.
  • 스레드가 어떤 부분을, 어떤 명령으로 실행해야 할 지 기록한다.
  • JVM은 Stacks-Base 방식으로 작동한다. 즉, JVM은 CPU에 직접 Instruction을 수행하지 않고 Stack에서 Operand를 뽑아내 이를 별도의 메모리 공간, PC Register에 저장한다

2) JVM Stack

  • Thread 제어를 위해 사용되는 메모리 영역이다. 스레드가 생성될 때마다 1개씩 생성된다.
  • 메소드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame(Stack Frame)을 저장하는 공간
  • 메소드 정보, 지역변수, 매개변수, 연산 중 발생하는 임시 데이터 저장하는 공간
  • 기본(원시)타입 변수는 스택 영역에 직접 값을 가진다. 참조타입 변수는 힙 영역이나 메소드 영역의 객체 주소를 가진다.
Method 가 호출 되면 Method와 Method 정보는 Stack에 쌓이게 된다.
Method 호출이 종료 될때 Stack point에서 제거 된다. Method 정보는 해당 Method의 매개변수, 지역변수, 임시변수,메소드 호출 한 주소 등을 저장하고 Method 종료시 메모리 공간이 사라진다.

멀티 Thread 프로그램의 경우 각 Thread가 자신의 Stack을 가지고는 있지만 Heap 영역은 공유하기 때문에, 프로그래밍시에 Thread-safe 하지 않는 이슈에 주의하며 프로그래밍을 해야 한다.

3) Native Method Stack

  • Java가 아닌 다른 언어로 작성된 코드(C, C++ 등)에서 제공되는 Method 정보를 저장하기 위한 Stack
  • JNI(Java Native Interface)가 해당 코드를 바이트코드로 변환하여 저장한다.

2-2 Thread가 공유하는 영역

4) Method Area/Class Area/Static Area

JVM이 읽어들인 클래스와 인터페이스에 대한 Runtime Constant Pool, 멤버 변수, 클래스 변수(Static 변수), 생성자와 메소드를 저장하는 공간이다.

  • 프로그램 실행 중 클래스가 사용되면, JVM은 클래스 파일(Java 바이트 코드)을 읽어서 분석하여 클래스 변수, 클래스의 인스턴스 변수, 메소드 코드 등을 Method Area에 저장한다.
프로그램이 실행되면 모든 코드가 저장되어 있는 상태가 아니다. new 키워드를 통해 객체가 동적으로 생성되기 이전에는 텍스트 일 뿐이다. 객체 생성 후에 메소드를 실행하게 되면 해당 클래스 코드에 대한 정보를 Method Area에 저장 하게 된다. 
  • 클래스 변수(Static 변수)는 Method Area의 Class Variable에 저장한다.
기본형이 아닌 static 클래스형 변수는 레퍼런스 변수만 저장되고 실제 인스턴스는 Heap에 저장되어 있다. 그 후 이 인스턴스의 변수를 저장하기 위해 Heap에 메모리가 확보가 되고 Heap의 인스턴스가 Method Area의 어느 클래스 정보와 연결되는지 설정 하게 된다.

Method Area/Class Area/Static Area에 저장되는 정보

(a) Type Information

클래스와 인터페이스를 Type라고 본다.

Type의 전체 이름 (패키지명 + 클래스명)
Type의 직계 하위 클래스 전체 이름
Type의 클래스 / 인터페이스 여부
Type의 modifier (public / abstract / final)
연관된 인터페이스 이름 리스트

(b) Runtime Constant Pool  

  • Method Area 에 있는 독자적인 영역
  • 클래스 파일 constant_pool 테이블에 해당하는 영역
  • 상수 자료형을 저장하여 참조하고 중복을 막기 위한 공간
Type, Field, Method로의 모든 Symbolic Reference 정보를 포함
클래스와 인터페이스 상수, 메소드와 필드에 대한 모든 레퍼런스를 저장한다
Constant Pool의 Entry는 배열과 같이 인덱스 번호를 통해 접근
Object의 접근 등 모든 참조를 위한 핵심 요소

(c) Field information : 인스턴스 변수

Field Type
Field modifier (public / private / protected / static / final / volatile / transient)

(d) Method Information : Constructor를 포함한 모든 메소드

Method Name
Method Return Type
Method Parameter 수와 Type
Method modifier (public / private / protected / static / final / syncronized / native / abstract)
Method 구현 부분이 있을 경우 ( abstract 또는 native 가 아닐 경우)
Method의 byteCode
Method의 Stack Frame의 Operand Stack 및 Local variable section의 크기
Exception Table

(e) Class Variable : static 키워드로 선언된 변수

  • 모든 인스턴스에 공유 되며 인스턴스가 없어도 직접 접근이 가능하다. 이 변수는 인스턴스의 것이 아니라 클래스에 속하게 된다.
  • 클래스를 사용 하기 이전에 이 변수들은 미리 메모리를 할당 받아 있는 상태가 된다.
  • final class 변수는 상수로 치환 되어 Runtime Constant Pool에 값을 복사한다.

5) Heap

  • 객체를 저장하는 가상 메모리 공간 (Method Area 는 클래스를 위한 공간)
  • new 연산자로 생성된 객체와 배열을 저장한다. 이때, Class Area에 올라온 클래스들만 객체로 생성할 수 있다.
  • 힙 영역에 생성된 객체와 배열은 스택 영역의 변수나 다른 객체의 필드에서 참조한다. 참조하는 변수나 필드가 없다면 의미 없는 객체가 되어 GC의 대상이 된다.

(a) Young Generation

  • Eden : 객체들이 최초로 생성되는 공간
  • Survivor 0 / 1 : Eden 에 의해 참조되는 객체들이 저장되는 공간

(b) Old Generation

접근 불가능 상태로 되지 않아 Young 영역에서 살아남은 객체들이 저장되는 공간

(c) Permanent Generation / Method Area

  • 생성된 객체들의 주소값이 저장된 공간
  • Class Loader 에 의해 로드되는 Class, Method 등에 대한 메타 데이터가 저장되는 영역. (해당 정보는 JVM에 의해 사용된다)
  • Reflection을 사용해 동적으로 클래스가 로딩 되는 경우에 사용
    → 내부적으로 Reflection 을 자주 사용하는 Spring Framework를 사용한다면 이 영역에 대한 고려가 필요

❗❕ Java 8부터는 Permanent 영역대신 Metaspcae 로 호출되는 네이티브 영역에 저장됩니다.

 

3. Execution Engine

Class Loader가 JVM에 로드한 클래스 파일(Java 바이트 코드)를 실행한다. Execution Engine을 통해서 Machine이 읽을 수 있는 형태로 ByteCode를 변환한다.

Execution Engine 구성 요소

1) Interpreter

  • Execution Engine은 Java 바이트 코드를 한 줄 씩(명령어 단위로) 해석해서 실행한다.
  • 한 줄 씩 읽어서 속도가 느리다는 단점이 있다.

2) JIT(Just - In - Time) 컴파일러 / 동적 번역(Dynamic Translation)

  • 인터프리터 방식의 단점을 보완하기 위해 도입
  • Java 바이트코드를 실행(이하 인터프리팅)하는 시점에 JIT 컴파일러는 Java 바이트코드를 컴파일하면서 변환된 기계어를 캐시에 저장한다. 한 번 컴파일된 이후에는 캐시에 보관된 기계어를 사용하기 때문에 바뀐 부분만 컴파일하여 빠르게 수행할 수 있다.
  • Java 바이트코드를 기계어 변환하는 데에도 비용이 들기 때문에 JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않는다. 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행한다.

3) Garbage Collector

  • RuntimeDataArea의 Heap 영역의 더 이상 참조되지 않는 객체를 해제하는 방식으로 메모리를 관리한다

 

참고자료

 

'Java' 카테고리의 다른 글

Java 기본 개념  (0) 2021.07.29