ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Java]JVM Architecture란?
    JVM 2020. 5. 24. 17:17
    반응형

    JVM(Java Virtual Machine)

    "자바 가상 머신"이라고 불리는 JVM은 자바 프로그램을 실행하고, 다른 언어로 작성된 것도 자바 byte code로 컴파일하여 실행할 수 있기 위하여 만들어졌습니다.


    자바의 작동 방식

    자바는 기본적으로 "Write once, Run anywhere"의 정신으로 만들어졌습니다. 말 그대로 한 번 작성한 내용은, 어디서든지 읽고 실행될 수 있어야 한다는 말인데요. 흥미롭습니다.

     

    C++ 같은 경우에는 특정한 운영체제, 하드웨어에서 실행되기 위해서 컴파일되지만, 자바는 byte code로 컴파일됩니다. 이는 흔히 우리가 보는 .class 파일입니다. 이때 JDK에 포함되어 있는 자바 컴파일러(javac)를 사용하여 컴파일하게 됩니다. 이 Bytecode를 JVM은 OS와 하드웨어가 이해할 수 있는 기계어(native machine language)로 바꾸게 됩니다(interpret).

     

    이러한 과정 덕분에 Bytecode는 OS, 하드웨어와 독립적으로 존재할 수 있습니다. 왜냐하면 OS 종류와 관계없이 오직 JVM을 위해서 컴파일을 하면 되기 때문입니다. 대신 JVM은 현재 사용하고 있는 로컬머신의 OS에 맞는 적합한 버전을 선택해야 합니다. 그래야 어떤 Bytecode라도 OS를 위해서 기계어로 잘 바꿔줄 수 있습니다.

     

    추가적인 내용은 아래의 링크를 참고해주세요.

     

    왜 JVM이 필요할까?


    JDK vs JRE

    참 헷갈리는 이름들입니다. JVM, JRE, JDK는 3대 자바 프로그래밍의 기술 패키지로 불립니다. JDKJava Development Kit, JREJava Runtime Environment입니다. 영어로 보니 대충 감이 옵니다. JDK자바 기반의 소프트웨어를 개발하기 위한 도구이며, JRE자바 코드를 실행하기 위한 환경입니다. JDK는 JRE를 가지고 있고, 컴파일러도 가지고 있습니다.

     

    앞에서 설명했지만 자바의 작동 과정을 간단히 다시 설명하겠습니다.

    1. Java 코드 작성
    2. JDK의 컴파일러로 컴파일 --> Bytecode 변환(.class확장자)

     

    추가적으로 이 글에서 알려드릴 내용은 JRE에 관한 내용입니다. 즉 컴파일 후 실행가능한 파일이 되고 난 뒤에 파일이 어떻게 실행되는지에 관한 글 입니다. 컴파일과 런타임의 상황을 구분지어 읽으셔야 합니다. 

     

     


    JVM Architecture

     

     

    1. Class Loader Subsystem

    먼저 "클래스 로더(Class Loader)"에 대해서 알아야 합니다.

     

    기본적으로 자바 프로그램은 클래스들로 이루어져 있습니다. 앞서 나온 설명처럼 IDE에서 프로그래머가 작성한 코드를 컴파일하면, JVM이 읽을 수 있는 .class 파일이 됩니다. 클래스 로더는 JVM안에 존재하며, 클래스를 메모리(RAM)에 로드해서 실행할 수 있게 해 줍니다. 왜냐하면 JVM은 운영체제로 부터 메모리를 사용할 수 있게 할당받았기 때문입니다.

     

    지금부터 메모리라는 단어가 많이 나옵니다. 조금 헷갈릴 수 있습니다. JVM에서 데이터를 저장하는 공간이 있지만, 결국에는 컴퓨터 하드웨어상의 RAM(메모리)에 올라가야 프로세스로서 실행이 됩니다. 즉 JVM안의 메모리라는 가상의 공간을 구분지어야 합니다.

     

    하지만 앞서 언급한 것처럼 자바는 먼저 바이트코드로 컴파일 후 JVM에 의해서 바이너리코드 변환 후 Interpret 됩니다. 이때 모든 코드가 통째로 Interpret 되는 것이 아닌, 애플리케이션에 필요한 객체들이 JIT에 의해서 Interpret 됩니다. 이를 자바의 동적인 클래스 로딩(Java's dynamic class loading)이라고 합니다. 그리고 이를 클래스 로더가 담당하고 있습니다. 

     

    위의 과정들은 클래스 파일들이 처음으로 Runtime 될 때 loads(적재), links(연결), initialize(초기화) 합니다. 컴파일될 때의 이야기가 아닙니다.

     

    JIT에 대해서는 뒤에 다시 한번 언급합니다. 너무 걱정 마세요.

     

    1.1) Loading (적재)

    클래스 로더의 주요한 일은 컴파일된 클래스를 메모리에 적재하는 일입니다. Loading의 작업은 보통 static main() 메서드부터 시작됩니다. 이미 로딩된 클래스는 "UnLoad"될 수 없습니다. Unloading 대신에 현재의 클래스 로더를 삭제하고, 새로운 클래스 로더를 생성할 수 있습니다.

    System/Application class loader(시스템/애플리케이션 클래스 로더)

    시스템의 클래스 패스(class path)를 따라서 클래스 파일 자체를 JVM으로 로딩합니다. 프로그래머가 만들고, 컴파일러가 변환한 .class 파일을 적재합니다. 

     

    Extension class loader(익스텐션 클래스 로더)

    부모 로더인 부트스트랩 로더에 클래스 로딩을 부탁하거나, 부트스트랩이 필요가 없다면 extensions directories로부터 클래스들을 로드합니다. 여기서 말하는 extensions directories에는 jre/lib/ext에 위치한 확장 클래스들을 적재합니다. 웹 프로그래밍을 하게 된다면 WAS가 구동될 때 위의 디렉토리에서 필요한 라이브러리를 사용하게 됩니다.

     

    Bootstrap class loader(부트스트랩 클래스 로더)

    기본적으로 클래스로더는 java.lang.ClassLoader에 의해 실행됩니다. 근데 이 ClassLoader는 그냥 클래스입니다. 그러면 이 ClassLoader는 누가 load 할까요?

     

    이것을 부트스트랩 클래스 로더가 합니다. 운영체제 시간에 배웠던 것을 되짚어 봅니다. 운영체제에서 부트스트랩 로더라는 것이 있습니다. 컴퓨터 전원이 켜지면 가장 먼저 부트스트랩 로더가 바이오스로부터 제어권을 넘겨받습니다. 그리고는 RAM, 하드웨어 등을 초기화하고 운영체제 커널을 컴퓨터 메모리에 올립니다. 그 후에 제어권을 운영체제 커널에게 넘기는데요. 이것만 보더라도 부트스트랩 로더라는 것이 어떤 실행 동작에 있어서 뿌리, 혹은 시작점이라고 생각을 할 수 있습니다.

     

    자바에서는 부트스트랩 로더가 자바의 클래스 로더를 실행시키고, rt.jar, 코어 자바 API 같은 일반적인 JDK 클래스를 로드합니다. 자바에서 부트스트랩 클래스 로더는 C, C++ 같은 Native language로 작성되어 있습니다. 여기서 말하는 Native code는 Unmanaged code라고도 불리는데요. 메모리에 할당된 것을 프로그래머가 직접 해제해줘야 하는, 기계어로 바로 컴파일되는 언어들입니다. 앞에서 언급했듯이 JVM은 C, C++로 만들어졌습니다. 그렇기 때문에 JVM이 ByteCode(.class파일)을 기계어로 바로 바꿔줄 수 있는 것입니다. 자바는 당연히 Managed code입니다. JVM이 가비지 컬렉터로 메모리 할당의 해제도 알아서 해주니까요. 

     

    전체적으로 정리하자면 부트스트랩 로더가 가장 부모입니다. JVM이 시작되면 가장 먼저 부트스트랩 로더가 적재되고, 다른 자식 클래스들을 가지고 오는 구조입니다. 

     

    1.2) Linking (연결)

    로드된 클래스나 인터페이스를 검증(verify)하고 준비(prepare)하는 과정입니다. 

     

    기본적으로 클래스 파일은 여러 개입니다. 하지만 한 개의 클래스 파일들이 전체 애플리케이션에 필요한 모든 작업을 혼자서 할 수는 없습니다. 서로 상호작용하는 클래스들을 연결해주는 작업이 필요합니다. 이때 아래의 조건은 반드시 필요합니다.

    • 클래스, 인터페이스는 링크되기 전에 반드시 적재(load)되어야 합니다
    • 클래스, 인터페이스는 다음 스텝인 초기화되기 전에 반드시 검증과 준비가 되어야 합니다

     

    링크는 아래의 3가지 단계로 실행됩니다.

     

    Verification(검증) 단계에서는 컴파일러가 변환한 바이트 코드가 바이너리 코드로 잘 변환을 했는지, 확인하는 과정입니다.

    보통 JVM에 들어오면 완벽한 바이너리 코드로 변환이 된다고 생각을 합니다. 하지만 클래스로더가 적재를 할 때는 기계가 읽을 수 있는 완벽한 바이너리 코드는 아닙니다. 아직은 '목적파일'이며, 이를 실행 가능한 상태로 만들어 주는 것이 linking 과정입니다.

    만약 변환한 클래스, 인터페이스 바이너리 코드에 구조적이나 보안상 문제가 있다면 VerifyError가 발생합니다. 그리고 JVM이 바이너리 코드를 검증하는데 에러가 발생한다면 LinkageError가 발생합니다. 이 과정은 가장 복잡하고, 시간이 오래 걸리는 작업입니다.

    Preparation(준비) JVM에 의한 데이터 구조나 Static 저장공간을 위해서 메모리를 할당하는 과정입니다. Static field는 기본 값으로 생성되고 초기화됩니다.

    Resolution(실행) 단계에서는 Symbolic reference가 direct reference로 대체됩니다. 즉 이는 참조하고자 하는 대상의 이름만 가지고 참조 관계를 구성하는 것이 아닌, 실제 객체의 주소를 참조하게 됩니다. 여기서 실제 객체의 주소를 참조한다는 것은 메모리에 할당된 실제 주소를 코드에 반영하고, 실행 가능한 바이너리 코드가 되는 것입니다.

     

    1.3) Initialize (초기화)

    클래스나 인터페이스의 초기화 로직이 실행됩니다(생성자). 이는 링크 단계에서 기본 값으로 초기화된 Static 변수들을 프로그래머가 입력한 값으로 정의해줍니다. 

     

     

     

    2. Runtime Data Area 

    이는 JVM 프로그램이 실행이 될 때 운영체제로 부터 할당받은 메모리의 영역입니다. 

     

    위에서 클래스로더가 메모리(RAM)에 적재를 한다고 했는데요, 정확히 말하자면 여기서 말하는 Runtime Data Area입니다. JVM 자체가 메모리를 사용하기 때문에 결과적으로는 RAM에 적재하지만, JVM안에서는 Runtime Data Area라는 영역에 할당을 합니다. 

     

    2.1) Method area 

    이는 JVM당 1개만 존재하는, 공유자원입니다. 모든 JVM의 스레드는 이 공간을 공유하게 됩니다. 컴파일된 코드의 정보를 클래스, 인스턴스 단위로 저장됩니다.

     

    위에서 언급했듯이 JVM은 전체 코드를 Byte code로 통째로 변환 후 실행에 필요한 코드를 Interpret 하는데요. 하지만 이 모든 코드들이 인스턴스화 된 객체가 아닙니다. 이때 Method area에 올라오는 클래스 단위의 코드들은 New로서 인스턴스화 된 클래스의 정보들입니다.

     

    메서드 영역은 아래와 같은 정보들을 저장합니다.

     

    Constant pool는 상수값을 저장합니다. 여기에 해당하는 상수는 런타임 시에 필요한 모든 종류의 숫자, 문자열, 식별자 이름, 클래스 파일, 메서드 정보들입니다.

     

    Constant pool은 이러한 상수값 정보들을 Symbol Table 형식으로 가지고 있습니다. 조금 생소합니다. 간단합니다. 자료구조의 개념 중 하나이며 Key, Value로 나누어져, Key를 찾으면 Value를 연결해줍니다. 즉 정보들을 Pool에 저장하고, 쉽게 가지고 오기 위해서 Key를 이용합니다. 

     

    그 외에도 Field data, Method data, Method/Constructor code 같은 것들을 저장합니다.

     

    2.2) Heap area

    힙 영역도 JVM당 1개만 존재하는, 공유자원입니다. 사용자들이 New를 통해서 메모리에 객체를 할당하게 되면 모든 객체들, 인스턴스 변수, 배열들이 할당되고 공유됩니다. Method area들은 메모리에 할당되는 인스턴스의 정보들을 가지고 있다면, Heap area는 실제로 할당된 데이터가 있는 공간입니다. 그래서 Method area를 Heap area의 logical part(논리적 부분)이라고 표현합니다.

     

    그리고 힙 영역의 크기는 프로그래머에 의해서 가변적으로 바꿀 수 있습니다. 한번 설정해둔 크기를 그대로 둘 수도 있고, 데이터의 크기에 따라서 변화를 줄 수도 있습니다. 또한 힙 영역에 올라온 인스턴스는 사라지지 않습니다. 더 이상 애플리케이션에서 사용되지 않을 때 GC에 의해서 제거되는데요. 더 자세한 내용은 아래의 내용을 참고해주세요.

     

    Java Memory Model(자바 메모리 모델)

     

    Heap, Method 영역은 다중스레드에 의해 공유되면서, 저장된 데이터들이 thread safe 하지 않게 됐습니다. 즉 동기화 문제가 발생합니다. 

     

    이러한 문제를 해결하기 위해서 한 번에 한 스레드만 진입할 수 있는, 임계구역(critical section)을 만들어야 합니다. 하지만 멀티스레드 환경에서 임계구역을 설정하는 것은 쉽지 않은 일입니다. 이때 세마포어(Semaphore)를 활용해서, Lock을 걸어야 합니다. 즉 한 스레드가 임계구역으로 들어갈 때 Lock을 걸고, 나오면서 Lock을 해제하는 방식입니다.

     

    하지만 이러한 세마포어 방식이 꼭 완벽한 것만은 아닙니다. 한 스레드가 일을 할 때, 다른 스레드는 기다려야 합니다. 이런 스레드들이 바쁜대기(Busy waiting)를 하게 되면서, CPU의 자원을 낭비하기도 합니다.

     

     

    3.3) Stack area

    스택 영역은 스레드당 하나씩 존재합니다. 즉 공유자원이 아닙니다.

     

    모든 JVM 스레드를 위해, 개별의 runtime stack은 스레드가 시작할 때 메소드 호출을 저장하기 위해서 생성됩니다. 모든 메소드 호출이 발생하면 하나의 시작점(entry)은 생성되어 runtime stack의 가장 위로 들어가게 됩니다. 이를 스택 프레임(stack frame)이라고 합니다. 각각의 프레임에는 로컬 변수의 배열, 오퍼랜드 스택, 상수 풀이 존재합니다.

     

    스택 영역은 힙 영역과 다르게 임시영역 입니다. Heap 영역에서 객체가 실행이 되고, 메소드가 필요하게 되면 스택 영역의 Frame에 메소드가 할당됩니다. 그리고 메소드가 끝나면 스택에서 빠져나오게 됩니다.


    이 프레임은 메서드가 리턴되거나 Exception을 던지면 제거됩니다. 정확히는 제거된다기보다는, pop 즉 빠져나오게 됩니다. 스택 영역은 공유자원이 아니기 때문에 thread safe 합니다.

     

    언뜻 읽었을 때는 Stack과 Heap 영역이 다른 게 없어 보입니다. 둘 다 결국 데이터가 올라가는 것아니야? 라고 생각할 수 있습니다. 다른 점은 데이터의 종류입니다. Heap에는 클래스, 인스턴스 단위의 인스턴스가 올라가게 되고, Stack은 Heap을 참조하는 메소드가 올라오게 됩니다. 그리고 메소드의 실행이 끝난다면 스택에서 빠져나오게 됩니다.

     

     

     

    2.4) PC Register (프로그램 가운터)

    컴퓨터공학과에서 배우는 운영체제 과목을 공부하신 분들은 쉽게 이해하실 수 있습니다. PC는 Program Counter입니다. 스레드당 1개씩 존재하며, 스레드가 실행되면 현재 실행하는 지시를 저장하고, 끝나면 다음 실행될 지시의 주소를 가리키게 됩니다.

     

     

    2.5) Native Method Stack

    자바 스레드와 네이티브 코드(C, C++)로 작성된 코드 사이를 매핑하는 역할을 합니다. 

     

    네이티브 메소드에 대해서 조금 더 설명을 해보자면, 이 메소드는 네이티브 라이브러리(Native Library)와 연결됩니다. 이때의 네이티브 라이브러리는 자바로 쓰여진 것이 아니라 C와 같은 다른 언어로 작성된 것들입니다. 이러한 라이브러리는 레거시(legacy) 데이터 혹은 성능을 위해서 사용되었는데요. 자바의 라이브러리도 발전을 하면서 점차 쓰이지 않게 되었습니다. 

     

    이러한 네이티브 코드는 프로그래머가 JNI(Java Native Interface)를 통해서 호출할 수 있습니다. 

     

    아래의 사진은 일반적인 자바 코드와 네이티브 코드의 차이점을 보여주는 예시입니다.

     

    stackoverflow

     

    3. Execution Engine

    컴파일러가 변환한 Bytecode가 실제로 실행되는 공간입니다. Runtime Data Area를 위해서 Bytecode를 한줄한줄 읽습니다. 

     

    3.1) Interpreter

    인터프리터는 Bytecode를 한줄한줄 읽고 실행합니다. 

     

    3.2) Just In Time Compiler(JIT)

    자바는 Bytecode로 변환한 후에 다시 기계어로 변환해야 해서, C보다 느릴 수밖에 없습니다. 이를 극복하기 위해서 이미 기계어로 변경한 것을 저장해 뒀다가, 다음에 다시 사용할 때 기계어를 그대로 사용하는 것입니다. 이때 사용된 기계어는 캐시에 저장되게 됩니다. 그렇기 때문에 좀 더 빨라졌지만, 굉장히 비싼 자원을 사용하고 있습니다. 

     

    그래서 "adaptive compiling"이 생겼습니다. 기본적으로 컴파일 방식에는 정적, 인터프리터 방식이 있습니다. 정적 컴파일 방식은 프로그램 실행 전 기계어로 한 번에 다 바꿔놓는 것입니다. 인터프리터 방식은 코드를 중간 코드(intermediate code)로 변환해서 한줄한줄 기계어로 바꾸면서 읽는 방식입니다. 여기서 말하는 중간 코드는 위에서 언급한 ByteCode 입니다. 즉 파이썬 같은 인터프리터 방식보다는, C의 컴파일러로 컴파일하는 것이 더 빠릅니다. 

     

    자바는 컴파일, 인터프리터 2개의 방식을 같이 사용합니다. .class 파일로 컴파일한 후에 이를 기계어로 인터프리트 하는 방식입니다. 이러한 방식을 사용하게 되면 Bytecode로 바꿔놓고, 메서드 콜이 발생하는 것만 읽으면 되기 때문에 속도 향상을 할 수 있습니다. Bytecode가 중간 코드에 해당한다고 볼 수 있습니다.

     

    이때 왜 C처럼 바로 기계어로 변환하면 되지 않을까? 궁금하신 분들을 위해서 작성했습니다. [Java]JVM 없는 JAVA?

     

     

     

     

    아래의 구성 덕분에 성능이 향상되었습니다. 

    • Intermediate code(중간 코드) 
    • Code Optimiser가 중간 코드가 만들어 지기 전 최적화를 합니다
    • Target Code Generator 기계어를 만듭니다
    • Profiler는 퍼포먼스의 병목(hotspots) 현상을 찾아냅니다(한 메서드가 여러 번 호출되는 것과 같은 경우)

     

    3.3) Garbage Collector(GC)

    객체가 더 이상 참조되지 않는다면, GC가 삭제를 하고 메모리를 더 이상 사용되지 않는 영역으로 만듭니다. 자바에서는 자동으로 작동이 되지만, System.gc()를 통해서 직접 호출할 수 도 있습니다. 

     

     

    4. Java Native Interface(JNI)

    이 인터페이스는 C, C++로 쓰인 Native Libraries을 제공하는 Native Method Libraries와 상호작용하기 위해서 존재합니다. 이는 JVM이 C/C++ 라이브러리를 호출하거나 하드웨어를 위한 C/C++ 라이브러리에 의해서 JVM이 호출되기도 합니다. 

     

    Native Method Libraries는 Native Libraries의 컬렉션(집합)입니다. 이는 위에서 언급한 Excution Engine을 실행하기 위해서 필요합니다. 


    수정사항

    2020년 5월 30일

    Bootstrap Class Loader 내용 추가 및 수정, 오타 교정 

     

    2020년 7월 19일

    Heap Area 동기화 문제 및 임계구역 내용 추가, 기존 불필요한 내용 삭제

     

    2020년 8월 9일

    자바의 작동방식 - 로컬머신 단어 추가, Linking 파트 내용 추가 

     

    2020년 10월 25일

    간략한 내용 추가 및 수정

     

    2020년 12월 22일

    클래스로더 파트 내용 추가 및 변경

     

    2020년 12월 29일

    Runtine Area 파트 내용 추가 및 변경

     

     

     

     

     

     

     

     

     

     

    참고문헌

    Chapter 5. Loading, Linking, and Initializing (oracle.com)

    http://www.ktword.co.kr/abbr_view.php?m_temp1=2658

    https://stackoverflow.com/questions/3434202/what-is-the-difference-between-native-code-machine-code-and-assembly-code

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

    www.itworld.co.kr/news/110837

    medium.com/@lazysoul/jit-just-in-time-16bb63f3ae26

    jeong-pro.tistory.com/148

    ko.wikipedia.org/wiki/JIT_컴파일

    m.blog.naver.com/ehcibear314/221228200531

    velog.io/@litien/가비지-컬렉터GC

    [기본] 바이트코드와 바이너리코드- :: 조주임의 저녁 (tistory.com)

    반응형

    'JVM' 카테고리의 다른 글

    왜 JVM이 필요할까?  (8) 2020.06.06
    Java Memory Model(자바 메모리 모델)  (7) 2020.05.27

    댓글

Designed by Tistory.