프로그래밍 언어/Java

[JAVA] Java란? (JVM 아키텍처 (1))

hanrabong 2022. 5. 11. 22:39

 

JAVA 로고

 

 요새 Spring boot를 이용하여 백엔드 개발을 하면서 프레임 워크와 마찬가지로 바탕이 되는 언어에 대해 공부하고 기록하려 합니다. 틀린 내용이 있으면 언제든 댓글로 지적해주세요!!

 

 

 JAVA란?


 Java는 1995년 썬 마이크로시스템즈의 제임스 고슬링과 다른 연구원들이 개발한 객체 지향적 프로그램 언어입니다. 많은 개발자들이 사용하고 개인적으로 국내에서 백엔드 개발자를 하기 위해서 거의 필수적으로 알아야 하는 언어라고 생각합니다. 

 

 

 Java 언어의 특징이라고 하면 객체지향 언어, 운영체제에 독립적, 자동으로 메모리 관리 등 여러가지가 있습니다. 이번 글에서는 Java가 동작하는 JVM에 대해서 얘기해보려고 합니다.

 

 

Java Environments


 대부분 언어들은 프로그램 개발, 컴파일 등 실행이 되기 위해서는 특정한 environment가 필요합니다. JAVA에도 이러한 environment가 2 개 있고, Java 언어로 작업하는 모든 사람은 local 환경이나 production 환경에 2 중 하나를 설정한 후 작업을 해야합니다.

 

  • JRE(Java Runtime Environment)
  • JDK(Java Development Kit)

 

JRE: Java application이 실행이 되기위해 필요한 최소한의 환경입니다. JVM(Java Virtual Machine)과 deployment tool을 포함하고 있습니다.

JDK: Java application을 개발하고 실행하는데 사용되는 개발 환경입니다. JRE와 development tool 모두 포함됩니다.

 

 

쉽게 말하면 JRE는Java 프로그램을 구동하기 위해 환경이고 JDK는 개발자를 위한 개발 도구로 이루어져 있습니다.Java 프로그램을 구동하기 위해서 JDK가 JRE를 필요로 하기에 포함되어 있습니다.

 

 

JAVA 가 어떻게 동작을 할까?


  Java는 다른 언어와 마찬가지로 특정한 포맷이 있습니다. C++의 경우 .cpp, js의 경우 .js처럼 Java는 .java 형식을 사용합니다. 또한 여러 IDE를 이용하여 개발을 하면 됩니다. Java를 위한 Intellij나 Eclipse등을 이용하여 좀 더 편하게 개발할 수 있습니다.

 

 앞서 Java언어의 특징중에 운영체제에 독립적이라고 하였습니다. C++과 같은 언어는 특정 플랫폼에만 일치하도록 소스코드를 컴파일하고 해당 OS 및 하드웨어에서 기본적으로 실행이 됩니다. Java의 경우 JDK에 있는 Java compiler(javac)를 사용하여 byte code(.class)의 중간상태로 컴파일이 됩니다. 컴파일이 된 이 파일을 JVM이 OS 및 하드웨어 플랫폼에서 이해할 수 있는 machine language로 interpret 합니다. 따라서 바이트 코드는 기본 OS 및 하드웨어 아키텍처에 관계없이 JVM간에 이식할 수 있는 독립적인 상태로 작동합니다. 하지만 JVM이 직접적으로 기본 하드웨어 및 OS 구조와 실행 및 통신해야 하기에 OS버전 및 프로세서 아키텍처에 맞는 JVM을 선택하여 설치해야 합니다. 

 

 이렇게 보면 JVM이 알아서 interpret하고 JVM 내부에 있는 JIT(Just -in-time) 컴파일러나 GC(Garbage Collection)가 알아서 동작하는 것처럼 보입니다. 저 또한 처음 Java를 공부했을 때 JVM이 내부적으로 어떻게 동작하는지 보다 어떤 일을 하는지에 대해서만 알고 있었습니다. 

 

이제 내부적으로 JVM이 어떻게 이루어져 있고 어떻게 동작하는지에 대해 알아보겠습니다.

 

 

JVM Architecture


 JVM을 이용하여 OS 및 하드웨어 독립적으로 byte code를 실행할 수 있습니다. 따라서 앞서 말했듯이 운영체제와 아키텍처에 맞게 JVM을 설치해야합니다. JVM은 기기에 따라 구현이 다르기에 일반적으로 생각되는 구조에 대해서 알아보겠습니다.

 

