[Java] 자바의 동작 원리와 특징

개요

"JAVA는 자바 가상 머신(Java Virtual Machine) 위에서 동작한다" - 자바를 처음 배울 때 들어봤을 개념이다. 그렇다면 왜 자바는 가상머신 위에서 동작하도록 설계된 것일까? 이번 포스팅에서는 자바의 동작 방식과 배경에 대해서 정리해보도록 하겠다.

 

C언어로 프로그램을 작성해 본 적이 있다면, 아마 다음과 같은 과정을 거쳐 소스파일을 실행파일로 변환했을 것이다.

gcc -c test.c -o test.o # 소스파일(test.c)을 이용해 오브젝트 파일(test.o)를 만든다.
gcc test.o -o test.out # 오브젝트 파일(test.o)를 이용해 실행 파일(test.out)을 만든다.

 

C언어는 "Write Once, Complie Anywhere"(WOCA)라는 특징을 가진다. 사용하는 기계에 맞는 어셈블리어를 공부해서 소스코드를 작성해야 했던 어셈블리어와 다르게, 개발자가 C언어 문법을 바탕으로 단 하나의 소스 코드만을 작성하면 기계에 맞는 컴파일러가 각 기계에 맞는 기계어 파일로 변환해줄 수 있었기 때문이다. 

 

하지만 이러한 C언어도 문제가 있는데, 사용하는 운영 체제에 종속적이라는 것이다. 예를 들어, 리눅스에서 컴파일하여 만든 실행파일을 윈도우에서 실행하면, 돌아가지 않게 된다.

이처럼 OS마다 차이가 존재하기 때문에 OS별로 별도의 컴파일러를 통한 크로스 컴파일(Cross Compile)이 필요하게 된다. 규모가 큰 프로그램의 경우 컴파일에도 오랜 시간이 걸릴 수 있다는 점을 고려할 때, 이는 프로그램의 유연성과 이식성을 떨어뜨리게 된다.

 

이를 보완하기 위해서 자바는 OS마다 각각에 맞는 JRE를 제공한다. 이 JRE에는 JVM, 자바 라이브러리를 비롯한 자바 실행을 위해 필요한 파일들이 포함되어 있다. 자바 바이트코드는 운영체제에 상관 없이 JVM 위에서 동작하기 떄문에 적절한 JVM만 설치되어 있다면 별도의 크로스 컴파일을 필요로 하지 않는데, 이렇게 특정 OS에 종속되지 않고, 가상 머신을 통해서 어디서든 실행될 수 있는 자바의 특징을 "Write Once, Run Anywhere"(WORA)라고 부른다. 

 

 

JDK, JRE, JVM

 

 앞서 말했듯이 JRE(Java Runtime Environment)는 자바 애플리케이션을 실행하기 위해서 필요한 모든 요소를 포함하고 있다. 가상 머신(JVM)도 JRE 안에 포함되어 있으며 OS별로 차이가 존재한다. 마지막으로 JDK(Java Development Kit)는 클래스 파일을 실행하는 JRE와 함께 소스코드(*.java)를 클래스 파일(*.class)로 컴파일 해주는 javac를 비롯한 여러 도구들을 포함하고 있다. 따라서 자바 프로그램을 실행하는데 초점을 둔다면 JRE만 설치하고, 직접 소스코드를 작성하고 컴파일해야 한다면 JDK를 설치하면 된다.

 

 

 

JVM

execution engine이란 흔히 인터프리터, GC, JIT 컴파일러를 묶어서 말한다.

 

 JVM(Java Virtual Machine)은 javac에 의해서 컴파일된 자바 바이트 코드를 실행하기 위한 기계로, Method/Heap, Stack/PC Register/Native method stack으로 이루어진 Runtime Data 영역, 인터프리터, JIT 컴파일러, 가비지 컬렉터, 클래스 로더 등으로 이루어져있다.

 

- Runtime Data Area
- Interpreter, JIT Compiler
- GC
- Class Loader

 

JVM - Runtime Data Area

 

