본문 바로가기
Back-end

JVM

by 신재권 2023. 8. 11.

Java 플랫폼 종속이지 않게 설계한 이유와 장단점

C/C++ 등 컴파일러가 기계어 코드로 변환할 때 프로그램이 실행할 대상 컴퓨터의 CPU와 운영체제 종속적인 코드로 변환됩니다. 그러면 다른 플랫폼에서는 다시 컴파일 하여 플랫폼에 맞도록 코드를 수정해야 합니다. 이를 플랫폼 종속성이라 합니다.

하지만 Java는 플랫폼 종속성을 피하고 “Write Once, Run Anywhere”라는 철학을 지향하여 설계되었습니다.

Java 코드를 한번 작성하면, 어떤 환경의 OS에서라도 JVM만 설치되어 있으면 동일한 실행 환경을 제공하며 동일한 실행 결과를 기대할 수 있습니다.

장점으로는 특정 플랫폼에 종속되지 않고 애플리케이션 배포가 가능하고, 코드를 한 번만 작성하면 다양한 플랫폼에서 동일한 실행 결과를 얻을 수 있기 때문에 유지 보수 용이성이 향상됩니다.

단점으로는 C/C++ 에 비해 성능이 떨어집니다.

어떻게 플랫폼 독립성을 보장하나요?

자바 플랫폼 독립성을 가능하게 하는 것은 자바 가상 기계와 바이트 코드 때문입니다.

바이트 코드는 JVM 에서만 실행되는 기계어로 어떤 CPU와도 관계없는 코드입니다.

자바 컴파일러는 자바 소스 프로그램을 컴파일 하여 바이트 코드로 된 클래스 파일을 생성합니다.

클래스 파일은 JVM만 있으면 서로 다른 플랫폼에서 자바 프로그램이 실행되는 동안 동일한 환경을 제공합니다.

즉, JVM이 플랫폼 종속성을 보장해 줍니다.

자바 컴파일 과정

  1. 개발자가 자바 소스코드를 작성합니다.
  2. 자바 컴파일러가 자바 소스를 컴파일 합니다. 이때 나오는 파일은 바이트 코드 파일로, 컴퓨터는 이해하지 못하고, JVM만 이해할 수 있는 코드입니다.
  3. 바이트 코드를 JVM의 클래스 로더에게 전달합니다.
  4. 클래스 로더는 동적 로딩을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역, 즉 JVM의 메모리에 올립니다.
  5. 실행 엔진은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행합니다.

JVM 구조

클래스 로더

클래스 로더는 클래스파일을 JVM으로 로드하는 역할을 합니다.

클래스 로더는 클래스를 필요할 때 동적으로 로드하며, 로드된 클래스들은 메모리에 저장되어 나중에 사용됩니다.

  1. 로드 : 클래스로더는 클래스 파일을 로드하여 JVM의 메모리로 가져옵니다.
  2. 링크 : 클래스로더는 클래스 파일을 링크 단계를 통해 클래스의 레퍼런스를 해석하고, 필요한 리소스를 연결합니다. 이 때 상수 풀을 구축하고 메서드와 필드에 대한 레퍼런스를 설정합니다.
  3. 초기화 : 클래스 로더는 클래스를 초기화 하는 단계를 수행합니다. 이 단계에서 static 변수의 초기 값을 설정하거나 static 블록 내의 코드를 실행합니다.

클래스 로더의 주요 계층

  • Bootstrap Class Loader
    • 촤상위 클래스로더로 네이티브 코드로 구현되어 있습니다.
    • Java API 중 가장 기본적인 부분을 로딩 합니다.(java.lang)
    • 자바 확장 라이브러리나 애플리케이션 클래스와 관련이 없는 시스템 클래스를 로드합니다.
    • JVM 시작 시에 가장 먼저 실행되는 클래스 로더입니다.
  • Extension Class Loader
    • java의 확장 클래스들을 로드합니다.(javax)
    • bootstrap 클래스 로더의 하위 계층에 속하며, 사용자 코드가 위치한 클래스들에 대한 로딩을 담당하지 않습니다.
  • Application Class Loader(System Class Loader)
    • 사용자가 작성한 클래스들을 로드합니다. 애플리케이션 클래스 경로에 있는 클래스들을 로드합니다.
    • 클래스 로딩 요청은 이 계층을 통해 처리됩니다.