JVM Architecture

 

1. Class Loader Subsystem


 실행하는 동안 Class Loader Subsystem을 이용하여 class 파일들을 RAM으로 가져옵니다. 이것을 Java의 dynamic class loading 기능이라고 부릅니다. compile time이 아닌 runtime에 처음으로 클래스를 참조할 때 클래스 파일을 load, link, intialize합니다.

 

Loading

 compile된 class(.class)를 메모리로 loading하는 작업은 Class loader의 주요 작업입니다. 일반적인 class loading 프로세스는 main class를 load하는 것부터 시작합니다. 이후의 모든 class loading은 이미 실행 중인 클래스의 다른 클래스 참조에 따라 수행됩니다. 

 

 

class loader에는 3가지 종류(계층 구조)가 있고 4가지 주요 원칙을 따릅니다.

 

 3가지 종류의 class Loader들은 다음과 같은 계층 구조를 띄고 있습니다. 위에서부터 Parent Class Loader이고 내려갈수록 해당 Parent의 Child Class Loder입니다.

 

 

Bootstrap Class Loader: JVM 실행 시 가장 먼저 실행되는 Class Loader입니다. 코드의 Java class를 로드하는 것이 아닌 $JAVA_HOME/jre/lib 디렉토리에 있는 핵심 Java API class와 같은 rt.jar에서 표준 JDK class를 load합니다. C/C++와 같은 언어로 구현되며 Java의 모든 class loader의 부모 역할을 합니다.

 

 

Extension Class Loader: class loading 요청을 부모인 bootstrap class loader에게 위임하고 실패할 경우 확장 경로인 $JAVA_HOME/jre/lib/ext 또는 다른 경로에서 class를 로드합니다. 이 class loader는 sun.misc.Launcher$ExtClassLoader class가 수행합니다.

 

 

System/Application Class Loader: -cp 또는 -classpath 명령줄 옵션을 사용하여 프로그램을 호출하는 동안 설정할 수 있는 system class 경로에서 응용 프로그램 특정 클래스를 로드합니다. 쉽게 말해 우리가 만든 .class 확장자 파일을 이 class loader가 loading합니다. 이 class loader는 sun.misc.Launcher$AppClassLoader class가 수행합니다.

 

 

위 클래스 loader가 따르는 4가지 규칙에 대해서 간략하게 알아보겠습니다.

 

Visibility principle: Child Class Loader는 Parent Class Loader에 의해 load된 class를 볼 수 있지만, 역은 성립하지 않습니다. 

 

 

Uniqueness principle: Parent Class Loader에서 load된 class는 Child Class loader에서 load되면 안 된다는 규칙입니다. 이러한 규칙으로 인해 class loading이 중복됨을 막을 수 있습니다.

 

 

Delegation Hierarchy Principle: 위에서 설명한 2가지 규칙을 만족하기 위해서 JVM은 각 class loading요청에 대한 class loader를 선택하는 위임 계층을 따릅니다. 위에서 본 Class loader의 계층 구조 사진에 따라 요청이 들어오면 각 요청을 자신의 Parent Class Loader로 위임합니다.(Application Class Loader -> Extension Class Loader -> Bootstrap Class Loader) 위임을 하였지만 해당 Class Loader에 없을 경우 다시 내려와서 Child Class Loader에서 지정 경로에 따라 class를 찾습니다. Application ClassLoader에도 없을 경우 java.lang.ClassNotFoundException이 발생합니다.

 

 

No Unloading Principle: Class Loader들은 class를 load할 수 있지만 unload 할 수 는 없습니다. unload하는 대신에 현재 class loader를 삭제하고 새로운 class loader를 생성할 수 있습니다.

 

Linking

 Linking은 load된 class 또는 interface, superclass, 필요에 따라 element type을 확인하고 준비합니다. Linking이 되기 전에 class 또는 interface는 완벽하게 loading이 되어야 합니다. linking하는 동안 오류가 발생하면 직접 또는 간접적으로 오류와 관련된 class 또는 interface에 대한 연결이 필요할 수 있는 프로그램의 한 지점에서 오류가 발생합니다.

 

Linking은 3 단계로 이루어집니다.

 

Verification: .class 파일의 정확성을 확인합니다. 예를 들어 코드가 Java 언어 사양에 따라 올바르게 작성이 되었는지?, JVM 사양에 따라 유효한 컴파일러에 의해 생성이 되었는지 등을 검사합니다. Class Load process 중 가장 오래 걸리고 복잡합니다. Linking이 오래 걸리기에 class load process를 느리게 하지만 한 번 검사를 하면 bytecode를 실행할 때 매번 검사를 할 필요가 없기에 효율적이고 효과적입니다. Verification에 실패하면 java.lang.VerifyError를 발생시킵니다.

 