JVM을 이루는 구성 요소 중 하나는 Runtime Data Area이다. JVM은 가상 머신이기 때문에 별도의 메모리 구조를 가지게 된다. 힙과 메서드 영역의 경우 모든 스레드가 공유하며, 스레드마다 PC 레지스터, 스택영역이 별도로 생성된다. 

 

Heap은 클래스의 인스턴스와 배열과 같은 참조형(new) 데이터 객체들의 실제 값이 저장되며, Garbage Collection이라는 동적 메모리 관리 시스템에 의해서 관리된다.

Method 영역에는 클래스 메타데이터, 클래스에서 사용하는 상수, 메서드 정보, static 필드와 같은 데이터들이 저장된다. JVM 종료시까지 메모리에서 해제되지 않기 때문에 메모리 상에 존재한다.

PC 레지스터는 실제 CPU의 그것과 동일하게 JVM이 실행중인 명령어의 주소를 저장한다.

Stack은 메서드 호출과 반환에 따라 생성, 삭제되며 지역변수, 매개변수, 함수의 실행 결과를 저장한다.

 

 

 

JVM - Interpreter

 자바는 C, C++과 같은 컴파일 언어일까, 아니면 python과 같은 인터프리터 언어일까? 앞선 설명들을 보면 JDK에서 javac(java compiler)를 사용하여 소스코드(*.java)를 JVM에서 실행시킬 수 있는 클래스 파일(*.class)로 컴파일하므로 당연히 컴파일 언어라는 생각이 들 것이다.

 

하지만 JVM 안에는 인터프리터가 존재한다. 이 인터프리터는 바이트코드 인터프리터를 사용하여 클래스 파일을 한 줄씩 해석하고 이진 코드로 변환하여 즉시 실행하기 때문에 자바는 하이브리드 언어라는 특이한 이름으로 불리게 되었다.

 

 

JVM - JIT(Just-In-Time) Compiler

 인터프리터의 성능적 단점을 보완하기 위해서 사용 가능한 컴파일러이다. 자주 실행되는 바이트코드 블록을 네이티브 기계어로 변환하고, 캐싱과 같은 기법을 사용하여 동일 코드가 실행될 때 재사용할 수 있다. 

 

 

JVM - Garbage Collection

 

 JVM은 힙 메모리에 동적 할당되었으나 참조되지 않은 대상을 데몬 쓰레드를 통해 탐지하여 메모리에서 해제하는 기능을 가지는데, 이를 Garbage Collection, GC라고 한다.

 

1. Stop the World

2. Mark and Sweep

 

 먼저 JVM에서 GC를 실행하는 스레드를 제외한 나머지 스레드들의 애플리케이션 실행을 멈추고(Stop the World), 스택에 있는 모든 변수가 참조하는 객체와 Reachable Object가 참조하는 객체들을 마킹하고, 마킹되지 않은 객체(Unreachable Object)를 찾아서 힙에서 제거한다(Mark and Sweep).

 

Hotspot JVM Heap

 

자바는 힙 메모리를 효율적으로 관리하기 위해서 (Eden)-(Survivor0(S0)/Survivor1(S1))-(Old Generation)으로 나누어 관리하는데, eden부터 survival까지를 Young generation이라고 부른다.

 

객체가 최초 할당되면 GC의 Eden영역에 할당되고, Eden영역이 꽉 차면 GC가 발생하여 사용하지 않는 메모리를 해제한 후 참조되고 있는 객체는 S0 또는 S1으로 번갈아가며 옮겨진다. 이때 S0과 S1중 하나에만 데이터를 저장하는데, 이는 내부 단편화를 최소화하기 위한 동작이다. 이러한 과정을 통해 객체는 오랜 시간동안 참조될수록 Survivor 영역을 거쳐 일정 age값 이상이 되면 Old generation으로 이동하게 된다. 이때 Young Generation에서 이루어지는 빈번한 GC를 Minor GC, Old Generation에서 이루어지는 GC를 Major GC라고 부른다.

 

Minor GC는 Old영역보다 크기가 작은 Young Generation을 대상으로 진행하기 때문에 실행 속도가 빠르지만, Major GC는 실행 시간이 길 뿐만 아니라 공간 효율성에 초점을 두고 진행된다.

 

