JVM에 대해서

계리·2023년 8월 8일
0
post-thumbnail

JVM이란

Java Virtual Machine의 약자로 직역 그대로 자바 가상 기계이다. Java가 OS에 종속되지 않게 중재자 역할을 해주는게 JVM이다.

또한 JVM은 Java코드를 OS가 읽을 수 있도록 해석 해주는 역할도 한다. Java코드 즉 원시코드(.java)를 Java Compiler(.javac)가 컴파일을 해주면 자바 바이트 코드(.class)로 컴파일이 된다.

위 그림과 같이 빨간색으로 표시된 부분이 자바 바이트 코드(.class)로 컴파일된 파일을 JVM이 OS가 읽을 수 있도록 해석 해주는 것이다.

자바 바이트코드는 무엇인가?

위에서 컴파일된 자바 코드를 JVM이 읽을 수 있도록 변환된 코드가 자바 바이트 코드이다.


그러면 바이트코드는 무엇인가?

일단 바이트 코드에 대해 설명을 하자면 바이트 코드는 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다. 하드웨어가 아닌 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다.

바이트 코드는 대부분의 명령 집합이 0개 이상의 매개 변수를 갖는 1바이트 크기의 명령 코드(opcode)였기 때문에 바이트코드라 불리게 되었다.

바이트 코드는 특정 하드웨어에 대한 의존성을 줄이고, 인터프리팅도 쉬운 결과물을 생성하고자 하는 프로그래밍 언어에 의해, 출력 코드의 한 형태로 사용된다. 컴파일되어 만들어진 바이트 코드는 특정 하드웨어의 기계 코드를 만드는 컴파일러의 입력으로 사용되거나, 가상 컴퓨터에서 바로 실행된다.


JVM이 OS에 종속적이지 않게 해줄 수 있는 이유

바이트 코드의 특징인 하드웨어에 대한 의존성이 낮고, 인터프리팅도 쉬운 결과물로 생성해주기 때문에 JVM이 설치만 되어 있다면 어떤 OS에서도 Java를 사용할 수 있게 해주는 것이다.


자바의 단점 - 속도가 느리다

자바의 단점 중 실행속도가 느리다는 것이 있는데 자바 실행속도가 느린 이유를 여기서 알 수 있게 된다. 자바 애플리케이션을 실행 시 JVM을 거쳐서 인터프리터를 해야 하기 때문에 자바의 속도가 느릴 수 밖에 없게 된다.


JVM에 대해서 알아야 하는 이유

자바가 느린 이유가 JVM에 의해 느려지게 된 것을 알게 됐고, JVM을 가지고 자바 성능 튜닝을 하여 자바 애플리케이션의 속도를 향상 시킬 수 있다는 것을 알게 되었고 나중에 자바 성능 튜닝 관련 공부를 할 기회가 생기면 해보면 될 것으로 보인다.


JVM 동작원리


1. 자바 코드(.java)를 Java Compiler(.javac)가 Java 바이트 코드(.class)로 컴파일을 한다.
2. 자바 바이트 코드(.class)를 Class Loader에 전달한다.
3. Class Loader는 동적로딩을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area(JVM의 실질적인 메모리를 할당 받아 관리하는 영역)에 올린다.
4. Runtime Data Area에 로딩된 바이트 코드는 Execution Engine에 해석된다.
5. 이 과정중에 Execution Engine에 의해 Garbage Collector의 작동과 Thread 동기화가 이루어진다.


JVM 구조

크게 5가지로 나뉜다.

  • Class Loader
  • Runtime Data Area
  • Execution Engine
  • Native Method Interface(JNI)
  • Native Method Library

Class Loader

Class Loader는 Byte Code(.class)들을 엮어서 JVM내로 동적으로 load하고 링크를 통해 Runtime Data Area(JVM 내의 메모리 영역)에 배치 해준다.


Class Loader의 3단계 구성

Loading

클래스 파일을 가져와서 JVM의 메모리에 load 한다

  • Bootstrap Class Loader

    1. jre/lib/rt.jar 디렉토리의 핵심 자바 라이브러리를 load한다
    2. 최상위 Class Loader로 유일하게 JAVA가 아닌 네이티브 코드로 구현이 되어있다.
    3. JVM이 실행될 때 같이 메모리에 올라간다.
    4. Object 클래스를 비롯하여 JAVA API들을 load한다.

  • Extension Class Loader

    1. jre/lib/ext 파일에 포함된 클래스 파일을 load한다.
    2. 기본 JAVA API를 제외한 확장 클래스들을 load한다.( 다양한 보안 확장 기능 load)
  • Application Class Loader

    1. 애플리케이션 classpath( 애플리케이션 실행할 때 주는 -classpath옵션 또는 java.class.path 환경 변수에 값에 해당하는 위치)에서 Class를 읽어 load한다.
      • 개발자가 애플리케이션 구동을 위해 직접 작성한 대부분의 클래스는 애플리케이션 클래스 로더에 의해 로딩된다.

