공부/Java

[공부/자바] JVM과 작동원리

오잎 클로버 2022. 2. 27. 09:12
728x90

자바라는 프로그래밍 언어는 어떤 플랫폼에서든 소스코드 변경이 없어도 실행할 수 있습니다.

즉 어떤 CPU나 OS에서든 실행할 수 있단 뜻입니다.

그래서인지는 몰라도 웹 어플리케이션 개발에도 사용되고, 안드로이드에서도 코틀린을 밀어주기 이전엔 자바로 기능들을 구현했습니다.

이를 가능하도록 이번 주제인 JVM이 있었기 때문입니다.

JVM이란?

JVM은 원래 Java Virtual Machine의 약어로, "자바 가상 머신"이라고 번역할 수 있습니다.

가상 머신의 정의는 다음과 같습니다.

특정한 프로그램이 실행될 수 있도록 컴퓨터에 가상 실행 환경을 만들어 주는 소프트웨어가 설치된 컴퓨터

JVM 검색 결과의 문서를 찾아보면

JVM은 자바 바이트코드를 실행할 수 있는 주체다. 일반적으로 인터프리터나 JIT 컴파일 방식으로 다른 컴퓨터 위에서 바이트코드를 실행할 수 있게 구현되나, jop 자바 프로세서처럼 하드웨어와 소프트웨어를 혼합해 구현하는 경우도 있다. 자바 바이트코드는 플랫폼에 독립적이며 모든 JVM은 JVM 규격에 정의된 대로 자바 바이트코드를 실행한다. 따라서 표준 자바 API까지 같은 동작을 하도록 구현한 상태에선 이론적으로 모든 자바 프로그램은 CPU, OS 종류와 무관하게 동일하게 동작할 것을 보장한다

라고 되어 있습니다.

환경을 만들어주는 것이 아닌 코드를 실행해주는 주체임을 알 수 있습니다.

그런데 Java는 타 컴파일 언어와 다르게 바이너리 코드가 아닌 바이트 코드입니다.

바이트코드는 JVM의 명령어 집합이라고 간단하게 설명할 수 있습니다. (자세한 정보는 생략)

자바 코드 실행 과정

자바가 작동하는 방식을 쉽게 요약하자면 다음과 같습니다.

  1. 자바 소스(.java)를 자바 컴파일러로 컴파일을 수행(javac)
  2. 해당 컴파일이 진행된 코드는 바이트코드로 바뀜(.class)
  3. 클래스 요청(Class Loader) -> 메모리 할당(Runtime Data Area) -> 실행(Execution Engine) -> ...

여기서 3번의 내용이 JVM에 해당하므로 중점적으로 다루도록 하겠습니다.

  • Class Loader Sub System
  • Runtime Data Areas
  • Execution Engine

(물론 추가로 Native Method Interface / Library가 있지만 현재 포스팅해서는 제외했습니다.)

Class Loader

클래스 로더는 자바 클래스를 JVM에 동적으로 로드하는 자바 런타임 환경의 일부입니다.

일반적으로 클래스는 요청 시에만 로드를 합니다.

클래스 로더는 자바 컴파일러에 의해 컴파일된 바이트 코드(.class 파일)를 Runtime Data Area로 옮기는 역할을 수행합니다. 이 클래스 로더에서 다음 3가지의 활동을 주로 합니다.

  1. Loading : 클래스 로더는 ".class" 파일을 읽고 그 파일의 바이너리 데이터를 생성해 메서드 영역에 저장
  2. Linking : 확인(verification), 준비(preparation), 해결(선택 사항)을 수행
  3. Initialization : 모든 static 변수가 코드 및 (존재할 경우) static 블록에 정의된 값으로 할당. 이것은 클래스에서 위에서 아래로, 클래스 계층에서 부모에서 자식으로 실행

Linking 부분에서 확인, 준비, 해결의 의미는 다음과 같습니다.

확인(Verification) : .class 파일의 정확성을 보장. 이 파일이 올바른 형식으로 지정되고 유효한 컴파일러에서 생성되었는지 여부를 확인. 확인에 실패하면 런타임 예외가 발생. 이 활동은 ByteCodeVerifier에 의해 수행되며, 이 활동이 완료되면 클래스 파일을 컴파일할 준비가 끝난 것을 의미.

준비(Preparation) : 클래스 변수에 메모리를 할당하고 메모리를 기본값으로 초기화하는 것

해결(Resolution) : 유형의 기호 참조(symbolic references)를 직접 참조(direct references)로 바꾸는 과정. 참조된 엔터티를 찾기 위해 메서드 영역을 검색해 수행

JVM은 위임 계층(Delegation-Hierarchy) 원칙에 따라 클래스를 로드합니다.

시스템 클래스 로더는 확장 클래스 로더에 로드 요청을 위임하고, 확장 클래스 로더는 부트 스트랩 클래스 로더에 요청합니다.

부트 스트랩 경로에서 클래스가 발견되면, 그 클래스가 로드되고, 그렇지 않으면 요청이 확장 클래스 로더로

재전송된 다음 시스템 클래스 로더로 전송됩니다.

 

그리고 시스템 클래스 로더가 클래스를 로드하지 못하면 런타임 예외가 발생합니다.