클래스 로딩 요청 처리

  1. 클래스 로더는 해당 클래스가 이미 로드 되었는지 검사합니다.
  2. 해당 클래스를 찾아 로드합니다. 클래스 로더 계층을 순차적으로 탐색합니다.
  3. 클래스 파일을 JVM 내부에서 사용할 수 있는 형식으로 변환하여 클래스 정보를 메모리에 저장합니다.
  4. 링크와 초기화 단계를 수행하여 클래스와 관련된 리소스 및 정보를 설정합니다.

동적 클래스 로딩

자바의 클래스 로딩은 클래스 참조 시점에 JVM에 코드가 링크되고, 실제 런타임 시점에 로딩되는 동적 로딩을 거친다.

  • 로드 타임 동적 로딩
    • JVM이 시작되고 부트스트랩 클래스 로더가 생성된 이후 모든 클래스가 상속받고 있는 Object 클래스를 읽어온다.
    • 클래스 로더는 해당 클래스를 로딩하기 위해 class 파일을 읽는다.
    • 클래스를 로딩하는 과정에서 필요한 클래스인 부가적인 클래스들을 로딩한다.
    • 즉, 하나의 클래스를 로딩하는 과정에서 동적으로 다른 클래스를 로딩한다.
  • 런타임 동적 로딩
    • Class.forName() 메서드가 실행되기 전까지는 어떤 클래스를 참조하고 있는지 알수 없다.
    • 즉, Class.forName()을 호출하는 순간에 인자로 전달된 클래스를 로딩한다.
    • 클래스를 로딩할 때가 아닌, 코드를 실행하는 순간에 클래스를 로딩한다.

클래스 로딩 시점

  • 클래스의 인스턴스 생성
  • 클래스의 정적 변수 사용(final 키워드 x)
  • 클래스의 정적 메서드 호출

클래스 로딩 x

  • 클래스에 접근하지 않을 때
  • 클래스의 정적 변수 사용(final 키워드 o)

static 블록, 멤버 변수초기화 시점

  • 클래스의 인스턴스 생성
  • 클래스의 정적 변수 사용(final 키워드 x)
  • 클래스의 정적 변수 할당
  • 클래스의 정적 메서드 호출

초기화 진행 순서

  • 정적 블록
  • 정적 변수
  • 생성자

한 번만 클래스가 로딩됨을 보장

JVM에 클래스가 로딩되고 초기화 될 때는 순차적으로 동작함을 보장한다.

멀티 스레드 환경에서 여러 개의 스레드가 동시에 클래스가 로딩하려고 하면 오직 한 개의 클래스만 로딩된다.

런타임 데이터 영역

메서드 영역

클래스 로더에 의해 로딩된 클래스 들의 static 변수, 상수, 메서드의 코드 등이 저장되는 영역 입니다.

상수는 런타임 상수 풀에 저장됩니다.

JVM이 시작될 때 생성되며, 모든 스레드가 공유하는 영역 입니다.

즉, 클래스 파일의 바이트 코드가 로드되는 곳 입니다.

클래스 별로 각 정보들이 관리됩니다.

PermGen(Permanent Generation) 영역 - 메서드 영역 - 자바 8 이전

클래스 메타 데이터와 관련된 정보를 저장하는 영역

로딩된 클래스의 구조, 메서드 정보, 정적 변수, 상수 등이 저장된다.

과거 static 변수들에 의해 예상치못한 메타데이터 영역의 오류를 발생시켰다.

  • Class Meta Data
  • Method Meta Data
  • Static Object Variable

MetaSpace 영역

PermGen 영역을 대체한다.

Metaspace는 native memory를 사용하기 때문에 메모리가 부족할 경우 이를 자동으로 눌려준다.

예상치 못한 메타데이터 영역의 오류를 방지하기 위해 Native Memory 영역에서 관리하게 되었다.

즉, JVM이 아닌 OS에서 관리한다.

static object variable는 힙 영역으로 옮겨져 GC의 대상이 될 수 있다.

힙 영역