Preparation: static storage나 method table과 같이 JVM에서 사용하는 모든 데이터 구조를 위한 메모리를 할당합니다. 

 

Resolution: Symbolic 참조를 직접 참조를 변걍합니다. 참조된 엔티티를 위치시키기 위해 method area를 검색하여 수행합니다.

 

 

Initialization

 Load된 각 class 또는 interface의 초기화 logic이 실행됩니다. JVM이 multi-threaded기 때문에 class 또는 interface 초기화는 적절한 동기화와 함께 매우 조심스럽게 발생해야 다른 thread가 동시에 동일한 class 또는 interface를 초기화하려고 시도하는 것을 방지할 수 있습니다. 

 

이 단계는 모든 static variable이 코드에 정의된 원래 값으로 할당되고 static blocking이 실행되는 마지막 단계입니다. 클래스에서는 위에서 아래로 클래스 계층에서는 부모에서 자식으로 한 줄씩 실행됩니다.

 

 

 

2. Runtime Data Area


1번과 2번으로 나누어서 각각 다른 단계라고 생각할 수도 있지만, Class Loader system이 class를 로드할 때 저장하거나 위에서 preparation 단계처럼 Class Loader System에서 load, link, initialize과정에서 데이터(class 등)들이 저장되는 위치라고 생각하면 됩니다.

 

 

 JVM이 OS에서 실행될 때 할당되는 메모리 영역입니다. Class Loader system은 .class file을 읽는 것 이외에도 해당 binary 데이터를 생성하고 다음 정보를 각 클래스의 method area에 별도로 저장합니다.

 

  • load된 class의 정규화된 이름과 직계 parent class
  • .class 파일이 Class/Interface/Enum과 관련이 있는지 여부
  • modifier, static 변수 그리고 method information 등

 

그런 다음 load된 모든 .class 파일에 대해 java.lang 패키지에 정의된대로 heap memory에 file을 나타내기 위해 정확히 하나의 class 객체를 생성합니다. 이 Class 객체는 나중에 우리가 코드에서 클래스 level 정보(클래스 이름, 부모 이름, 메서드, 변수 정보, 정적 변수 등)를 읽는데 사용할 수 있습니다. 쉽게 말해 .class 파일에 class나 Interface등이 있는데 나중에 heap memory에 코드에서 생성한 instance를 저장할 때 참고하기 위한 class 객체 하나를 만들어 놓습니다. 

 

 

Method Area

 이 영역은 JVM에 하나뿐인 영역으로 모든 thread가 공유하는 영역입니다. method area는 class level data를 저장합니다. 쉽게 말해 프로그램 실행 중 어떤 클래스가 사용되면, Class Loader에서 class를 load해야하는데 .class 파일이나 해당 디렉토리에서 load한 클래스를 저장합니다. 

 

  • Run time constat pool: 숫자 상수, field reference, method reference, 각 class 및 interface의 상수뿐만 아닌 method 및 field에 대한 모든 reference가 있다. method나 field가 참조될 때 JVM은 run time constant pool을 사용하여 메모리에서 method나 field의 실제 주소를 검색한다.
  • Field data: 이름, 유형, modifier, attributes
  • Method data: 이름, 반환 유형, 매개변수 유형, modifier, attributes
  • Method code: bytecode, operand stack size, 지역 변수 크기, 지역 변수 테이블, 예외 테이블 등

 

Method Area는 쉽게 말해 .class bytecode나 class에 대한 정보들을 담고 있습니다.

 

 

※ Method area에 대해 공부하고 자바 메모리 구조를 찾아보았을 때 method area라는 명칭이 나오지 않아서 많이 헷갈렸습니다. Method area는 JVM내에서 class에 대한 meta data와 run time constant pool등을 저장하는 논리적인 개념을 말합니다. 실제로 이러한 데이터가 저장되는 장소는 Java 8버전 이전에는 heap 영역의 Perm gen에 저장을 했다고 합니다. 현재 JAVA 8 이후에는 metaspace에 해당 데이터를 저장하고 있습니다. metaspace는 JVM내의 heap memory에 위치하는 거이 아닌 native memory인 시스템 기본 메모리에 위치합니다. 자세한 차이점은 아래 글을 확인해주세요

