Java와 JVM

ugly duckling·2022년 5월 10일
0

Java

목록 보기
1/4

JVM : Java Virtual Machine

자바는 OS에 종속받지 않는 특징이 있다. 이러한 특징은 JVM을 통해 이루어진다.

참고사항
JRE(Java Runtime Environment) : 자바 애플리케이션 실행에 필요한 최소 환경 (사용자용)
JDK(Java Development Kit) : 자바 애플리케이션을 개발하고 실행하는데 필요한 환경 (개발자용)

어떻게 그런게 가능한거냐???

java 프로그램이 실행되는 과정을 먼저 살펴보자

  1. Java Source Code를 작성한다.(Java 프로그래밍 언어로 작성된 .java 파일이다.)

  2. Java Source Code를 컴파일 하여 Class파일을 생성한다. 여기서 .Class 파일은 Java Byte code다. 이 Java Bytecode는 JVM에서 실행이 되며 이 때문에 JVM만 있으면 OS에 영향을 받지 않고 실행될 수 있다. C/C++의 컴파일러들이 목적파일(Object File)을 만들어 기계어로 번역을 하는 것과는 다르다.

  3. Class Loader에서 Class 파일을 JVM에 로딩한다. 아래 그림의 초록색 부분에 해당

  4. 로딩한 Java Bytecode를 런타임 데이터 영역에 배치한다.

  5. 배치된 Bytecode를 Execution Engine을 통해 Java Bytecode를 명령어 단위로 읽어서 실행한다.

1~5의 과정을 간단하게 나열했지만 사실 각 과정 하나하나가 중요하며 내용이 방대하다. 또한 JVM이 하드웨어나 OS에 따라 달라질 수 있기 때문에 적절한 JVM 버전을 선택해야 한다.

JVM의 특징

  • 스택 기반의 가상 머신 :
    일반적인 하드웨어가 레지스터를 기반으로 동작하는 데 비해 JVM은 스택을 기반으로 동작한다.
    -> 프로세서에 독립적이다.

  • 심볼릭 레퍼런스 :
    기본 자료형을 제외한 모든 타입을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
    -> 심볼릭 레퍼런스(이름을 통한 참조값을 통해)를 런타임 시점에 메모리에 실제로 존재하는 물리적인 주소로 대체하는 linking 작업을 수행한다.

  • 가비지 컬렉션 :
    메모리에 할당된 인스턴스는 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 파괴된다.
    -> 메모리 관리를 알아서 해준다.

  • 자료형의 명확한 정의를 통해 플랫폼 독립성 보장 : C/C++과 달리 기본 자료형이 명확하게 정의되어 있어 호환성을 유지하고 플랫폼 독립성을 보장한다. C/C++은 OS에 따라 컴파일러에 따라 자료형의 크기가 조금씩 다르다.

JVM의 구조

참고 : https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973


JVM은 host OS의 RAM에 할당되어 있으시다가 클래스를 처음 참조할 때 runtime에 Class Loader를 이용해 클래스 파일을 메모리에 할당하고 초기화 한다. 이를 dynamic class loading이라 한다. 다음의 그림을 보면 JVM이 어떻게 할당되는 것인지 이해가 쉽다.
참고 : https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973

그리고 host OS의 메모리에 할당되어 있는 JVM은 런타임 데이터와 컴파일된 코드를 저장하기 위해 다시 JVM 내부에 별도의 메모리 공간(Heap, Non-Heap)을 가진다.

1. Class Loader Subsystem

1.1 Loading

컴파일된 클래스 파일들을 런타임에 메모리에 로딩한다. 일반적으로 static main()메소드 선언이 있는 클래스인 Main class부터 로딩을 시작하며 아래의 경우에 실행 중인 클래스의 클래스 참조를 따라 로딩을 수행한다.

  • 바이트코드가 클래스에 대한 정적 참조를 만들 때 (System.out)
  • 바이트코드가 클래스 객체를 생성할 때 (new 연산자를 통해 새로운 객체를 생성할 때)

클래스 로더는 4가지 원칙을 따른다.

  • 가시성 제한 원칙 : 자식 클래스 로더는 부모 클래스 로더가 로드한 클래스를 찾을 수 있지만, 부모 클래스 로더는 자식 클래스 로더가 로딩한 클래스를 찾을 수 없다.

  • 유일성 원칙 : 부모에 의해 로딩된 클래스가 다시 자식 클래스 로더에 의해 로딩되지 않도록 한다. 이는 중복된 클래스 로드가 발생하지 않도록 하는 것이다.

  • 위임 계층 원칙 : 가시성 제한 원칙과 유일성 원칙을 만족시키기 위해서 JVM은 각 클래스 로딩 요청마다 클래스 로더를 선택하는 위임 계층을 따른다. 위의 java 프로그램이 실행되는 과정 3번, JVM Architecture 이미지의 초록색 부분의 Loading 부분을 보면 Application Class Loader는 가장 낮은 자식 레벨부터 수신된 클래스 로딩 요청을 Extension Class Loader에 위임하고 Extension Class Loader는 다시 Bootstrap Class Loader에게 요청을 위임한다.

  • 언로드 불가 : 클래스 로더는 클래스를 로드할 수 있지만 로드된 클래스를 언로드할 수 없다. 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