동적으로 생성된 객체와 배열이 저장되는 영역입니다.

프로그램에서 new 키워드를 사용하여 생성된 객체들이 힙 영역에 저장되며, GC의 대상이 됩니다.

힙 영역은 모든 스레드가 공유하는 메모리 영역 입니다.

스택 영역

각 스레드마다 별도로 생성되는 영역으로, 메서드 호출 시 지역 변수, 메서드 호출 스택 정보가 저장됩니다.

메서드 호출이 발생할 때 마다 스택 프레임이 생성되어 스택에 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거 됩니다.

PC 레지스터

각 스레드마다 현재 실행중인 JVM 명령어의 주소를 가리키는 정보가 저장되는 영역입니다.

스레드가 다른 메서드를 호출하거나 스레드 전환(context switch)이 발생하면 PC 레지스터의 값도 변경됩니다.

즉, JVM이 실행하고 있는 현재 위치를 저장하는 역할을 합니다.

만약 네이티브 언어의 메서드를 수행하고 있다며 undefined 상태가 됩니다. 자바에서는 두 경우를 따로 처리하기 때문입니다.

네이티브 메서드 스택

각 스레드 별로 생성되며, 자바 코드가 아닌, 네이티브 코드를 실행하기 위한 스택 영역입니다.

JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택이 생성됩니다.

네이티브 메서드 호출 시 사용되며, 스택 프레임이 생성됩니다.

네이티브 메서드는 자바 코드와 네이티브 코드 간의 인터페이스 역할을 합니다.

네이티브 메서드 종료 시 스택 프레임이 제거됩니다.

실행 엔진

실행엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.

인터프리터 방식

  • 바이트코드를 읽어와서 직접 해석하여 실행합니다.
  • 바이트코드를 매번 해석해야 하므로 실행속도가 상대적으로 느립니다.
  • 주로 프로그램이 시작될때나 간단한 작업을 수행할 때 사용됩니다.

JIT 컴파일러 방식

  • 바이트코드를 런타임에 기계어로 변환하여 실행합니다.
  • 초기 실행 시에는 바이트코드 해석기를 사용하여 실행 속도를 빠르게 시작합니다.
  • 반복적으로 실행되는 코드는 JIT 컴파일러에 의해 기계어로 변환되어 캐시에 저장되며, 이후 캐시에 있는 코드가 직접 실행되어 실행속도가 향상됩니다.
  • 즉, 인터프리터의 속도 문제를 해결하기 위해 디자인 되었다.
  • 컴파일 임계치는 JVM 내에 있는 메서드 호출 횟수 + 메서드가 루프를 빠져나오기까지 회전한 횟수를 더해 판단하는데, 임계치가 일정 횟수에 도달한 코드는 특정 큐에 들어가 컴파일 스레드에 의해 컴파일 되기를 기다리고,

바이트 코드 해석 방식

바이트코드는 각 명령어는 1바이트 크기의 OpCode와 추가 피연산자로 이루어져 있고, 실행 엔진은 OpCode를 가져와서 피연산자와 작업을 수행한 다음, 그 다음 OpCode를 수행하는 식으로 동작한다.

Heap 영역

힙 영역은 크게 Young 영역과 Old 영역으로 구분된다.

Young 영역은 Eden, survivor 0, survivor 1 영역으로 나눌 수 있다.

Young 영역 : 생명 주기가 짧은 객체를 GC 대상으로 하는 영역입니다.

Eden 영역 : new를 통해 새로 생성된 객체가 위치하고, GC 이후 살아남은 객체는 Survivor로 이동하게 됩니다.

Survivor의 각 영역이 채워지게 되면, 살아남은 객체는 비워진 Survivor로 순차적으로 이동하게 됩니다.

Old 영역은 생명주기가 긴 객체를 GC 대상으로 하는 영역입니다. Young 영역에서 마지막까지 살아남은 객체가 이동되는 공간입니다.

'Back-end' 카테고리의 다른 글

접근 지정자  (0) 2023.08.13
GC  (0) 2023.08.12
2023.08.08 TIL  (0) 2023.08.09
2023.08.07 TIL  (0) 2023.08.07
2023.08.06 TIL  (0) 2023.08.07