Java Virtual Machine의 약자로 직역 그대로 자바 가상 기계이다. Java가 OS에 종속되지 않게 중재자 역할을 해주는게 JVM이다.
또한 JVM은 Java코드를 OS가 읽을 수 있도록 해석 해주는 역할도 한다. Java코드 즉 원시코드(.java)를 Java Compiler(.javac)가 컴파일을 해주면 자바 바이트 코드(.class)로 컴파일이 된다.
위 그림과 같이 빨간색으로 표시된 부분이 자바 바이트 코드(.class)로 컴파일된 파일을 JVM이 OS가 읽을 수 있도록 해석 해주는 것이다.
위에서 컴파일된 자바 코드를 JVM이 읽을 수 있도록 변환된 코드가 자바 바이트 코드이다.
일단 바이트 코드에 대해 설명을 하자면 바이트 코드는 특정 하드웨어가 아닌 가상 컴퓨터에서 돌아가는 실행 프로그램을 위한 이진 표현법이다. 하드웨어가 아닌 소프트웨어에 의해 처리되기 때문에, 보통 기계어보다 더 추상적이다.
바이트 코드는 대부분의 명령 집합이 0개 이상의 매개 변수를 갖는 1바이트 크기의 명령 코드(opcode)였기 때문에 바이트코드라 불리게 되었다.
바이트 코드는 특정 하드웨어에 대한 의존성을 줄이고, 인터프리팅도 쉬운 결과물을 생성하고자 하는 프로그래밍 언어에 의해, 출력 코드의 한 형태로 사용된다. 컴파일되어 만들어진 바이트 코드는 특정 하드웨어의 기계 코드를 만드는 컴파일러의 입력으로 사용되거나, 가상 컴퓨터에서 바로 실행된다.
바이트 코드의 특징인 하드웨어에 대한 의존성이 낮고, 인터프리팅도 쉬운 결과물로 생성해주기 때문에 JVM이 설치만 되어 있다면 어떤 OS에서도 Java를 사용할 수 있게 해주는 것이다.
자바의 단점 중 실행속도가 느리다는 것이 있는데 자바 실행속도가 느린 이유를 여기서 알 수 있게 된다. 자바 애플리케이션을 실행 시 JVM을 거쳐서 인터프리터를 해야 하기 때문에 자바의 속도가 느릴 수 밖에 없게 된다.
자바가 느린 이유가 JVM에 의해 느려지게 된 것을 알게 됐고, JVM을 가지고 자바 성능 튜닝을 하여 자바 애플리케이션의 속도를 향상 시킬 수 있다는 것을 알게 되었고 나중에 자바 성능 튜닝 관련 공부를 할 기회가 생기면 해보면 될 것으로 보인다.
1. 자바 코드(.java)를 Java Compiler(.javac)가 Java 바이트 코드(.class)로 컴파일을 한다.
2. 자바 바이트 코드(.class)를 Class Loader에 전달한다.
3. Class Loader는 동적로딩을 통해 필요한 클래스들을 로딩 및 링크하여 Runtime Data Area(JVM의 실질적인 메모리를 할당 받아 관리하는 영역)에 올린다.
4. Runtime Data Area에 로딩된 바이트 코드는 Execution Engine에 해석된다.
5. 이 과정중에 Execution Engine에 의해 Garbage Collector의 작동과 Thread 동기화가 이루어진다.
크게 5가지로 나뉜다.
Class Loader는 Byte Code(.class)들을 엮어서 JVM내로 동적으로 load하고 링크를 통해 Runtime Data Area(JVM 내의 메모리 영역)에 배치 해준다.
클래스 파일을 가져와서 JVM의 메모리에 load 한다
Bootstrap Class Loader
Extension Class Loader
Application Class Loader
Verify(검증)
Class Load 모든 과정 중에서 가장 복잡하고 시간이 많이 걸리는 과정으로, 읽어들인 클래스가 자바 언어 명세( Java Language Specification ) 및 JVM 명세에 명시된 대로 구성되어 있는지 검사한다. 확인 실패 시 Runtime Exception이 발생한다.
Preparing(준비)
Class가 필요로 하는 메모리들을 할당한다. 필요한 메모리들은 Class에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조들을 말한다.
Resolve
Class의 상수 풀 내 모든 유형의 기호 참조( Symbolic references )를 직접 참조( Direct references )로 변경한다.
Symblic Reference : 이름에 대한 참조 / Direct Reference : 실제 메모리 주소에 대한 참조
클래스 변수들을 적절한 값으로 초기화한다. ( static 필드들을 설정된 값으로 초기화 등 )
Runtime Data Area는 크게 6가지로 나뉜다.
JVM이 시작될 때 생성되는 공간으로 바이트 코드(.class)를 처음 메모리 공간에 올릴 때 초기화되는 대상을 저장하기 위한 공간이다.
메서드 영역은 모든 쓰레드가 공유하는 영역이기 때문에 다음과 같은 정보들이 저장된다.
한마디로 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 한다.
힙 영역도 메소드 영역처럼 모든 쓰레드가 공유하는 영역이며 JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역이다.
new 연산자로 생성하는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다.
메소드 영역에 저장된 클래스만이 생성되어 적재된다.
힙 영역에 있는 인스턴스를 핸들링 하기 위해서는 스택 영역에서 힙의 참조 주소를 갖고 있는 객체를 통해서 핸들링 할 수 있다.
만일 참조하는 변수나 필드가 없다면 의미가 없는 객체로 인식해 쓰레기 취급을 하고 JVM은 GC를 실행시켜 힙 영역에서 해당 객체를 자동으로 삭제한다.
이렇게 힙 영역은 GC에 대상이 되는 영역이기도 하다. 그리고 GC를 수행하기 위해 위 사진처럼 세부적으로 5가지 영역으로 나뉜다.
생명주기가 긴 객체를 GC 대상으로 하는 영역. Young Generation에서 살아남은 객체들이 이동
자료구조 스택의 특징인 마지막에 들어간 것이 제일 먼저 나오는 구조(LIFO, Last In First Out 또는 FILO, First In Last Out 가장 먼저 들어간 것이 가장 마지막에 나오는 구조)이고 push, pop 방식으로 사용된다.
스택 영역은 기본 자료형(int, long, boolean 등등)을 생성할 때 가지고 있는 공간으로 임시적으로 사용되는 변수나 정보들을 저장하는 공간이다.
메서드 호출 시 각각의 스택 프레임이 생성되면서 메서드 안에 있는 값들을 저장(push)하고 호출된 메서드의 수행이 끝나면 스택 프레임 별로 삭제(pop)한다.
위 그림에서 각각의 A, B, C, D, E 라고 표현한 것들이 각각의 스택 프레임이다. 위 그림처럼 메서드가 호출될 때마다 프레임이 만들어지고 현재 실행되고 있는 메서드 상태 정보를 저장하는 곳이다.
메서드가 호출되고 종료가 되면 스택에서 삭제가 된다. 스택 프레임에 쌓이는 데이터들은 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산된 결과 값들이 있다.
힙 영역에서는 "new 연산자로 생성하는 클래스와 인스턴스 변수, 배열 타입 등 Reference Type이 저장되는 곳이다."
스택 영역은 "기본 자료형(int, long, boolean 등등)을 생성할 때 가지고 있는 공간으로 임시적으로 사용되는 변수나 정보들을 저장하는 공간이다."
즉, 기본 타입의 직접적인 값들은 스택 영역에서 가지고, 참조타입의 변수는 메서드 영역이나 힙 영역의 객체 주소 값을 가진다.
PC 레지스터는 쓰레드가 시작될 때 생성되며 현재 수행중인 JVM 명령어 주소를 저장하는 공간이다. JVM 명령어 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행하는지에 대한 기록을 한다.
자바는 OS나 CPU 입장에서는 하나의 프로세스이다. 그래서 기 때문에 JVM의 리소스를 이용해야 해서 PC 레지스터라는 메모리 영역이 필요로 하게 됐다.
네이티브 메서드 스택은 바이트 코드가 아닌 기계어로 작성된 프로그램(C, C++, 어셈블리어) 을 실행 시키는 영역, 즉 네이티브 코드로 실행하기 위한 공간이라고 할 수 있다.
위 과정처럼 수행을 할 수 있게 된 것 중에 JNI(Java Native Interface)가 있어서 가능한 것도 있다.
JNI는 자바가 다른 언어로 만들어진 애플리케이션과 상호작용 할 수 있도록 만들어진 인터페이스다. JVM이 네이티브 메서드를 적재할 수 있도록 해주는 역할을 한다.
C, C++ 로 작성된 라이브러리가 필요할 경우 Native Method Library를 이용해 로딩하여 실행한다.
클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다. 명령어 단위로 실행하는 방식은 두 가지를 혼합해서 사용하는데 두 가지에 대해서도 알아보겠다.
바이트 코드 명령어를 한 줄씩 읽어서 해석하고 실행한다. JVM안에서 기본적으로 인터프리터 방식으로 실행된다. 바이트 코드 명령어를 한줄 씩 읽고 해석하다 보니 느린 것도 있지만 같은 메서드라도 여러 번 호출하고 해석하고 실행시키는 이유도 있다.
인터프리터의 단점을 보완한 방식으로 반복되는 코드를 발견하여 바이트 코드 전체를 컴파일 하여 네이티브 코드 변경하고 이후에는 해당 메서드를 더 이상 인터프리터 하지 않고 캐싱에 저장해두었다가 저장해둔 네이티브 코드로 직접 실행한다.
컴파일된 네이티브 코드를 실행하는 방식이기 때문에 인터프리터 보다 전체적인 속도는 더 빠르다. 하지만 바이트 코드를 네이티브 코드로 변환하면서 많은 비용이 발생하기 때문에 JVM이 기본적으로 인터프리터 방식을 사용하다가 일정 기준이 넘어가면 JIT-Compiler를 사용하는 방식이다.
JVM은 가비지 컬렉터를 이용하여 힙 영역에서 사용하지 않는 메모리들을 자동으로 처리해준다. C언어에서는 개발자가 직접 메모리 관리를 해줘야 하지만 자바에서는 개발자가 개발에 집중할 수 있도록 GC가 관리해준다.
Full GC가 발생하는 경우 GC를 제외한 모든 스레드가 중지되기 때문에 장애가 발생할 수 있다.
참고