면접을 보러다니며, Java에 대한 기초가 많이 부족하다고 느꼈다.
Java 개발자가 되려고 하면서, Java에 대해 잘 모르는게 참 모순 같다고 느꼈다.
그래서 중요한 부분들을 차근차근 공부해보려고 한다.
가장 먼저 공부하려는 부분은 Java의 핵심, JVM 이다!
JVM?
가장 먼저 JVM이 뭘까?
JVM은, Java Virtual Machine
의 약어로, 자바 가상 머신을 의미한다.
Java Appliation
의 시작과 끝 모두를 담당한다고 볼 수 있는 부분이다.
크게 구분되는 역할은 아래와 같다.
- 생성되어진 클래스들을 런타임에 옮긴다.
- 코드를 한 줄씩 해석하고 실행한다.
- 메모리를 자동으로 관리한다.
Java Application
의 전체적인 흐름을 모두 잡고 있다고 봐도 무방하다.
아래로 자세히 어떻게 구성이 되는 지, 어떤 일을 하는지 알아보도록 하자.
Java의 컴파일 방식?
정확한 JVM
의 구조에 대해 알기 전에, Java
가 어떤 언어인지 부터 알아보자.
Java
는 컴파일러 언어일까? 인터프리터 언어일까?
Java
는 특이하게도 두 성격을 모두 띄고 있다.
C/C++
과 같은 컴파일러 언어는 코드를 기계어(Native)로 해석하게 된다.
해석된 기계어를 목적 코드로 변환하고, 링크하여 실행 파일을 만들게 된다.
Python
과 같은 인터프리터 언어는 코드를 한 줄씩 그대로 실행하게 된다.
기계어로 해석하지 않고 한 줄씩 읽어 실행하기에, 속도가 매우 느리다.
Java
는 이 두 특성을 모두 잡고자 한 것 같다.
먼저 javac
라는 컴파일러가 코드를 ByteCode
의 형태로 컴파일한다.
그럼 이후에 JVM
내에 존재하는 Interpreter
가 ByteCode
를 읽기 시작한다.
이 때, 인터프리터 언어의 특성과 같이 한 줄씩 읽으며 실행하는 것이다.
이러한 특성 때문에, Java
는 굉장히 높은 이식성을 가지게 된다.
C/C++
과 같은 언어는 OS
에 종속적인 기계어로 해석을 하기 때문에, 이식성이 낮다.
하지만 Java
는 OS
가 아닌 JVM
이 해석하도록 컴파일을 하는 것이다.
따라서 어떤 OS
를 사용하던지, JVM
만 있다면 실행이 가능한 것이다.
JVM의 구조
앞서 설명했듯이, javac
가 코드를 해석해서 .class
파일을 JVM
에게 넘겨준다.
그럼 이제부터 JVM
이 본격적으로 활동하기 시작한다.
아래로 JVM
이 동작하게 되는 순서대로 구성요소를 알아보도록 하자.
Class loader
가장 먼저 Class loader
이다.
.class
파일, 즉 ByteCode
를 동적으로 받아와, 메모리에 적재하는 일을 수행하는 부분이다.
아래에서 후술할 Runtime Data Area
에 ByteCode
를 동적으로 적재한다.
Loading -> Linking -> Initialization
순서로 작업을 실행하여 적재시킨다.
Execute Engine
다음으로 Execute Engine
, 즉 실행 엔진이다.
이 부분은 작게 Interpreter, JIT Compiler, GC
로 다시 나누어진다.
앞서 Class loader
가 Runtime Data Area
에 배치한 바이트코드가 있었다.
이 코드를 Interpreter
에서 해석을 하고, 한 줄씩 실행하게 된다.
여기서 Java
의 인터프리터적인 모습이 나오는 것이다.
하지만 계속 인터프리터처럼 한 줄씩 실행을 하다보면, 성능이 저하되기 마련이다.
그런 일을 방지하기 위해 JIT(Just In Time) Compiler
가 존재한다.
반복되는 코드가 발견된다면, JIT Compiler
가 Native
로 해석하게 된다.
그럼 이제 해당 코드는 캐싱해두었다가, 인터프리터가 아닌 컴파일러와 같이 동작하는 것이다.
해당 부분은 이미 해석된 코드들을 실행하기 때문에, 더 빠른 속도로 실행된다.
위에서 코드들이 실행되는 동안, 수많은 객체들이 생겨나고 사라질 것이다.
Java
는 이런 부분을 개발자가 직접 관리하지 않아도 된다.
바로 Garbage Collector
가 알아서 자동으로 메모리 해제를 담당하기 때문이다.
Runtime Data Area
이 부분은 쉽게 말해, JVM
의 메모리 영역이라고 보면 될 것 같다.
아래와 같은 구조로 구성되어있다.
먼저, Method
공간은 JVM
이 가장 먼저 시작될 때 생성되는 공간이다.
Class loader
가 처음으로 Runtime Data Area
에 코드를 올릴 때, 생성되는데,
초기화 해야 할 대상을 저장하는 공간으로 사용된다.
JVM
동작 이후, 클래스가 로드될 때 적재되어 프로그램이 종료되면 해제된다.
다음으로 Heap
공간은 흔히 아는 동적 메모리로 사용되는 Heap
공간과 같다.
new
키워드로 생성되는 인스턴스, 배열 등의 Reference type
이 저장된다.
당연히 Method
공간에 적재되어 있는 클래스만 할당이 가능하다.
Stack
영역은 원시 자료형을 생성할 때 저장하는 공간이다.
임시적으로 사용되는 변수나, 정보들이 저장된다는 말과 같다.
메서드가 호출될 때마다 Stack
내에 각각의 Stack Frame
을 할당받는다.
호출된 메서드의 매개변수, 지역변수, 반환 값..
등을 저장하여 사용하게 된다.
메서드 수행이 끝나면, 해당 Stack Frame
은 사라지게 된다.
중요한 점은 Stack
공간에 Heap
영역에 대한 참조변수가 저장된다는 점이다.
Heap
영역을 할당받은 객체나 배열 같은 Reference Type
의 변수들은,
Heap
공간에 접근하는 것이 아닌, Stack
에 저장된 참조 변수에 접근하는 것이다.
또한 멀티 쓰레드 환경에서 Stack
은 각 쓰레드마다 생성이 된다.
이후 쓰레드가 종료될 때, Stack
도 같이 사라지게 된다.
PC Register
는 쓰레드가 생성될 때 생성된다.
현재 수행중인 JVM 명령어가 저장되는 공간이라고 보면 된다.
운영체제에서 프로세스를 실행시킬 때, Program Counter
를 이용하는 것과 동일하다.
Native Method Stack
은 굉장히 특이한 공간이다.
Java
에서 사용되는 ByteCode
가 아닌 Native
코드가 저장되는 공간이다.
즉, JIT Compiler
로 해석된 Native
코드가 이 공간에서 실행이 되는 것이다.
이외에도, Native
코드로 해석되어야 하는 부분이 있다면 이 공간에서 실행이 된다.
JNI
JNI
는 Java Native Interface
의 약어로, Native
와 관련되어 있다.
Native
, 즉 다른 언어로 작성된 어플리케이션과 상호작용 할 수 있는 인터페이스이다.
하지만 실질적으로 동작하는 언어는 Java
의 모언어인 C/C++
정도라고 한다.
이 부분은 Native Method Stack
과 연결되어, Native
코드의 실행을 돕는다.
Native Method Library
Native Method Stack
에서 실행되는 코드에 대해 라이브러리가 필요할 때 사용된다.
헤더와 같은 라이브러리가 필요할 때, 이 부분에서 JNI
가 호출하게 된다.
위와 같이 JVM
에 대해서 알아보았다.
다음번에는 Garbage Collector
에 대해서 조금 더 자세하게 알 필요가 있을 것 같다.
굉장히 복잡한 구조이기 때문이다.