GC에도 다음과 같이 다양한 종류의 GC가 존재한다.

1. Serial GC - GC를 처리하는 스레드가 1개
2. Parallel GC - GC를 처리하는 스레드가 여러개
3. Concurrent Mark-Sweep GC - Stop The World 시간을 최소화시킨 GC. 응답시간이 중요한 애플리케이션에서 사용
4. G1 GC - 논리적 공간인 region으로 구분하여 처리

 

 

JVM - Class Loader

 클래스 로더는 런타임에 클래스 파일의 바이트 코드를 읽어 JVM의 Runtime Data 영역에 할당한다. 클래스 로더는 계층 구조로 되어 있어, 부모 클래스 로더부터 순차적으로 로딩을 시도한다. 

 

1. 부트스트랩 클래스 로더 (Bootstrap Class Loader)

-> JVM에 내장된 클래스 로더로, java.lang.*, java.util.*과 같이 자바 런타임 환경의 핵심 클래스들을 로딩한다.

2. 확장 클래스 로더 (Extension Class Loader)

-> 환경변수에 설정된 디렉토리의 클래스 파일을 로드한다.

3. 애플리케이션 클래스 로더 (Application Class Loader)

-> 사용자가 작성한 클래스와 라이브러리를 로드한다.

 

JVM은 클래스 로더의 실행을 통해서 클래스를 로딩한 후, static 초기화 함수를 합쳐 한꺼번에 실행시킨다. 

 

 

 

실행과정 정리

 

1. JDK javac를 통해서 소스 파일(*.java)을 바이트 코드(*.class)파일로 변환한다.

2. JRE에서 바이트 코드를 실행하면 JVM이 시작되면서 바이트 코드가 Interpreter에 의해 기계어로 해석되어 실행된다. 이때 성능 향상을 위해 JIT Compiler가 사용될 수 있다.

3. JVM 시작시 모든 스레드들이 공유하는 Heap/Method 영역과 스레드별로 Stack/PC Register영역이 할당된다. 

4. 로딩: Class Loader는 JVM 실행에 필요한 클래스를 메서드 영역으로 로딩한다. 기타 클래스들도 하위 클래스 로더에 의해서 로딩된다.

(아래 5~7 과정을 합쳐 링크 과정이라고 한다.)

5. 검증: 로딩된 클래스들의 바이트 코드가 JVM 명세를 따르는지 검증한다.

6. 준비: 클래스가 필요로 하는 메모리를 할당하고, static 필드를 초기화한다 (int=0, reference type=null)

7. 분석: 메서드 영역의 Runtime Constant Pool에 있는 심볼릭 참조를 직접 참조한다.

8. 초기화: 클래스의 static 초기화 함수를 실행한다.

9. 자바 어플리케이션의 실행을 위해서는 특정 클래스를 로딩, 링크, 초기화 과정을 거친 뒤 main method를 실행한다.

 

 

 

 

 

 

참고:  https://medium.com/@khurshidbek-bakhromjonov/java-memory-management-understanding-the-jvm-heap-method-area-stack-24a4d4fa2363

 

Java Memory Management: Understanding the JVM, Heap, Method Area, Stack

If you’re seeking a simplified explanation of the concepts of young generation, old generation, and permanent generation in Java, you’ve…

medium.com

https://velog.io/@sgwon1996/JAVA%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC%EC%99%80-JVM-%EA%B5%AC%EC%A1%B0#runtime-data-area

 

JVM 구조와 JAVA의 동작 원리

자바 가상 머신은 자바 플랫폼의 초석이다. JAVA는 자바 가상 머신 위에서 실행 되며 JAVA의 동작 방식을 이해하기 위해서는 자바 가상 머신에 대한 이해가 선행 되어야 한다. 이번 글에서는 JVM의

velog.io

 

 

'[ Languages ] > Java' 카테고리의 다른 글

[Java] 람다식 이해하기  (0) 2023.02.07
[Java] stream  (0) 2022.08.07
[Java] 자바의 싱글톤 패턴  (0) 2022.07.02
[JAVA] BufferedReader, BufferedWriter  (0) 2022.02.13
[Java] equals와 hashCode  (0) 2022.02.01