그리고 클래스가 불러와지는 방식에는 로드타임 동적 로딩(load-time dynamic loading)과 런타임 동적 로딩(runtime dynamic loading) 2가지가 있습니다.

  • 로드타임 동적 로딩 : 하나의 클래스를 로딩하는 과정에서 동적으로 필요한(=이와 관련된) 다른 클래스들을 로딩하는 방식.
  • 런타임 동적 로딩 : 객체를 참조하는 순간 동적으로 로딩하는 방식. 객체를 사용한 코드 부분에서 Class.forName()이 실행되기 전까진 메인 메서드가 있는 클래스에서 어떤 클래스를 참조하는지 알 수 없음. 그래서 코드를 실행하는 그 순간에 클래스를 로딩하는 동적인 방식으로 클래스를 로드하는데, 이를 런타임 동적 로딩이라 함

Runtime Data Areas

JVM이 프로그램을 실행하기 위해 OS한테 할당받는 메모리 영역입니다. 이 영역은 목적에 따라 5개의 영역으로 나누어집니다.

  • PC Registers : 쓰레드가 생성될 때마다 생기는 공간. 쓰레드가 어떤 명령을 실행할지에 대한 내용을 기록한다. JVM은 CPU 레지스터로 직접 작동하는 게 아니라 스택 기반(Stacks-base)으로 작동하는데, JVM은 CPU에 직접 지시를 내리지 않고 스택에서 피연산자(Operand)를 뽑아내 별도의 메모리 공간에 저장해 연산한다. 이 때의 메모리 공간이 PC Registers다.
  • Method Area : 프로그램 실행 도중 클래스가 사용되면 JVM은 그 클래스 파일을 읽고 분석해 클래스의 인스턴스 변수, 메서드 코드 등을 Method Area에 저장한다. 이 때 클래스 변수도 이 영역에 함께 생성된다. 프로그램이 실행되면 모든 코드가 저장돼 있는 상태가 아니다. new 키워드로 객체가 생성되기 이전엔 텍스트일 뿐이다. 이 영역에 저장되는 정보들은 아래와 같다.
    • 타입 정보 : 타입은 클래스와 인터페이스를 통칭하는 것이다
      • 타입의 전체 이름(패키지명+클래스명)
      • 타입에 속한 하위 클래스들의 전체 이름
      • 타입이 클래스 또는 인터페이스인지 여부
      • 타입의 제어자(public, abstract, final 등)
      • 연관된 인터페이스 이름 리스트
    • Runtime Constant Pool : 타입의 모든 상수 정보를 갖고 있음
      • 타입, 필드, 메서드로의 모든 심볼릭 참조 정보
      • Object의 접근 등 모든 참조를 위한 핵심 요소
    • Field Information : 인스턴스 변수
      • 필드 타입
      • 필드 제어자(public, private, protected, static, final, volatile, transient)
    • Method Information : 생성자를 포함한 모든 메서드
      • 메서드명, 리턴타입, 파라미터 수와 타입, 제어자 등
    • Class Variable : 클래스 변수는 static 키워드로 선언된 변수를 말한다. 기본형이 아닌 이 변수는 참조 변수만 저장되고 실제 인스턴스는 힙 영역에 저장돼 있다.
  • Heap : 사용자가 관리하는 인스턴스가 생성되는 공간. 객체를 동적 생성하면(=new 키워드 사용) 인스턴스가 힙 영역의 메모리에 할당돼 사용된다. 프로그램은 시작 시 미리 힙 영역을 많이 할당해 놓으며 여기에 인스턴스와 인스턴스 변수가 저장된다. 참조 변수의 경우 포인터가 저장되며, 가비지 컬렉션의 대상이 되는 영역이다.
  • JVM Stacks : 쓰레드 제어를 위해 쓰이는 메모리 영역. 쓰레드가 만들어질 때마다 하나씩 만들어지며 한 쓰레드 당 메서드가 호출되는 순간 메모리를 차지한다. 메서드가 호출되면 메서드와 메서드의 정보들은 스택에 쌓이며, 메서드 호출이 종료될 때 Stack Point에서 제거된다. Method 정보는 해당 메서드의 매개변수, 지역변수, 임시변수, 메서드를 호출한 곳 등을 말하고 메서드가 종료되면 메모리 공간이 사라진다.

이렇게 수 많은 정보들을 저장하는 영역이라고 요약할 수 있습니다.

Execution Engine

클래스 로더를 통해 JVM 안에 Runtime Data Areas에 놓여진 바이트 코드를 실행하는 영역으로

바이트 코드를 명령어 단위로 읽어 바이트 코드를 JVM이 실행할 수 있도록 컴파일을 해줍니다.

해당 방식은 인터프리터 방식과 JIT(Just In Time)방식으로 나뉘는 데, JVM은 기본적으로 모든 코드를 인터프리터 방식으로 바꿔줍니다. 그러다 반복되는 코드가 있으면 JIT방식으로 명령어를 실행합니다.

참조

https://velog.io/@hygoogi/JVM-%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%9E%91%EB%8F%99-%EC%9B%90%EB%A6%AC

https://inspirit941.tistory.com/296

 

 

이상입니다.