자바는 OS에 독립적인 특징을 가지고 있다.
JVM이 OS와 프로그램의 사이에서 기계어로 행석앻주는 역할을 하기 때문이다.

 

JVM JAVA Virtual Machine

컴파일된 바이트 코드를 기계가 이해할 수 있는 기계어로 변환

스택기반의 가상 머신 

메모리 관리와 GC를 수행

 

자바 코드의 실행 과정

1. 개발자가 자바코드를 작성한다. 

2. ( .java ) 인 파일을 자바 컴파일러를 통해 자바 바이트 코드로 컴파일한다.

3. 컴파일 된 바이트 코드를 JVM의 Class Loader에 전달한다. 

4. Class Loader는 동적 로딩을 통해 필요 클래스들을 로딩 및 링크하여 Runtime Data Area에 올린다. (JVM의 메모리)

5. 실행 엔진은 JVM메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행한다.

 

 

 

 

 

JVM
Class Loader + Runtime Data Areas + Execution Engine

 

 

 

 

Class Loader 특징

1. 계층 구조 

클래스 로더는 여러 클래스 로더끼리 부모-자식 관계를 이루어 계층적인 구조로 되어 있다.

 

  • 부트스트랩 클래스 로더 Bootstrap Class Loader
    • 최상위 클래스로더, 유일하게 JAVA가 아닌 네이티브 코드로 구현
    • JVM이 실행될 때 같이 메모리에 올라감
    • Object 클래스를 비롯하여 JAVA API들을 로드함

 

  • 익스텐션 클래스 로더 Extension Class Loader
    • 기본 JAVA API를 제외한 확장 클래스들을 로드함 - 다양한 보안 확장 기능 로드

 

  • 시스템 클래스 로더 System Class Loader
    • (부트 스트랩과 익스텐션 클래스로더가 JVM 자체의 구성 요소들을 로드한다) 시스템 클래스 로더는 어플리케이션의 클래스들을 로드함
    • 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드함

 

  • 사용자 정의 클래스 로더 User-Defined Class Loader
    • 어플리케이션 사용자가 직접 코드 상에서 생성하여 사용하는 클래스 로더
    • WAS와 같은 프레임워크는 서로 독립적으로 동작하게 하기 위해서 이를 위해 사용자 정의 클래스 로더들을 사용하여 클래스 로더의 위임 모델을 통해 어플리케이션의 독립성을 보장함

 

2. 위임모델 

처음 바이트 코드를 넘겨받은 클래스로더.

필요한 클래스를 로드할 때 혹은 실행 엔진에서 명령어 단위로 바이트 코드를 실행하다가 처음으로 참조하는 클래스에 대해 클래스 로더에게 로드를 요청할 때.

로드를 요청받은 클래스 로더는 다음 순서대로 요청받은 클래스가 있는지 확인함

  1. 클래스 로더 캐시
  2. 상위 클래스 로더
  3. 자기 자신

이전에 로드된 클래스인지 클래스 로더 캐시를 확인한다.

없다면 상위 클래스 로더를 하나씩 거슬러 올라가며 확인하는데, 이 때 올라가는 도중에 클래스를 발견하더라도 부트 스트랩 클래스 로더(최상단 로더)까지 확인을 해서 부트 스트랩 클래스 로더에도 해당 클래스가 존재한다면 그 클래스를 로드하게 된다.

 

 

3. 가시성 제한

클래스 로더가 클래스 로드를 요청받았을 때 위임 모델에 의해서 클래스 로더 캐시를 확인하고 없으면 상위 클래스 로더를 확인한다. 이때 하위 클래스는 확인이 불가능한 특성이 바로 가시성 제한이다.

 

 

4. 언로드 불가

클래스를 로드하는 것은 가능하지만 그 반대로 unload하는 것은 불가능하다.

 

5. 이름 공간

네임 스페이스는 각 클래스 로더들이 가지고 있는 공간으로 로드된 클래스를 보관하는 공간

위임 모델을 통해서 상위 클래스 로더들을 확인해야할 때 확인하는 공간이 바로 이름 공간

네임 스페이스에 보관되는 기준은 FQCN Fully Qualified Class Name 으로 패키지 명까지 포함되어있는 식별자를 뜻한다.

 