Linking

  • Verify(검증)
    Class Load 모든 과정 중에서 가장 복잡하고 시간이 많이 걸리는 과정으로, 읽어들인 클래스가 자바 언어 명세( Java Language Specification ) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다. 확인 실패 시 Runtime Exception이 발생한다.


  • Preparing(준비)
    Class가 필요로 하는 메모리들을 할당한다. 필요한 메모리들은 Class에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조들을 말한다.


  • Resolve
    Class의 상수 풀 내 모든 유형의 기호 참조( Symbolic references )를 직접 참조( Direct references )로 변경한다.

    Symblic Reference : 이름에 대한 참조 / Direct Reference : 실제 메모리 주소에 대한 참조

Initialization

클래스 변수들을 적절한 값으로 초기화한다. ( static 필드들을 설정된 값으로 초기화 등 )


Runtime Data Area

  1. JVM의 메모리 영역이다.
  2. JVM이 운영체제 위에서 실행되며 할당 받는 메모리 영역이다.
  3. Class Loader에서 준비한 데이터들을 보관하는 저장소이기도 하다.

Runtime Data Area는 크게 6가지로 나뉜다.

  • Method Area
  • Heap Area
  • Stack Area
  • PC Register
  • Native Method Stack


Method Area(= Class Area = Static Area)

JVM이 시작될 때 생성되는 공간으로 바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 공간이다.

메서드 영역은 모든 쓰레드가 공유하는 영역이기 때문에 다음과 같은 정보들이 저장된다.

  • Field Information : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
  • Method Information : 메소드 이름, return 타입, 함수 매개변수, 접근 제어자의 정보
  • Type Information : Class 인지 Interface 인지 여부 저장, Type의 속성, Super Class의 이름(Interface 이거나 Object인 경우 제외, Heap에서 관리)

메소드 영역 / 런타임 상수 풀의 사용기간 및 스레드 공유 범위

  • JVM 시작 시 생성
  • 프로그램 종료 시 까지
  • 명시적으로 null 선언

Runtime Constant Pool

  • 메소드 영역에 존재하는 별도의 관리 영역
  • 각 클래스 / 인터페이스 마다 별도의 constant pool 테이블이 존재하는데 클래스 생성할 때 참조해야 할 정보들을 상수로 가지고 있는 영역이다.
  • JVM은 constant pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.

한마디로 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 한다.


Heap Area

힙 영역도 메소드 영역처럼 모든 쓰레드가 공유하는 영역이며 JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다.

new 연산자로 생성하는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다.

메소드 영역에 저장된 클래스만이 생성되어 적재된다.


힙 영역의 사용기간 및 스레드 공유 범위

  • 객체가 더 이상 사용되지 않거나 명시적으로 null을 선언한 시
  • GC(Garbage Collection) 대상

힙 영역에 있는 인스턴스를 핸들링 하기 위해서는 스택 영역에서 힙의 참조 주소를 갖고 있는 객체를 통해서 핸들링 할 수 있다.

만일 참조하는 변수나 필드가 없다면 의미가 없는 객체로 인식해 쓰레기 취급을 하고 JVM은 GC를 실행시켜 힙 영역에서 해당 객체를 자동으로 삭제한다.

이렇게 힙 영역은 GC에 대상이 되는 영역이기도 하다. 그리고 GC를 수행하기 위해 위 사진처럼 세부적으로 5가지 영역으로 나뉜다.

NEW/Young Generation

  • 생명주기가 짧은 객체를 GC 대상으로 하는 영역
    • Eden
      • new를 통해 생성되는 객체가 위치
      • 정기적은 쓰레기 수집 후 살아남은 객체들은 Survivor로 이동
    • Survivor0, 1
      • 살아남은 객체들이 Survivor 순서대로 채워진다

Old Generation

생명주기가 긴 객체를 GC 대상으로 하는 영역. Young Generation에서 살아남은 객체들이 이동


Permanent Generation

  • 생성된 객체들 정보의 주소가 저장되는 공간
  • 클래스로더에 의해 로드되는 클래스, 메소드 등에 대한 메타 정보가 저장되는 영역
  • Reflection을 사용하여 동적으로 Class가 Load되는 경우 사용
    • Reflection
      • 객체를 통해 클래스의 정보를 분석해 내는 프로그래밍 기법
      • 구체적인 타입을 알지 못해도 컴파일된 바이트 코드를 통해 역으로 클래스의 정보를 알아내어 사용

Stack Area

자료구조 스택의 특징인 마지막에 들어간 것이 제일 먼저 나오는 구조(LIFO, Last In First Out 또는 FILO, First In Last Out 가장 먼저 들어간 것이 가장 마지막에 나오는 구조)이고 push, pop 방식으로 사용된다.

