자바는 OS에 독립적인 특징을 가지고 있다.
JVM이 OS와 프로그램 사이에서 기계어로 해석해주는 역할을 하기 때문이다.
Java의 탄생배경
Java는 썬 마이크로시스템즈의 제임스 고슬링 이라는 사람과 다른 연구원들이 개발한 프로그래밍 언어입니다. 1991년 그린 프로젝트(Green Project)라는 이름으로 시작해 1995년 발표가 되었다고합니다 !
처음에는 가전제품 내에 탑재해 동작하는 프로그램을 개발했는데 아이러니하게도 현재는 웹 어플리케이션 개발에 가장 많이 사용하는 언어 중에 하나가 되어있죠 ㅎㅎ
소문에 의하면 제임스 고슬링이라는 사람이 가전제품 내에 동작하는 프로그램을 개발하려고 하는데 그 당시에는 유닉스 기반의 배경을 가지고 있었기 때문에 사용하던 프로그래밍 언어 C/C++의 특성상 여러 하드웨어를 커버하기에는 같은 기능의 소스를 각 하드웨어에 맞게 작성해야하는 번거로움이 있어서 Java를 개발하게 되었다고합니다.
Java의 가장 큰 특징 중 하나가 바로 어느 플랫폼, 한마디로 어느 하드웨어(CPU)던, 어느 운영체제(OS)이던 상관없이 컴파일된 코드(바이트 코드)가 독립적이라는 점입니다.
다시 말해 어느 플랫폼이든 작성한 소스를 변경할 필요 없이 다 실행시킬 수 있다는 것입니다. 바로 이 점이 웹 어플리케이션의 특성과 맞아 떨어져 폭발적인 인기와 함께 현재 웹 어플리케이션 개발에 가장 많이 사용되는 언어 중에 하나가 되었습니다.
이러한 특징을 구현하기 위해서는 해당 게시글의 주제인 JVM(Java Virtual Machine)이 필요합니다. JVM은 단순하게 말하면 컴파일된 코드(바이트 코드)를 실행시켜주는 가상의 컴퓨터라고 생각하시면 아래 글을 이해하기 쉬우실꺼에요 ! (JVM은 H/W와 OS 위에서 실행되기 때문에 JVM 자체는 플랫폼에 종속적 즉, 플랫폼에 따라 호환되는 JVM을 실행시켜줘야합니다.)
Java 코드 실행과정
위 이미지는 JVM에게 코드가 전달되기 까지의 과정를 표현해 놓은 이미지입니다. 어떤식으로 돌아가는지 단계별로 확인해보겠습니다 !
① 작성한 자바소스(Java Source), 즉 확장자가 .java인 파일을 자바 컴파일러(Java Compiler)를 통해 자바 바이트 코드(Java Byte Code)로 컴파일합니다.
② 컴파일된 바이트 코드를 JVM의 클래스로더(Class Loader)에게 전달합니다.
③ 클래스로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올립니다.
④ 실행엔진(Execution Engine)은 JVM메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와 실행합니다.
JVM 동작원리 및 기본개념
위의 이미지는 *JRE 내부에 있는 JVM입니다.
* JRE란 ? JRE은 Java Runtime enviroment의 약자로 자바 실행 환경이라는 의미를 가지고 있습니다.
JVM이 자바 프로그램을 실행시킬 때 반드시 필요한 라이브러리 및 기타 필수 파일을 가지고 있습니다.
클래스 로더(Class loader)
클래스 로더의 특징은 계층구조, 위임모델, 가시성 제한, 언로드(Unload) 불가, 이름공간(Name Space) 크게 5가지로 나눌 수 있습니다 !하나하나 살펴보겠습니다 !
계층구조
클래스 로더는 단순하게 하나로 이루어져 있지 않습니다. 위의 이미지처럼 여러 클래스 로더끼리 부모-자식 관계를 이루고 있어서 계층적인 구조로 되어있고, 각 클래스로더들을 정리하자면 다음과 같습니다 !
부트스트랩 클래스 로더(Bootstrap Class Loader)
- 최상위 클래스로더로 유일하게 Java가 아닌 네이티브 코드로 구현 되어있습니다.
- JVM이 실행될 때 같이 메모리에 올라갑니다.
- Object 클래스를 비롯하여 Java API들을 로드합니다.
익스텐션 클래스 로더(Extansion Class Loader)
- 기본 Java API를 제외한 확장 클래스들을 로드합니다. (다양한 보안 확장기능 로드)
시스템 클래스 로더(System Class Loader)
- 부트스트랩과 익스텐션 클래스로더가 JVM 자체의 구성요소들을 로드한다면, 시스템 클래스 로더는 어플리케이션의 클래스들을 로드합니다.
- 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드합니다.
사용자 정의 클래스 로더(User-Defined Class Loader)
- 어플리케이션 사용자가 직접 코드상에서 생성하여 사용하는 클래스로더입니다.
웹 어플리케이션 서버(Web Application Server : WAS)와 같은 프레임 워크는 웹 어플리케이션, 엔터프라이즈 어플리케이션이 서로 독립적으로 동작하게 하기 위해서 사용자 정의 클래스 로더들을 사용하여 클래스 로더의 위임 모델을 통해 어플리케이션의 독립성을 보장한다고합니다.
따라서 ! WAS의 클래스 로더 구조는 WAS벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다고합니다.
위임모델
위임모델이란 처음 바이트 코드를 넘겨받은 클래스 로더가 필요한 클래스를 로드할 때 혹은 실행엔진에서 명령어 단위로 바이트 코드를 실행하다가 처음으로 참조하는 클래스에 대해 클래스 로더에게 로드를 요청할 때 로드를 요청받은 클래스 로더는 다음 순서대로 요청 받은 클래스가 있는지 확인합니다.
클래스 로더 캐시 -> 상위 클래스 로더 -> 자기자신
이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 하나씩 거슬러 올라가며 확인하는데 이 때 중요한 점은 올라가는 도중에 클래스를 발견하더라고 부트스트랩 클래스 로더까지 확인을 해서 부트스트랩 클래스 로더에도 해당 클래스가 존재하면 부트스트랩 클래스 로더에 있는 클래스를 로드한다는 점입니다.
이러한 특성으로 인해 아키텍처를 구성하는 수준의 개발자라면 JVM에 대한 지식이 꼭 ! 필요하다고합니다 ! (IBM에서 만든 어떤 WAS는 옵션을 통해 중간에 클래스를 발견하면 부트스트랩 클래스 로더까지 올라가지 않도록 할 수 있다고합니다.)
마지막으로 부트스트랩 클래스 로더에도 해당 클래스가 없으면 로드를 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는 것으로 마무리합니다. (파일 시스템에서도 찾지 못하면 클래스를 찾기 못했다는 예외가 발생할 것 입니다 !)
가시성 제한
앞에 두 특징보다는 조금 간단한 특징인데요, 클래스 로더가 클래스 로드를 요청 받았을 때 위임 모델에 의해서 클래스 로더 캐시를 확인하고 없으면 상위 클래스 로더를 확인하는데 이 때 하위 클래스 로더에 있는 클래스는 확인이 불가능한 특성이 바로 가시성 제한입니다 !
언로드(Unload) 불가
언로드 불가 역시 간단한 개념으로 말 그대로 클래스를 로드하는 것은 가능하지만 반대로 언로드(Unload)하는 것은 불가능하다는 특성입니다 !
이름공간(Name Space)
네임스페이스란 각 클래스 로더들이 가지고 있는 공간으로써 로드된 클래스를 보관하는 공간입니다. 클래스를 로드할 때 위임모델을 통해 상위 클래스 로더들을 확인하는데 그 때 확인하는 공간이 바로 네입스페이스입니다. 네임스페이스에 보관하는 기준은 FQCN(Full Qualified Class Name)을 기준으로 보관되는데 FQCN이란 패키지명까지 포함되어있는 식별자를 뜻합니다.
각각의 클래스 로더가 각자 네임스페이스를 가지고 있기 때문에 패키지명까지 같은 즉, FQCN이 같은 클래스라도 네임스페이스가 다르면 다른 클래스로 간주하는 것입니다.
클래스 로드 과정
클래스 로더가 아직 로드되지 않은 클래스를 로드하는 과정을 살펴보겠습니다 !
① 로드 : 클래스 파일을 가져와서 JVM의 메모리에 로드합니다.
② 검증 : 클래스 로드 전 과정 중에서 가장 복잡하고 시간이 많이 걸리는 과정으로 읽어들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 구성되어있는지 검사합니다.
③ 준비 : 클래스가 필요로 하는 메모리를 할당합니다. 필요한 메모리란 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조들 등을 말합니다.
④ 분석 : 클래스의 상수 풀 내 모든 *심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경합니다.
⑤ 초기화 : 클래스 변수들을 적절한 값으로 초기화합니다. (static 필드들을 설정된 값으로 초기화 등)
*심볼릭 레퍼런스란 ? 기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아닌 심볼릭 레퍼런스를 통해 참조한다.
런타임 데이터 영역(Runtime Data Area)
JVM이 OS위에서 실행되면서 할당받는 메모리 영역이 바로 런타임 데이터 영역(Runtime Data Area)입니다. 이 영역은 크게 5가지, 세분화하면 6가지 영역으로 나눌 수 있습니다 !
이 중 PC Register, JVM Stack, Native Method Stack은 쓰레드(Thread)마다 하나씩 생성되고 Heap, Method Area는 모든 쓰레드가 용유해서 사용됩니다.
PC Register
PC Register는 현재 수행 중인 명령의 주소를 가지며 쓰레드가 시작될 때 마다 생성되어 각 쓰레드마다 하나씩 존재합니다.
JVM Stack
스택프레임(Stack Frame)이라는 구조체를 저장하는 스택입니다. 예외 발생 시 printStackTrace() 메서드로 보여주는 Stack Trace의 각 라인 하나가 스택 프레임을 표현합니다. JVM 스택 역시 Pc Register와 마찬가지로 쓰레드가 실행될 때 마다 생성되며, 각 쓰레드마다 하나씩 존재합니다.
Native method Stack
Java 외의 언어로 작성된 네이티브 코드를 위한 스택입니다. JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택이 생성됩니다.
Heap
인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션(Garbage Collection)의 대상입니다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간입니다. 힙 구성 방식이나 컬렉션 방법 등은 *JVM 벤더들의 재량입니다.
Method area
모든 쓰레드가 공유하는 영역으로 JVM이 시작될 때 생성됩니다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필두와 메서드에 대한 정보, Static 변수, 메서드의 바이트 코드 등을 보관합니다.
* 메서드 영역은 JVM벤더 마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area 혹은 Permanent Generation이라고 불립니다. 메서드 영역에 대한 가비지 컬렉션은 JVM벤더의 선택사항입니다.
Runtime Constant Pool
JVM 동작에서 가장 핵심적인 역할을 수행하는 곳으로 JVM 명세에서도 따로 중요하게 기술합니다. 각 클래스와 인터페이스의 상수 뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블로 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아 참조합니다.
* JVM 벤더란 ? JVM 벤더란 JVM 제공사 혹은 판매사이다.
실행 엔진(Execution Engine)
실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행합니다.
바이트 코드의 각 명령어는 1바이트 크기의 OpCode(Operation Code)와 추가 피연산자로 이루어져 있고, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 작업을 수행한 다음 OpCode를 수행하는 식으로 동작합니다.
이 수행 과정에서 실행 엔진은 바이트 코드를 기계가 실행할 수 있는 형태로 변경하는데 다음 두 가지 방식으로 변경합니다.
인터프리터
바이트 코드 명령어를 하나씩 읽어서 해석하고 실행합니다. 하나하나의 해석은 빠르지만 전체적인 실행속도는 느리다는 단점을 가지고 있습니다. JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작합니다.
JIT 컴파일러(Just-In-Time-Compiler)
인터프리터의 단점을 보완하기 위해 도입된 방식으로 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅 하지 않고 네이티브 코드로 직접 실행하는 방식입니다. 하나씩 인터프리팅하여 실행하는 것이 아니라 바이트 코드 전체가 컴파일된 네이티브 코드를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠릅니다.
네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 캐시에서 바로 꺼내어 실행하기 때문에 빠르게 수행됩니다. 하지만 JIT 컴파일러가 컴파일하는 과정은 바이트 코드를 하나씩 인터프리팅 하는 것보다 훨씬 오래 걸리기 때문에 JIT 컴파일러를 사용하는 JVM은 내부적으로 해당 메서드가 얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘었을 때만 JIT 컴파일러를 통해 컴파일하여 네이티브 코드를 생성합니다.
정리
- 자바 컴파일러를 통해 .java 파일을 바이트 코드(class 파일)로 만들고, JRE에서 바이트 코드를 실행시키면 JVM 시작되면서 JVM위에서 바이트 코드가 기계어로 해석되어 실행됩니다.
- JVM의 명세를 따르는 가상 머신은 모두 JVM입니다. 대표적으로 오라클 핫스팟이 존재합니다.
- JVM의 런타임 데이터 영역에는 모든 스레드들이 공유하는 Heap, Method 영억, 각 쓰레드마다 존재하는 Stack, PC Register, Native Method Stack이 존재합니다.
- 클래스 로더에는 BootStrap, Platform, System 클래스 로더가 존재하며 각 클래스 로더들은 위임 모델과 계층 구조를 지닙니다.
- JVM은 동적으로 로딩, 링크, 초기화 과정을 진행하며 Java 어플리케이션의 실행은 특정 클래스를 로딩, 링크, 초기화 과정을 거친 후 해당 클래스의 main method를 실행하는 것을 의미합니다.
'프로그래밍 > Java' 카테고리의 다른 글
[Java, 자바] @JsonProperty, @JsonNaming이란? (0) | 2024.06.20 |
---|---|
[Java, 자바] 추상클래스와 추상메소드 (0) | 2024.06.19 |
[Spring, Spring Boot] @Value가 null일 때의 원인과 해결 방법 (0) | 2024.06.17 |
[StringBoot] Azure 번역기 API 사용하기 (0) | 2024.06.12 |
[Java, 자바] 추상 클래스(Abstract Class)와 인터페이스(Interface)는 언제 사용해야 될까? (0) | 2024.05.31 |
개발의 모든 것 !
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!