각각의 클래스 로더가 각자 네임 스페이스를 가지고 있기 때문에 FQCN이 같은 클래스라도 네임스페이스가 다르면 다른 클래스로 갖주한다. ( 이 기능을 통해 로드한 클래스 로더를 제거하고 언로드의 효과를 줄 수 있다. )

 

 

클래스 로드 과정 

 

1. 로드 - 클래스 파일을 가져와서 JVM의 메모리에 로드한다.

2. 검증 - 클래스 로드 전 과정 중에서 가장 복잡하고 시간이 많이 걸리는 과정으로, 읽어들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.

3. 준비 - 클래스가 필요로 하는 메모리를 할당한다. 

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

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

 

 

 

 

 

 

런타임 데이터 영역 Runtime Data Area

JVM이 OS 위에서 실행되면서 할당받는 메모리 영역이 바로 런타임 데이터 영역이다.

이 영역은 크게 5가지, 세분화시 6가지로 나눠 볼 수 있다. 

PC 레지스터, JVM 스택, 네이티브 메서드 스택은 스레드마다 하나씩 생성되고 

힙, 메서드 영역은 모든 스레드가 공유해서 사용된다.

 

 

  • PC 레지스터 
    • Program Counter 레지스터는 현재 수행 중인 명령의 주소를 가지며 스레드가 시작될 때 생성되며, 각 스레드마다 하나씩 존재함
  • JVM 스택 
    • 스택 프레임이라는 구조체를 저장하는 스택
    • 예외 발생 시 printStackTrace( ) 메서드로 보여주는 Stack Trace의 각 라인 하나가 스택 프레임을 표현함
    • 스레드가 시작될 때 생성되며 각 스레드마다 하나씩 존재함
  • 네이티브 메서드 스택
    • 자바 외의 언어로 작성된 네이티브 코드를 위한 스택
    • JAVA Native Interface를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택 생성
    • 인스턴스 또는 객체를 저장하는 공간
    • Garbage Collection의 대상
    • JVM 성능 등의 이슈에서 가장 많이 언급되는 공간
    • 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더들의 재량
  • 메서드 영역 
    • 모든 스레드가 공유하는 영역
    • JVM이 시작될 때 생성된다.
    • JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드에 대한 정보, static 변수, 메서드의 바이트 코드들을 보관함
  • 런타임 상수 풀
    • JVM 동작에서 가장 핵심적인 역할을 수행하는 곳
    • 각 클래스와 인터페이스의 상수 뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블
    • 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.

 

 

 

 

 

실행 엔진 Execution Engine

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

 

바이트 코드와 각 명령어는 1바이트 크기의 OpCode와 추가 피연산자로 이뤄져있음

실행 엔진은 하나의 OpCode를 가져와서 피연산자와 작업을 수행한 다음 OpCode를 수행하는 식으로 동작한다.

 

이 수행 과정에서 실행 엔진은 바이트 코드를 기계가 실행할 수 있는 형태로 변경한는데 두가지 방법을 사용한다.

  • 인터프리터 
    • 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나하나의 해석은 빠르지만 전체적인 실행 속도는 느리다는 단점을 가진다. 
    • JVM 안에서 바이트 코드는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT 컴파일러 Just-In-Time
    • 인터프리터의 단점을 보완하기 위해 도입된 방식
    • 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더이상 인터프리팅 하지 않고 네이티브 코드로 직접 실행하는 방식
    • 전체적인 실행 속도는 인터프리팅방식보다 빠르다 ( 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 캐시에서 바로 꺼내어 실행해 빠르게 수행된다 )
    • 하지만 JIT 컴파일러가 컴파일하는 과정은 바이트 코드를 하나씩 인터프리팅 하는 것보다 훨씬 오래 걸리기 때문에 JVM은 해당 메서드가 얼마나 자주 호출되고 실행되는 지를 체크하고, 일정 기준을 넘었을 때만 JIT 컴파일러를 통해 컴파일 하여 네이티브 코드를 생성한다.

 

 

 


참조

'🔥 > JAVA' 카테고리의 다른 글

[JAVA] 객체 지향 프로그래밍(OOP)이란?  (0) 2021.08.11
[JAVA] 자바가상머신, JVM이란?  (0) 2021.08.11
[JAVA] HashMap과 HashTable의 차이점  (0) 2021.08.04
[JAVA] Hash란?  (0) 2021.08.04

+ Recent posts