• JVM 어떻게 동작하는가? :: 마이구미
    Java 2017. 2. 2. 23:30
    반응형

    이번 글은 JVM(Java Virtual Machine)의 구조와 동작에 대해 다뤄본다.

    원본 글을 번역하였다.


    JVM은 자바 가상 머신으로 불리는 C언어와 가장 큰 차이점이기에 익히 들어보았으리라 생각한다.


    JVM이란 무엇인가?

    자바 프로그램을 실행시키기 위해 런타임(실행시간) 엔진의 역할을 한다.

    실제로 자바 코드에서 main 메소드를 호출하며, JRE(Java Run Environment)의 일부이다.


    자바 프로그램은 WORA(Write Once Run Anywhere)로 표현한다.

    "한번 쓰고 어디에서든 실행한다" 라는 의미를 가진다.

    아무 제약없이 어디서든 개발이 가능한 것을 강조하기 위함이다.

    간단한 예로 일반적인 프로그램은 os가 다르면 호환되지 않는다.

    하지만 자바는 JVM을 통해 호환이 가능하다.


    JVM 호환

    출처


    우리는 .java 파일을 컴파일할 때, 자바 컴파일러로 인해 같은 이름의 .class 파일을 생성한다.

    .class 파일은 바이트 코드(바이너리 코드)로 포맷되어있다. (바이트 코드 -  JVM만을 위한 코드)

    또한 참조하는 라이브러리를 포함하지 않고, 단순히 symbolic references를 가진다.
     * symbolic references란 참고하는 클래스의 특정 메모리 주소를 참조 관계로 구성한 것이 아닌 참조하는 대상의 이름만을 지칭한 것.


    생성된 .class 파일은 다양한 단계로 진행된다. 이러한 진행이 전체 JVM에 대해 설명할 수 있다.



    JVM 구조



    위 그림 전체적인 JVM의 구조이다.

    먼저 클래스 로더(Class Loader)를 살펴보자.


    클래스 로더는 3가지 역할을 담당한다.

    • Loading
    • Linking
    • Initialization

    Loading


    클래스 로더는 .class 파일을 읽어 바이트 코드를 메소드 영역(Method Area)에 저장한다.

    각 .class 파일은 JVM에 의해 메소드 영역 다음의 정보들을 저장한다.

    • 로드된 클래스 비롯한 그의 부모 클래스의 정보
    • class 파일이 Class, Interface, Enum와 관련 여부
    • 변수나 메소드의 정보 등

    .class 파일이 로딩된 후에는, JVM은 힙 메모리 영역에 이 파일이 나타내는 클래스 유형의 객체를 생성한다.


    프로그래밍 코드로 표현해보자.

    예를 들어, java.lang 패키지는 내부적으로  자동으로 import된다.

    이 클래스 객체를 이용하면 클래스의 이름 및 부모 클래스 이름, 메소드, 변수와 같은 정보를 보여줄 수 있다.


    import java.lang.reflect.Field; import java.lang.reflect.Method; public class Test { public static void main(String[] args) { Student s1 = new Student(); Class c1 = s1.getClass(); // 클래스 이름 - Student System.out.println(c1.getName()); // 메소드 - getName, setName, getRoll_no, setRoll_no Method m[] = c1.getDeclaredMethods(); for (Method method : m) System.out.println(method.getName()); // 변수 - name, roll_No Field f[] = c1.getDeclaredFields(); for (Field field : f) System.out.println(field.getName()); } } class Student { private String name; private int roll_No; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getRoll_no() { return roll_No; } public void setRoll_no(int roll_no) { this.roll_No = roll_no; } }



    Linking


    증명(verification), 준비(preparation), 해결(resolution) 3가지를 수행한다.


    • Verification - .class 파일의 정확성을 보장한다. 파일이 적절히 포맷되었는지, 유효한 컴파일러에 의해 생성되었는지 확인한다. 만약 검증이 실패한다면 우리는 런타임 에러를 볼 수 있다. (java.lang.VerifyError)

    • Preparation - JVM은 메모리를 기본 값으로 초기화 한 후, 클래스 변수들을 위한 메모리에 할당한다.

    • Resolution - symbolic references 를 direct references 로 바꾸는 프로세스.

    Initialization

    이 단계에서 모든 정적 변수들이 정의된 값으로 초기화된다.


    일반적으로 3가지의 클래스 로더가 존재한다.

    • 부트스트랩(Bootstrap) class loader - 모든 JVM 구현에는 신뢰된 클래스들을 로딩하기 위해 부트스트랩 클래스 로더가 필요하다. 이 로더는 JAVA_HOME/jre/lib 디렉토리에 존재하는 핵심 java API들을  로드한다. 또한 Java가 아닌 C나 C++ 같은 네이티브 언어를 통해 구현되어 있다.

    • 확장(Extension) class loader - 부트스트랩 클래스 로더의 자식이다. JAVA_HOME/jre/lib/ext 또는 java.ext.dirs 시스템 속성으로 명시된 다른 특정 디렉토리인 확장 디렉토리 안에 존재하는 클래스들을 로드한다. sun.misc.Launcher$ExtClassLoader class로 Java로 구현되었다.

    • 시스템(System/Application) class loader - 확장 클래스 로더의 자식이다. application class 경로부터 클래스들을 로드한다. 내부적으로 java.class.path에 맵핑된 환경 변수를 사용한다. 또한 sun.misc.Launcher$ExtClassLoader class로 Java로 구현되었다.

    public class Test { public static void main(String[] args) { // String 클래스는 Bootstrap loader로부터 로드된다. // 부트스트랩 로더는 자바 객체가 아니다. 그러니 null이 나온다 System.out.println(String.class.getClassLoader()); // Test 클래스 Application loader로부터 로드된다. System.out.println(Test.class.getClassLoader()); } }


    // output

    null sun.misc.Launcher$AppClassLoader@73d16e93


    JVM은 클래스들을 로드하기 위해 위임-계층(Delegation-Hierarchy) 원리를 따른다. 
    시스템 클래스 로더는 로드 요청을 확장 클래스 로더로 위임하고, 확장 클래스 로더는 부트스트랩 클래스 로더로 위임하게 된다. 
    이렇게 먼저 부트스트랩 로더에서 클래스를 찾지 못하면, 확장 로더에서 찾고, 찾지 못하면, 시스템 로더에서 찾게 된다. 
    마지막으로 시스템 로더에서도 클래스 로드를 실패한다면, 우리는 java.lang.ClassNot-FoundException 런타임 에러를 볼 수 있다.
    결론적으로 위에서 아래로 계층적으로 진행되는 것이다. (그림을 보며 이해하자)

    Class loader


    JVM 메모리 영역에 대해 살펴보자.
    Runtime Data Area라고 불리는 영역으로, 간단히 프로그램 수행을 위해 os로부터 할당받는 메모리 영역으로 5가지로 볼 수 있다.

    Method area
    • 클래스 이름, 부모 클래스 이름, 메소드, 변수 정보 등과 같은 수준의 모든 클래스 정보들을 저장한다. 전역변수도 포함된다. JVM에서 오직 하나의 영역만 존재하고, 공유할 수 있다. 그 말은 즉, 다른 스레드에서도 접근할 수 있다는 의미이다.
    Heap area
    • 모든 객체들의 정보를 저장한다. JVM에서 오직 하나의 영역만 존재하고, 공유할 수 있다. 그 말은 즉, 다른 스레드에서 접근할 수 있다는 의미이다. 
    Stack area
    • 모든 스레드에 대해 JVM은 여기에 저장된 하나의 런타임 스택을 만든다. 이 스택의 모든 블록은 메소드 호출이 저장되는 활성화 기록/스택 프레임이라 불린다. 메소드의 모든 지역 변수들은 그들이 해당하는 프레임에 저장된다. 스레드가 끝나면 런타임 스택은 JVM에 의해 소멸된다. 공유 자원은 아니다. 
    PC Registers
    • 스레드의 현재 실행 명령의 주소를 저장한다. 분명히 각 스레드에는 별도의 PC 레지스터를 가지고 있다.
    Native method stacks 
    • 모든 스레드에 대해 별도의 네이티브 스택을 생성한 후 네이티브 메소드 정보를 저장한다.
    아래 그림을 통해 이해하자.

    JVM 메모리 영역



    Execution Engine을 알아보자.

    실행 엔진은 바이트 코드로 된 .class 파일을 실행한다. 
    바이트 코드를 한줄씩 읽고, 다양한 메모리 영역에 나타난 데이터와 정보를 사용하고 명령을 실행한다. 
    이것을 3가지 파트로 분류할 수 있다.

    • 인터프리터(Interpreter) - 바이트코드를 한줄씩 실행하여 해석한다. 단점은 여러번 하나의 메소드를 호출할 경우 매번 해석을 요청해야하기 때문에 비효율적이다.

    • Just-In-Time(JIT) - 인터프리터의 효율을 증가하는 데 이용한다. 전체 바이트 코드를 컴파일하고 네이티브 코드를 변경하여 메소드의 반복 호출을 확인할 때마다 JIT는 직접 네이티브 코드로 제공함으로써 재해석을 요구하지 않으므로 효율성이 증가한다.

    • 가비지 콜렉터(Garbage Collector) - 참조되지 않는 객체를 소멸시킨다. 

    Java Native Interface(JNI) 를 알아보자.

    Native Method Libraries와 상호작용하고 실행에 필요한 네이티브 라이브러리(C, C++)을 제공하는 인터페이스이다. JVM은 C/C++ 라이브러리를 통해 호출할 수 있고, 특정 하드웨어와 관련된 C/C++ 라이브러리로 호출 될 수도 있다.

    마지막으로 Native Method Libraries 를 알아보자.

    Execution Engine으로 인해 요구되는 네이티브 라이브러리(C,C++)의 모음이다.

    원본 글을 참고하면서, 본인이 잘못 번역했거나 잘못 이해했을 수 있다.
    댓글에 남겨주면 감사하겠다.
    원본 글 참고하길 바란다.

    symbolic references 참고


    반응형

    댓글

Designed by Tistory.