JAVA 탄생 배경
JAVA는 썬 마이크로시스템즈의 제임스 고슬링이라는 사람과 다른 연구원들이 개발한 프로그래밍 언어이다.
1991년 그린 프로젝트라는 이름으로 시작해 1995년에 발표가 되었다.
제임스 고슬링은 가전제품 내에 탑재해 동작하는 프로그램을 개발하려고 하는 데 그 당시에는 유닉스 기반의 배경을 가지고 있었기 때문에 사용하던 프로그래밍 언어 C/C++의 특성상 여러 하드웨어를 커버하기에는 같은 기능의 소스를 각 하드웨어에 맞게 작성해야 하는 번거로움이 있었기에 JAVA를 개발하게 되었다.
JAVA의 가장 큰 특징 중 하나가 컴파일된 코드가 플랫폼 독립적이라는 점이다.
즉 어느 플랫폼이든 작성한 소스를 변경할 필요 없이 다 실행시킬 수 있다.
이러한 특징을 구현하기 위해서는 JVM(JAVA Virtual Machine)이 필요하다.
JVM은 단순하게 말하면 컴파일된 코드를 실행시켜 주는 가상의 컴퓨터라고 생각하면 된다.
- JVM은 H/W와 OS 위에서 실행되기 때문에 JVM 자체는 플랫폼에 종속적이다. (플랫폼에 호환되는 JVM을 실행시켜야 함)
자바 코드 실행 과정
1. 작성한 자바소스 파일을 자바 컴파일러를 통해 자바 바이트코드로 컴파일한다.
2. 컴파일된 바이트코드를 JVM의 클래스 로더에게 전달한다.
3. 클래스 로더는 동적로딩을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역, JVM의 메모리에 올린다.
4. 실행엔진은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행한다.
JVM 동작 원리 및 기본 개념
JVM에는 클래스 로더, 런타임 데이터 영역, 실행 엔진이 존재한다. 이를 하나씩 알아보자
클래스 로더(Class loader)
클래스 로더의 특징을 5가지로 분류할 수 있다.
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. 이름 공간(Name space)
네임스페이스는 각 클래스 로더들이 가지고 있는 공간으로 로드된 클래스를 보관하는 공간이다.
클래스를 로드할 때 위임 모델을 통해서 상위 클래스 로더들을 확인하는데 그때 확인하는 공간이 바로 네임스페이스이다.
네임스페이스에 보관되는 기준은 FQCN(Fully Qualified Class Name)을 기준으로 보관되는데 FQCN이란 패키지명까지 포함되어 있는 식별자를 뜻한다.
클래스 로드 과정
아직 로드되지 않은 클래스를 로드하는 과정을 살펴보자
1. 로드: 클래스 파일을 가져와서 JVM의 메모리에 로드한다.
2. 검증: 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
3. 준비: 클래스가 필요로 하는 메모리를 할당한다. 필요한 메모리란 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조들을 의미
4. 분석: 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 디렉트 레퍼런스로 변경한다.
5. 초기화: 클래스 변수들을 적절한 값으로 초기화한다.
- 기본 자료형을 제외한 모든 타입을 명시적인 메모리 주소 기반의 레퍼런스가 아닌 심볼릭 레퍼런스를 통해 참조한다.
- 컴파일 과정에서 링크될 부분을 직접 참조가 아닌 간접참조로 두는 것을 심볼릭 레퍼런스라고 한다.
- 클래스 로더가 심볼릭 레퍼런스를 실제 라이브러리의 구현체로 매핑하는 과정이 4번에 해당한다.
런타임 데이터 영역(Runtime Data Area)
JVM이 OS 위에서 실행되면서 할당받는 메모리영역이 런타임 데이터영역이다.
PC 레지스터, JVM 스택, 네이티브 메서드 스택은 쓰레드마다 하나씩 생성된다.
힙, 메서드 영역은 모든 쓰레드가 공유해서 사용된다.
PC레지스터
- 현재 수행 중인 명령의 주소를 가지며 쓰레드가 시작될때 생성되며 각 쓰레드마다 하나씩 존재한다.
JVM스택
- 스택 프레임이라는 구조체를 저장하는 스택이다.
- 예외 발생 시 printStackTrace() 메서드로 보여주는 Stack Trace의 각 라인 하나가 스택 프레임을 표현한다.
- 쓰레드가 시작될 때 생성되며 각 쓰레드마다 하나씩 존재한다.
네이티브 메서드 스택
- JAVA 외의 언어로 작성된 네이티브 코드를 위한 스택이다.
- JNI(JAVA Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 스택이 생성된다.
힙
- 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션의 대상이다.
- 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더들의 재량이다.
메서드 영역
- 모든 쓰레드가 공유하는 영역으로 JVM이 시작될 때 생성된다.
- JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드에 대한 정보, static 변수 등을 보관한다.
- 메서드 영역에 대한 가비지 컬렉션은 JVM벤더의 선택사항이다.
런타임 상수 풀
- 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다.
- 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
실행 엔진(Execution Engine)
실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
바이트 코드의 각 명령어는 1바이트 크기의 Operation Code와 추가 피연산자로 이루어져 잇다.
실행 엔진은 하나의 Operation Code를 가져와서 피연산자와 작업을 수행한 다음, 그다음 Operation Code를 수행하는 식으로 동작한다.
실행 엔진은 바이트 코드를 기계가 실행할 수 있는 형태로 변경하는데 두 가지 방식으로 변경한다.
인터프리터
- 바이트 코드 명령어를 하나씩 읽어서 해석하고 실행한다.
- 하나하나의 해석은 빠르지만 전체적인 실행 속도는 느리다.
- JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작한다.
JIT 컴파일러(Just - In - Time Compiler)
- 인터프리터의 단점을 보완하기 위해 도입된 방식이다
- 바이트 코드 전체를 컴파일하여 네이티브 코드로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네 이티비 코드로 직접 실행하는 방식이다.
- 하나씩 인터프리팅하여 실행하는 것이 아닌 바이트 코드 전체가 컴파일된 네이티브 코드를 실행하는 것이기 때문에 실행속도가 빠름
네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 캐시에서 바로 꺼내어 실행하기 때문에 빠르게 수행된다.
하지만 JIT 컴파일러가 컴파일하는 과정은 바이트 코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리기 때문에 JIT 컴파일러를 사용하는 JVM은 내부적으로 해당 메서드가 얼마나 자주 호출되고 실행되는지 체크하고, 일정 기준을 넘었을 때에만 JIT 컴파일러를 통해 컴파일하여 네이티브 코드를 생성한다.
JIT 컴파일러를 통한 컴파일 과정은 바이트 코드를 바로 네이티브 코드로 만드는 것이 아닌 안에서 IR(Intermediate Representation)로 변환하여 최적화를 수행하고 그다음에 네이티브 코드로 변환하는 과정을 거친다.
오라클 핫스팟 JVM은 핫스팟 컴파일러라고 불리는 JIT 컴파일러를 사용하는 데 내부적으로 프로파일링을 통해 가장 컴파일이 필요한 부분을 찾아낸 다음, 이 부분을 컴파일하기 때문에 핫스팟이라 부른다. 핫스팟JVM은 한번 컴파일된 바이트 코드라도 해당 메서드가 더 이상 자주 불리지 않는다면, 캐시에서 네이티브 코드를 덜어내고 다시 인터프리터모드로 동작한다.
'JAVA' 카테고리의 다른 글
[JAVA] 불변 객체를 사용해야 하는 이유 (2) | 2023.05.17 |
---|---|
다양한 가비지 컬렉션(Garbage Collection) 알고리즘 (0) | 2023.05.15 |
[JAVA] Private 메서드 테스트 방법 및 지양하는 이유 (0) | 2023.05.15 |
[JAVA] 가비지 컬렉션(Garbage Collection)의 개념 및 동작 원리 (0) | 2023.05.14 |
[JAVA] Optional 잘 사용하는 방법 (0) | 2023.05.12 |