1. Java의 실행과정
Java는 대표적인 객체지향 언어로 주로 대규모 서버 애플리케이션 개발이나 모바일 애플리케이션 개발에 사용되는 언어입니다. 따라서 Java 개발자라면 컴퓨터 내부에서 Java가 어떤 과정으로 실행되는지 깊게 이해하고 있어야 성능 최적화나 디버깅에 효율적으로 대처할 수 있을 것입니다.
이번 기회에 Java의 실행과정과 JVM이 무슨 일을 하는지 알아봅시다.
위 그림은 .java 확장자를 가진 파일이 해석되는 전체적인 과정을 나타낸 것입니다. 우린 평소에 아래와 같은 Java 파일을 생산했을 것입니다.
1.1 Java Compiler(javac)
우리와 같은 사람이 작성한 java파일은 첫 번째로 Java Compiler에 의해 바이트코드로 변환됩니다. 바이트 코드의 확장자는 .class입니다.
바이트코드는 기계어보다는 고수준이며, JVM이 이해하고 처리할 수 있는 형태입니다. JVM은 바이트코드를 실행하여 프로그램을 구동합니다.
이렇게 중간 단계의 바이트코드를 사용하는 이유는 플랫폼 독립성을 제공하기 위해서입니다. 개발자는 운영체제에 따라 여러 번 컴파일할 필요 없이 단 한 번의 컴파일만으로 다양한 운영체제에서 동일하게 실행할 수 있습니다.
javac 명령어를 통해 boj_1330.java 파일을 컴파일하여 boj_1330.class파일을 생성했습니다.
이제 우리는 JVM을 통해 이 class 파일을 실행할 수 있게 되었습니다.
2. Java Virtual Machine(JVM)
JVM은 자바 가상 머신의 약자입니다. JVM은 바이트코드를 기계어로 번역하여 프로그램을 실행시키는 가상 머신입니다. JVM은 총 4개의 구성요소로 이루어져 있습니다.
- Class Loader
- Execution Engine
- Runtime Data Area
- Native Method Interface
2.1 Class Loader
클래스 로더는 실행 시에 필요한 클래스 파일들을 로드하고 이를 Runtime Data Area에 올립니다. 올리고자 하는 클래스 파일을 파일 시스템, 네트워크, JAR 파일 등에서 찾고 이를 실행 중 필요한 시점에 동적으로 로딩합니다.
또한, 로더는 여러 개로 분리된 class파일을 하나로 합치는 링킹(Linking)도 수행합니다. 링킹은 각 클래스들을 하나로 합쳐 실행 가능한 상태로 만들게 되는데, 이때 클래스의 static 필드를 메모리에 할당하고 static블록을 실행시켜 초기화합니다.
클래스 로더가 작업을 마치면 그다음 Execution Engine에 의해 바이트코드가 실행됩니다.
2.2 Java Interpreter
로딩이 끝나고 처음에는 Interpreter가 실행됩니다.
인터프리터 언어는 컴파일러와 달리 소스코드를 한 줄씩 연속적으로 번역하고 실행합니다. 소스코드 단위를 바로 실행할 수 있는 장점이 있지만 기계어를 미리 생성해 놓는 컴파일 언어에 비해 속도측면에서는 느리다는 단점이 있습니다.
Java 인터프리터도 바이트코드를 한 줄씩 읽어서 기계어를 실행합니다. 인터프리터는 런타임에 기계어로 번역하기 때문에 Java는 플랫폼 독립성을 얻을 수 있지만, 반복되는 코드를 수행할 때 속도 저하가 발생합니다.
JVM은 인터프리터의 속도 저하를 JIT Compiler를 통해 개선하였습니다.
2.3 JIT(Just In Time) Compiler
만약 인터프리터가 반복되는 코드를 실행하게 되면 매번 같은 코드를 번역해야 하기 때문에 속도 저하가 발생합니다. 프로그램 초기에는 인터프리터가 코드를 실행하고 시간이 지남에 따라 JIT 컴파일러가 컴파일 한 기계어를 실행합니다.
JIT 컴파일러는 실행 시간에 코드를 분석하여 실행 빈도가 높은 코드를 식별하고 이를 컴파일합니다. 또한, 해당 코드를 캐싱하여 해당 코드를 수행할 때마다 캐싱된 코드를 재사용합니다.
JIT 컴파일러는 프로그램 성능에 가장 크게 영향을 주는 요소입니다. 개발자가 직접 JVM의 튜닝 옵션을 설정하여 JIT 컴파일러의 동작을 조정할 수 있습니다.
2.4 Garbage Collector
Java는 개발자가 메모리 주소에 직접 접근하는 것을 제한하고 있습니다. 따라서 동적으로 할당된 객체들을 메모리(Heap)에서 해제시켜야 하는데 이를 GC가 수행합니다.
GC는 더 이상 참조되지 않는 객체를 식별하고, 해당 객체가 사용하고 있는 메모리를 재사용 가능한 상태로 만듭니다. GC는 프로그램 수행 시 주기적으로 실행되며 GC의 스레드를 제외한 모든 스레드들이 멈추고, GC가 완료되면 다시 실행됩니다.
GC는 자동적을 실행되기 때문에 개발자는 메모리 관리에 완전히 자유로울 수 있지만, GC의 실행시간, 사용되는 알고리즘, 힙의 크기와 같은 설정들은 직접 조정할 수 있습니다.
2.5 Runtime Data Area
런타임 데이터 영역은 JVM의 메모리 영역입니다. JVM이 프로그램을 구동할 때 사용하는 메모리들을 적재하는 공간입니다. 런타임 데이터 영역은 저장된 데이터들의 종류에 따라 총 5가지 영역을 가지고 있습니다.
- Methode Area
- Heap
- Stack
- PC Register
- Native Method Stack
각 영역에 어떤 데이터들이 저장되고 JVM은 이를 어떻게 활용하는지는 다음 포스팅에서 더 자세히 다뤄보겠습니다.
3. JVM, JRE, JDK
JVM, JRE와 JDK는 Java를 개발하다 보면 자주 접하는 용어이지만, 비슷하기 때문에 각자 어떤 것인지 헷갈릴 때가 있습니다. 이번 기회에 정리해 봅시다.
- JVM : 위에서 정리했던 데로 바이트코드를 실행하는 가상 머신이다. JVM의 가장 큰 특징은 플랫폼 독립성과 자동 메모리 관리입니다.
- JRE : Java Runtime Environment의 약자로 JVM과 자바 라이브러리들을 묶어서 배포하는 패키지입니다.
- JDK : Java Development Kit의 약자로 개발자에게 필요한 자바 SDK라고 생각하면 됩니다. JDK 안에는 JRE와 javac, javadoc과 같은 개발 도구들이 포함되어 있습니다.