https://stackoverflow.com/questions/27131165/what-is-the-difference-between-permgen-and-metaspace

 

What is the difference between PermGen and Metaspace?

Until Java 7 there was an area in JVM memory called PermGen, where JVM used to keep its classes. In Java 8 it was removed and replaced by area called Metaspace. What are the most important differ...

stackoverflow.com

 

Heap Area

 이 영역 또한 공유 자원입니다. Thread끼리 공유하기 때문에 동기화 문제가 발생할 수 있습니다. 모든 객체의 정보와 해당 instance 변수 및 배열은 heap memory에 저장이 됩니다.  프로그램 실행 중 생성되는 인스턴스가 모두 이 영역에 저장된다고 생각하면 됩니다. Garbage collection의 대상이 되는 영역입니다.

 

Stack Area

 스택 영역은 각 thread마다 갖고 있는 영역입니다. JVM thread에 대해 thread가 시작되면 method 호출을 저장하기 위해 별도의 runtime stack이 생성됩니다. 매번 method call을 부를 때마다 해당 Stack Frame이 runtime stack의 top에 추가(push)가 됩니다.

 

 각 stack frame에는 지역 변수 array, Operand stack, 실행 중인 method가 속한 class의 runtime constant pool에 대한 참조가 있습니다. 지역 변수 array와 Operand stack의 크기는 컴파일하는 동안에 결정됩니다. 따라서 stack frame의 크기는 method에 따라 고정되어 있습니다.

 

method가 정상적으로 반환이 되거나 method 호출 중에 잡히지 않은 예외가 throw되면 frame이 제거(pop)가 됩니다. 스택 영역은 공유리소스가 아닌 thread별로 존재하기에 thread로 부터 안전합니다.

 

JVM Stack Configuration (Image: Cubrid.org)

 

 

Stack Frame은 세 가지 하위 항목으로 나뉩니다.

 

  • Local Variable Array(지역 변수 array): 0부터 시작하는 인덱스를 갖습니다. 0은 method가 속한 class 인스턴스의 참조이빈다. 1부터 method로 전송된 매개변수가 저장됩니다. method 매개변수 다음으로 지역 변수가 저장됩니다.
  • Operand Stack(피연산자 스택): 작업을 하는 공간 
  • Frame Data: method와 관련된 모든 symbol이 여기 저장됩니다. 예외처리나 constant pool 참조 정보 등이 있습니다.

 

런타임 stack frame이므로 thread가 종료되면 해당 stack frame도 JVM에 의해 파괴됩니다.

 

PC register

 각 JVM의 thread는 thread가 시작될 때 현재 실행중인 명령어의 주소를 유지하기 위해 별도의 PC(Program counter) 레지스터가 생성됩니다. 실행이 완료되면 PC 레지스터는 다음 명령어의 주소로 업데이트 됩니다.

 

Native Method Stack

  Java thread와 기본 운영체제 thread 간에 직접 매핑이 있습니다. Java thread에 대한 모든 상태를 준비한 후 JNI(Java Native Interface)를 통해 호출되는 기본 메서드 정보를 저장하기 위해 별도의 기본 스택이 생성됩니다. 쉽게 말해 native method를 수행하기 위한 스택이라고 생각하면 됩니다. Native method라고 하면 C나 C++로 구현된 메소드로 주로 하드웨어에 접근하는 method입니다.

 

 

 

JVM에서 어떻게 class가 memory에 loading되고 메모리 구조는 어떻게 이루어져 있으면 어떻게 할당을 받는지에 대해 알아보았습니다. 다음에는 Execute engine을 통해 어떻게 실행되는지에 대해 알아보겠습니다.

 

 

 

 

※ 밑의 블로그를 참고하여 정리 하였습니다. 이해가 안 가거나 틀린 내용은 언제든지 댓글 달아주세요

 

 

참고자료

https://medium.com/platform-engineer/understanding-jvm-architecture-22c0ddf09722

 

Understanding JVM Architecture

Understanding JVM architecture and how Java really works under the hood is an important learning for every Java developer.

medium.com

 

https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html

 

Chapter 2. The Structure of the Java Virtual Machine

Conditional branch: ifeq, ifne, iflt, ifle, ifgt, ifge, ifnull, ifnonnull, if_icmpeq, if_icmpne, if_icmplt, if_icmple, if_icmpgt if_icmpge, if_acmpeq, if_acmpne.

docs.oracle.com