위에 언급한 3가지 클래스 로더 이외에 프로그래머가 사용자 정의 클래스 로더를 직접 만들 수도 있다. 또한 각 클래스 로더에는 로드된 클래스를 저장하는 네임스페이스가 존재한다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. FQCN이 같아도 네임스페이스가 다르면 당연히 다른 클래스 로더가 로드한 클래스 이므로 다른 클래스로 간주된다.

링크는 로드된 클래스나 인터페이스, 해당 클래스나 인터페이스의 수퍼클래스나 수퍼인터페이스를 확인하고 준비하며 필요한 경우 각 클래스의 변수들을 초기화한다. 위의 java 프로그램이 실행되는 과정 3번, JVM Architecture 이미지의 초록색 부분의 Linking 부분

링크의 속성

  • 클래스 또는 인터페이스는 링크 전에 완전히 로드되어야 한다.
  • 클래스나 인터페이스는 초기화 전에 Verifying과 Preparing이 완료되어야 한다.

링킹은 다음의 3가지 단계로 이루어 진다.

  1. 검증(Verifying) :
    읽어 들인 클래스가 자바 언어 명세 및 JVM 명세대로 구성되어 있는지 검사한다. 클래스 로드의 전 과정에서 가장 까다로운 검사를 수행하는 과정으로 가장 복잡하고 시간이 많이 걸린다. 이로인해 로딩 프로세스를 느리게 하지만 Bytecode를 실행할 때 이러한 검사를 여러번 수행할 필요가 없어 전체 실행이 효율적이게 된다.

  2. 준비(Preparing) :
    클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는데 필요한 데이터 구조를 준비한다.

  3. 분석(Resolving) :
    클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.

1.3 Initialization

Initialization은 클래스 변수들을 적절한 값으로 초기화 한다. 클래스 생성자 호출 및 static initializer를 실행하고 static 필드들을 설정된 값으로 초기화 한다.

2. 런타임 데이터 영역

런타임 데이터 영역은 host OS의 메모리에 할당되어 있는 JVM의 메모리 영역이다. 5개의 영역으로 나눌 수 있다.

PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드마다 하나씩 생성된다.

힙(Heap), 메서드 영역(Method Area)은 모든 스레드가 공유해서 사용한다.

2.1 메소드 영역

JVM이 시작될 때 생성되며, 모든 JVM 쓰레드는 메소드 영역을 공유한다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다.

2.1.1 런타임 상수 풀
클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다.
각 클래스와 인터페이스의 상수, 메서드와 필드에 대한 레퍼런스를 담는 테이블이다.
즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.

2.2 힙 영역

공유 자원으로서 JVM당 1개의 힙 영역을 가진다. 인스턴스 또는 객체, 배열 등을 저장하는 공간으로 가비지 컬렉션 대상이다.

2.3 PC 레지스터

각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다.

2.4 네이티브 메서드 스택

Native Method를 호출하는 코드를 수행하기 위한 스택이다.(자바가 아닌 언어에서 제공되는 메소드) Java Code를 수행하다 JNI(Java Naming Interface)를 호출하여 Java Stack에서 Native Stack으로 dynamic linking을 통해 호출한다.

2.5 스택

출처 : https://medium.com/platform-engineer/understanding-jvm-architecture-22c0ddf09722

JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다. 예외 발생 시 printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.

2.5.1 스택 프레임
JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어
해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다.
각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack),
현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다.
지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.


- 지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다.
0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고,
1부터는 메서드에 전달된 파라미터들이 저장되며,
메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.


- 피연산자 스택: 메서드의 실제 작업 공간이다.
각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고,
다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop).
피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로,
피연산자 스택의 크기도 컴파일 시에 결정된다.

3 Execution Engine

Java Bytecode의 실제 실행이 일어나는 부분으로 런타임 데이터 영역에 할당된 데이터를 읽어서 바이트 코드의 명령을 한 줄씩 실행한다. 다만 한줄 씩 읽는 인터프리터의 경우 인터프리팅 결과의 실행이 느리기 때문에 JIT 컴파일러도 같이 제공하여 실행 엔진은 다음의 2가지 방식을 제공한다.

3.1 Interpreter

인터프리터는 바이트 코드를 해석하고 명령을 하나씩 실행한다. 하나의 메소드를 여러번 호출할 때 매번 인터프리팅을 진행해야 하고 이로인해 성능저하가 발생한다.

3.2 JIT 컴파일러

위의 인터프리터의 단점을 대체할 수 있도록 제공하는 컴파일러이다. Bytecode를 전부 네이티브 코드(기계어 코드)로 컴파일한 뒤, 반복되는 메소드 호출을 빠르게 처리하도록 한다. 다만 java bytecode를 해석하는데 있어 인터프리터보다 많은 시간을 소요하기 때문에 한번만 실행되는 코드 세그먼트의 경우 인터프리터가 유리하다.

3.3 GC

Garbage Collection은 더이상 참조되지 않는 객체를 메모리에서 해제하여 메모리 관리를 수행한다.

참고 자료

https://d2.naver.com/helloworld/1230
https://medium.com/platform-engineer/understanding-java-memory-model-1d0863f6d973
https://medium.com/platform-engineer/understanding-jvm-architecture-22c0ddf09722

0개의 댓글