개요
최근에 프로젝트를 진행하면서 Mockito를 사용한 단위 테스트 중 static 메서드를 Mocking하지 못하는 문제를 만나게됐고 Mockito는 Proxy 방식으로 Mocking을 하기 때문에 static 메서드에 대한 Proxy 객체를 만들 수 없어서 Mocking할 수 없다는 걸 알게 됐다.
오히려 원인을 알게되니 '왜 static 메서드는 Proxy 객체를 만들지 못하지?'라는 생각이 들었고 이유를 파고들다보니 JVM 구조와 내부 동작 방식에 대해서 이해할 필요가 있었다. 자바의 기초이지만 대충 알고 넘어갔던 나 자신을 반성하면서 JVM에 대한 내용을 정리해본다.
Write once, Run anywhere
JVM은 자바 프로그램을 실행하기 위한 프로그램이다. 위 문구처럼 한 번 작성한 코드로 어떠한 환경에서도 실행할 수 있는 것이 특징이다.
통역사에 비유해보자면 JVM은 다국어 통역사와 같다. Java로 쓰인 원고(소스 코드)를 통역가가 읽을 수 있는 언어(바이트코드)로 번역한 후, 각 청중(윈도우, 맥, 리눅스)에 맞게 실시간으로 통역한다. 하나의 원고로 여러 청중에게 전달 할 수 있지만, 실시간 통역 과정에서 약간의 시간이 소요된다.
JVM 동작 흐름
- Javac(Java 컴파일러)가 소스 코드(.java)를 컴파일하면 동일한 클래스 이름을 가진 자바 바이트코드(.class)파일이 생성된다.
- 변환된 자바 바이트코드(.class)는 Class Loader에 의해 Runtime Data Area에 적재된다.
- Execution Engine의 자바 인터프리터와 JIT 컴파일러가 자바 바이트코드(.class)를 네이티브 코드(기계어)로 변환한다.
네이티브 코드란 CPU가 읽을 수 있는 기계어를 말한다.
JVM 구성 요소
클래스 로더(Class Loader)
클래스 로더는 자바의 특징 중 하나인 동적 로드를 담당한다. 동적 로드란 컴파일 타임이 아닌 런타임에 바이트코드(.class)를 읽어 메모리로 로딩하는 것을 말한다. 이러한 특징 덕분에 각 클래스들은 필요한 시점에 메모리에 올라가게 된다.
클래스 로더는 로딩, 링킹, 초기화 단계를 통해 동적 로드를 하게 된다. 각 단계에 대해 알아보자
로딩(Loading)
클래스 로더는 .class 파일을 읽고 클래스 정보, 메서드 정보, static, final, 상수 등과 같은 정보를 메서드 영역(Method Area)에 저장한다.
.class 파일을 로드한 후, JVM은 힙 영역에서 해당 파일을 나타내기 위해서 Class 객체를 생성한다.
Class 객체는 개발자가 클래스, 메서드 및 변수 정보 등 클래스 관련 정보를 얻는 데 사용할 수 있다.
getClass() 메서드를 사용하여 Class 객체를 가져올 수 있다. 이 때 사용되는게 Reflection이다.
링킹(Linking)
로딩 이후 검증, 준비, 해석을 진행한다.
- 검증(Verifying): .class 파일이 올바르게 포맷되었고 유효한 컴파일러에 의해 생성되었는지 확인한다. 만약 검증에 실패하면 예외를 발생한다.(java.lang.VerifyError)
- 준비(Preparing): JVM은 static 변수에 대한 메모리를 할당하고 메모리를 기본값으로 초기화한다.
- 해석(Resolving): 타입의 심볼릭 레퍼런스를 메서드 영역에 있는 실제 메모리 주소로 교체해준다. 심볼릭 참조란 실제 메모리 주소가 아닌 이름이나 설명으로 된 참조를 말한다. 해석 단계는 선택적으로 수행된다.
초기화(Initializing)
링킹 준비 단계에서 할당한 메모리 영역에 모든 static 변수 값을 할당한다.
런타임 데이터 영역(Runtime Data Area)
- 메서드 영역(Mehtod Area): 메서드 영역에는 클래스 이름, 부모 클래스 이름, 상수, 메서드 및 변수 정보등 클래스 관련 정보가 저장되고 static, final 변수도 저장된다. 메서드 영역은 JVM당 하나뿐이며 모든 스레드가 공유한다.
- 힙 영역(Heap Area): New로 생성되는 모든 객체(인스턴스)는 힙 영역에 저장된다. 메서드 영역과 마찬가지로 JVM당 하나뿐이며 모든 스레드가 공유한다.
- 스택 영역(Stack Memory): 메서드 파라미터, 지역 변수 등이 저장된다. 스레드가 종료되면 해당 스택 영역은 JVM에 의해 삭제된다. 각 스레드는 별도의 스택 영역을 가지며 공유 리소스가 아니다. for문이나 재귀함수를 무한 루프되도록 작성해보면 만날 수 있는 StackOverflow의 Stack이 바로 이 Stack이다.
- PC 레지스터(PC Register): 스레드가 현재 실행중인 명령의 주소를 저장한다. 스레드가 시작될 때 함께 생성되며 각 스레드는 별도의 PC 레지스터를 갖는다.
- 네이티브 메서드 스택(Native Method Stack): Java가 아닌 다른 언어(C, C++)로 작성된 코드를 저장한다. JNI를 통해 바이트 코드로 전환하여 저장된다.
실행 엔진(Execution Engine)
실행 엔진은 자바 바이트코드(.class)를 실행한다. 각 메모리 영역에 있는 데이터와 정보를 사용하고, 명령어를 실행한다. 실행 엔진은 다음과 같이 구성되어 있다.
- 인터프리터(Interpretor): 바이트코드를 라인별로 해석한 후 실행한다. 인터프리터의 단점은 같은 명령어를 보더라도 매번 새롭게 해석을 해줘야 하기 때문에 느리다.
- JIT 컴파일러(Just-In-Time): 인터프리터의 효율성을 높이기 위해 사용된다. 바이트코드 전체를 컴파일하여 네이티브 코드로 변경한다. 덕분에 인터프리터가 반복되는 명령어를 볼 때마다 JIT가 해당 부분에 대한 네이티브 코드를 제공해서 다시 해석할 필요가 없기 때문에 효율성이 향상된다. 다만, JIT 컴파일러가 컴파일하는 과정은 인터프리터보다 느리기 때문에 일정 횟수 이상 반복되는 명령어에 대해서만 JIT 네이티브 코드로 변환한다.
- GC(가비지 콜렉터): 가비지 콜렉터는 참조되지 않는 객체를 메모리 해제해준다. 덕분에 Java에서 메모리를 효율적으로 관리할 수 있다. GC에 대한 자세한 내용은 다른 글에서 다루겠다.
JNI(Java Native Interface)
실행에 필요한 네이티브 라이브러리를 제공하는 인터페이스이다. 프로세스 성능 향상을 위해 자바코드가 네이티브 응용 프로그램, C, C++, 업셈블리 같은 다른 언어들로 작성된 라이브러리들을 호출하거나 호출하는 것을 가능하도록 하는 기능을 제공한다.
참조
'Java' 카테고리의 다른 글
Java - 자바 가비지 컬렉션(Garbage Collection)을 알아보자 (1) | 2024.11.09 |
---|---|
Mybatis - SAXParseException: The content of elements must consist of well-formed character data or markup 원인과 해결법 (0) | 2024.06.05 |
Java - File로 파일 목록 이름 조회하기 (1) | 2024.01.25 |
Java - @JasonCreator로 DTO에서 유연하게 Enum Type 받기 (0) | 2023.04.26 |
Java - 커스텀 애너테이션으로 유효성 검사하기 (0) | 2023.04.22 |