스택 영역은 기본 자료형(int, long, boolean 등등)을 생성할 때 가지고 있는 공간으로 임시적으로 사용되는 변수나 정보들을 저장하는 공간이다.

메서드 호출 시 각각의 스택 프레임이 생성되면서 메서드 안에 있는 값들을 저장(push)하고 호출된 메서드의 수행이 끝나면 스택 프레임 별로 삭제(pop)한다.


스택 프레임

위 그림에서 각각의 A, B, C, D, E 라고 표현한 것들이 각각의 스택 프레임이다. 위 그림처럼 메서드가 호출될 때마다 프레임이 만들어지고 현재 실행되고 있는 메서드 상태 정보를 저장하는 곳이다.

메서드가 호출되고 종료가 되면 스택에서 삭제가 된다. 스택 프레임에 쌓이는 데이터들은 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산된 결과 값들이 있다.


데이터 타입에 따라 저장되는 방식이 다르다

힙 영역에서는 "new 연산자로 생성하는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다."

스택 영역은 "기본 자료형(int, long, boolean 등등)을 생성할 때 가지고 있는 공간으로 임시적으로 사용되는 변수나 정보들을 저장하는 공간이다."

즉, 기본 타입의 직접적인 값들은 스택 영역에서 가지고, 참조타입의 변수는 메서드 영역이나 힙 영역의 객체 주소 값을 가진다.


PC Register(Program Counter Register)

PC 레지스터는 쓰레드가 시작될 때 생성되며 현재 수행중인 JVM 명령어 주소를 저장하는 공간이다. JVM 명령어 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행하는지에 대한 기록을 한다.

자바는 OS나 CPU 입장에서는 하나의 프로세스이다. 그래서 기 때문에 JVM의 리소스를 이용해야 해서 PC 레지스터라는 메모리 영역이 필요로 하게 됐다.


Native Method Stack

네이티브 메서드 스택은 바이트 코드가 아닌 기계어로 작성된 프로그램(C, C++, 어셈블리어) 을 실행 시키는 영역, 즉 네이티브 코드로 실행하기 위한 공간이라고 할 수 있다.


수행 과정

  1. 일반적으로 메서드를 실행하게 되면 자바 스택에서 호출하다가 해당 메소드 내부에 네이티브 방식을 사용하는 메소드가 있다면 해당 메서드는 네이티브 스택에서 호출한다.
  2. 그리고 네이티브 메서드가 수행이 끝나면 다시 자바 스택으로 돌아와 다시 작업을 수행한다.

JNI(Java Native Interface)

위 과정처럼 수행을 할 수 있게 된 것 중에 JNI(Java Native Interface)가 있어서 가능한 것도 있다.

JNI는 자바가 다른 언어로 만들어진 애플리케이션과 상호작용 할 수 있도록 만들어진 인터페이스다. JVM이 네이티브 메서드를 적재할 수 있도록 해주는 역할을 한다.


Native Method Library

C, C++ 로 작성된 라이브러리가 필요할 경우 Native Method Library를 이용해 로딩하여 실행한다.


Execution Engine

클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다. 명령어 단위로 실행하는 방식은 두 가지를 혼합해서 사용하는데 두 가지에 대해서도 알아보겠다.


Interpreter(인터프리터)

바이트 코드 명령어를 한 줄씩 읽어서 해석하고 실행한다. JVM안에서 기본적으로 인터프리터 방식으로 실행된다. 바이트 코드 명령어를 한줄 씩 읽고 해석하다 보니 느린 것도 있지만 같은 메서드라도 여러 번 호출하고 해석하고 실행시키는 이유도 있다.


JIT-Compiler(Just In Time Compiler)

인터프리터의 단점을 보완한 방식으로 반복되는 코드를 발견하여 바이트 코드 전체를 컴파일 하여 네이티브 코드 변경하고 이후에는 해당 메서드를 더 이상 인터프리터 하지 않고 캐싱에 저장해두었다가 저장해둔 네이티브 코드로 직접 실행한다.

컴파일된 네이티브 코드를 실행하는 방식이기 때문에 인터프리터 보다 전체적인 속도는 더 빠르다. 하지만 바이트 코드를 네이티브 코드로 변환하면서 많은 비용이 발생하기 때문에 JVM이 기본적으로 인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT-Compiler를 사용하는 방식이다.


가비지 컬렉터(GC, Garbage Collector)

JVM은 가비지 컬렉터를 이용하여 힙 영역에서 사용하지 않는 메모리들을 자동으로 처리해준다. C언어에서는 개발자가 직접 메모리 관리를 해줘야 하지만 자바에서는 개발자가 개발에 집중할 수 있도록 GC가 관리해준다.

Full GC가 발생하는 경우 GC를 제외한 모든 스레드가 중지되기 때문에 장애가 발생할 수 있다.




참고

profile
gyery

0개의 댓글