ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [JAVA/JVM & Compile] JVM과 컴파일
    Mhwan's Study/JAVA & Kotlin 2021. 1. 19. 02:23

    JAVA는 OS에 독립적으로 실행되는 특징을 갖고 있다. 이는 JVM이 있기 때문에 가능한데, JVM은 자바 프로그램이 기기나 운영체제 상관없이 실행 될 수 있게 하며, 프로그램 메모리(스택, 힙 영역의 관리)를 관리하고 최적화한다.

     

    # JVM의 구성요소

    Class Loader, Execution Engine, Runtime Data Area, Garbage Collector

    여기서 Runtime Data Area (JVM의 메모리 영역 (메소드, 스택, 힙, PC 레지스터, 네이티브 메소드 스택)), Garbage Collector는 이후 더 자세히 적어보려고 한다.

     

    # 컴파일 과정

    이미지 출처 : https://hoonmaro.tistory.com/category/프로그래밍/Java

    1. Java compiler가 소스코드를 컴파일해 바이트 코드(.class)로 변환한다.
    2. 바이트 코드는 JVM은 읽을 수 있지만, 컴퓨터는 읽을 수 없음
    3. 바이트 코드를 JVM의 Class Loader로 로딩한다.
    4. Class Loader는 동적 로딩을 통해 필요한 클래스를 분석해 Runtime Data Area에 배치한다.
    5. 실행 엔진(Execution Engine)은 Runtime Data Area에 배치된 바이트 코드를 해석해 실행한다. 
      • 인터프리터 방식 : 바이트 코드를 한줄씩 읽고 해석하고 실행함, 하나씩 해석하고 실행해서 바이트 코드 하나하나의 실행은 빠르지만, 인터프리팅 결과의 실행은 느리다는 단점을 가짐
      • JIT(Just-In-Time) 컴파일 방식 : 바이트 코드 전체를 컴파일 하여 네이티브 코드로 바꾸고, 이후에는 이를 더이상 인터프리팅하지 않고 네이티브 코드를 바로 실행하는 방식. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관되기 때문에 한번 컴파일된 코드는 계속 빠르게 수행됨
      • 대신 JIT 컴파일러의 컴파일은 바이트 코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸림
      → 인터프리터 방식을 사용하다가 일정 기준을 넘으면 JIT 방식으로 실행함 (만약 한번만 실행하는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리), JVM이 내부적으로 해당 메소드가 얼마나 자주 수행되는지 체크하고, 일정 수준을 넘을때에만 컴파일을 수행

     

    # ClassLoader

    JVM에서 필요한 클래스를 JVM의 메모리 영역에 로딩하는 역할을 담당합니다. 자바는 동적 로딩 방식으로, 런타임에 클래스를 처음 참조할때 해당 클래스를 로드하고 링크. 클래스로더는 로드된 클래스를 보관하는 namespace를 갖고 있어 namespace에 보관되었는지를 기준으로 로드할지를 판단합니다.

    클래스 로더는 로드되지 않은 클래스를 찾으면 아래의 과정을 거침

    1. 로드 : 클래스를 파일에서 가져와서 JVM의 메모리에 로드
    2. 검증 : 읽어들인 클래스가 자바 언어 명세와 JVM명세대로 되어있는지 검사. 클래스 로드의 과정중에 가장 까다롭게 겁사를 수행하는 만큼 시간이 오래 걸림
    3. 준비 : 클래스가 필요로하는 메모리를 할당, 클래스에 정의된 필드, 메소드, 인터페이스를 나타내는 데이터 구조 준비
    4. 분석 : 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경
    5. 초기화 : 클래스 변수를 적절한 값으로 초기화

    클래스 로더는 처음 classpath라는 환경변수에 등록된 디렉토리에 있는 클래스를 먼저 JVM에 로딩한다.

     

     

    # Runtime Data Area (JVM의 메모리 영역)

    이미지 출처 : https://hoonmaro.tistory.com/category/프로그래밍/Java

    스레드마다 생성되는 영역

    • PC레지스터 : 스레드가 시작될때 생성되며, 현재 수행중인 JVM 명령의 주소를 가짐
    • JVM Stack : 스레드가 시작될때 생성, Stack Frame이라는 구조체를 저장하는 스택으로, JVM은 스택에 스택 프레임을 push, pop하는 동작만 수행 (예외처리시 printStackTrace()로 보여주는 것이 하나의 스택 프레임을 표현), 메소드가 실행될때마다 하나의 스택 프레임이 생성되어 JVM 스택에 push되고, 메소드가 종료되면 스택 프레임이 pop됩니다.
      • 지역 변수 배열 : 실제 지역 변수의 배열을 저장하는것이 아닙니다!! 이 배열의 0번째 인덱스는 메소드가 속한 클래스 인스턴스의 this레퍼런스 저장, 1부터는 메소드에 전달된 파라미터들이 저장, 이후 인덱스에는 메소드의 지역변수들이 저장
      • 피연산자 스택 : 메소드의 실제 작업 공간, 각 메소드는 피연산자 스택과 지역변수 배열 사이에서 데이터를 교환하고 다른 메소드 호출 결과를 push하거나 pop함
    • Native Method Stack : JNI를 통해 호출하는 C/C++등의 코드를 수행하기 위한 스택 영역

    스레드끼리 공유하는 공통 영역

    • 메소드 영역 : JVM이 시작될때 생성되는 영역, JVM이 읽어 들인 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메소드 정보, static 변수, 메소드의 바이트 코드 등을 저장, java 8이전에는 heap의 permanent 영역에 존재했지만 8 이후부터는 permanent영역이 없어지고, 네이티브 heap에 할당 (os가 관리)
      • 런타임 상수 풀 : JVM 동작에서 가장 핵심적인 역할 수행, 각 클래스와 인터페이스의 상수 뿐만 아니라 메소드와 필드에 대한 모든 레퍼런스를 담고 있음. 즉, 어떤 메소드나 필드를 참조할때 JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리상 주소를 찾아서 참조함
    • 힙 영역 : 인스턴스 또는 객체를 저장하는 공간, 가비지 컬렉션의 대상

     

     

    ** Java의 경우 Call by value의 특징을 갖고 있는데, reference type의 경우 원본 객체가 전달되어 마치 call by reference인 것 처럼 보입니다. 이는 사실 call by value이지만 해당 객체의 주소값(stack에서 heap 영역의 데이터 참조를 위해 가진 값)을 복사해서 넘깁니다. 그렇기 때문에 자바는 실제 할당된 값의 위치를 값으로 갖고 있고, 이를 전달하기에 heap영역에 갖고 있는 데이터에 접근할 수 있지만, 객체를 새로 덮어씌여버리면 원본 값에는 전혀 영향이 가지 않습니다.

     

     

     - Garbage Collection에 대한 내용은 다음 포스팅에서 다루겠습니다.

     

     

    출처 및 참고 : hoonmaro.tistory.com/category/프로그래밍/Java, gyoogle.dev/blog/computer-language/Java/Java%20Virtual%20Machine.html, https://d2.naver.com/helloworld/1230

    댓글

Designed by Mhwan.