✅ 빌드
Build Type | Process | Example |
compile | 소스코드 전체를 기계어로 번역 | C, C++, Go |
interpret | 소스코드를 한 줄씩 번역하면서 실행 | Python |
hybrid | 소스코드 전체를 중간코드(bytecode)로 번역한 뒤, 가상머신(VM)에서 한 줄씩 실행 | Java |
영어를 쓰는 사람이 많기 때문에 프랑스어, 힌두어의 책을 1차적으로 영어로 번역함 (중간 번역)
그런 다음 한국어로 번역하면 번역 가능한 사람이 많이 때문에 번역 빨리 할 수 있음
바이트 코드 (bytecode)
- .java → .class
- 기계어는 아니지만, 어셈블리어처럼 ‘기계에 가까운 언어’
- 하드웨어가 직접 처리하는 것이 아닌, 소프트웨어(VM)에 의해 처리됨
- 바이트코드는 VM 전용 기계어
- compiler : javac.exe
VM (Virtual Machine, 가상의 운영체제)
- bytecode를 기계어로 바꿔줌
- VM 안에 인터프리터같은 해석기가 있어서 이들이 bytecode를 해석하여 각 OS에 맞게 명령어를 해석하고 작동함
- JVM은 내부적으로 기계어 파일을 생성하는게 아니라, class 파일을 읽고, 매 줄마다 해석을 해서 기계어로 번역한 뒤, 해당 명령어에 맞게 운영체제에 명령을 전달하여 얻은 결과값을 다시 반환하는 것 뿐이지, 어떠한 파일을 생성하지는 않는다.
- 플렛폼에 VM이 있으면 소스코드를 실행 가능하기 때문에 플렛폼 독립적임 (어느 플렛폼에서든지 동일한 결과를 얻을 수 있음)
- VM을 통해 ‘플렛폼 독립적’인 장점을 가지고 왔고, 초기 컴파일 단계를 통해 바이트코드로 기계어에 더 가까운 언어로 번역을 한 번 해놓았기 때문에 속도도 기존 인터프리터 언어에 비해 더 빠르다는 장점 또한 가지고 옴
- jvm : java.exe
✅ JVM
자바 애플리케이션을 클래스 로더를 통해 읽어 자바 API와 함께 실행
- 자바 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당받는다.
- 자바 컴파일러(javav)가 자바 소스코드(.java)를 자바 바이트 코드(.class)로 컴파일한다.
- Class Loader는 동적 로딩을 통해 필요한 클래스들을 Loading&Linking&Initialization 하여 Runtime Data Area에 올린다.
- Runtime Data Area에 로딩 된 바이트 코드는 Execution Engine을 통해 해석된다.
- 이 과정에서 Execution Engine에 의해 Garbage Collector의 작동과 Thread 동기화가 이루어진다.
1️⃣ Class Loader (클래스 로더)
로드된 바이트 코드(.class)들을 엮어서 JVM의 메모리 영역인 Runtime Data Area에 배치한다.
- 로딩 기능은 한번에 메모리에 올리지 않고, 어플리케이션에서 필요한 경우 동적으로 메모리에 적재하게 된다.
- Loading
- .class 파일을 읽어와 JVM 메모리에 적재(로드)한다.
- Linking
- 클래스가 메모리에 로딩된 후에 실행되는 단계이다. 클래스 파일의 정보를 분석하여 해당 클래스가 참조하고 있는 다른 클래스, 메서드, 변수 등의 레퍼런스(참조)를 연결한다.
- 검증 단계
- 로딩된 클래스 파일이 올바른 자바 클래스인지 검증하는 과정 (클래스 파일이 자바 언어 명세에 맞게 작성되어 있는지 검증)
- 예를 들어, 클래스 파일이 올바른 바이트 코드로 작성되어 있는지, 필요한 클래스가 존재하는지 등을 검사
- 클래스 파일이 올바르지 않으면 'VerifyError'와 같은 예외 발생
- 준비 단계
- 클래스가 필요로 하는 메모리 공간을 할당한다. 이때, 클래스 변수 (static variable)는 기본값으로 초기화되고, 할당된 메모리 공간은 해당 클래스의 인스턴스가 생성될 때까지 유지된다.
- 해석 단계
- 클래스의 상수 풀(constant pool)에서 필요한 심볼릭 참조(symbolic reference)를 실제 메모리상의 레퍼런스로 교체하는 과정이다.
- 레퍼런스를 연결한다는 말은, 예를 들어 클래스 A가 클래스 B를 참조한다고 가정해보자. 이때 링크과정에서 A클래스의 코드 내에서 B클래스를 참조하는 부분을 찾아, B클래스가 로딩되는 메모리에 올라가고 초기화된 후에 그 참조를 실제로 연결한다. 이렇게 참조된 클래스가 나중에 사용될때 정적멤버가 초기화되고, 인스턴스 생성시에는 인스턴스 멤버가 초기화된다.
- 상수 풀이란, 클래스 파일 내부에 있는 상수들을 모아 놓은 것으로, 클래스 파일 내부에서 사용되는 모든 상수들이 저장되어 있다. 이때, 상수 풀의 정보들은 미리 해석되어 있어야 실행 중에 바로 사용할 수 있다.
- 심볼릭 참조 : 클래스나 인터페이스의 이름, 메서드 등의 이름을 나타낸다. 심볼릭 참조는 클래스 로더가 클래스를 읽어올 때, 해당 클래스나 인터페이스, 필드, 메소드의 실제 메모리 주소를 찾기 위해 사용된다. 이때, 심볼릭 참조는 링크 단계에서 레퍼런스를 해결하는데 사용된다. 즉, 클래스나 인터페이스, 필드, 메서드 등을 찾아서 런타임 상수풀(Constant Pool)에 저장하고, 필요한 경우 런타임 상수풀에서 참조를 이용하여 해당 요소를 사용한다.
- 검증 단계
- 클래스가 메모리에 로딩된 후에 실행되는 단계이다. 클래스 파일의 정보를 분석하여 해당 클래스가 참조하고 있는 다른 클래스, 메서드, 변수 등의 레퍼런스(참조)를 연결한다.
- 초기화
- 클래스의 정적변수(static variable)와 클래스의 정적블록(static block)이 초기화 되는 단계
- 정적 변수는 클래스가 로딩되는 과정에서 메모리에 할당된다. 그리고 이 변수들은 초기화 전에 기본값으로 초기화 된다. (예를 들어 정수형 변수는 0, boolean 타입은 false로 초기화된다.) 이후, 정적 변수가 명시적으로 초기화된다.
- 클래스의 인스턴스가 생성될 때는 인스턴스 변수와 인스턴스 블록이 초기화되는데, 이를 객체 초기화 단계라고 한다. 반면, 초기화 단계는 클래스 자체의 초기화 단계를 의미한다.
- 초기화 단계에서는 인스턴스 멤버가 생성되는 것이 아니라 정적(static) 멤버가 생성된다.
- 정적 멤버는 클래스 로딩 시에 초기화 되고, 인스턴스 멤버는 new 등을 통해 객체가 생성될 때 초기화힌다. 따라서 클래스 로딩은 프로그램 시작 시 한번만 일어나게 되지만, 객체는 여러개가 생성될 수 있다.
2️⃣ Execution Engine (실행 엔진)
클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다.
- 바이트 코드를 실제 JVM 내부에서 기계가 실행할 수 있는 형태로 변경
☑️ Interpreter (인터프리터)
바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행한다.
(바이트 코드 -> 기계어 변환 및 실행)
- JVM 안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작
- 같은 메소드라도 여러번 호출이 된다면 매번 해석하고 수행해야 되서 전체적인 속도는 느리다.
☑️ JIT Compiler (Just-In-Time 컴파일러)
자주 사용되는 바이트코드를 기계어로 변환하여 후속 실행 시 성능을 개선
위의 Interpreter의 단점을 보완하기 위해 도입된 방식으로, 반복되는 코드를 발견하여 바이트 코드 전체를 컴파일하여 기계어로 변경하고 이후에는 해당 메서드를 더 이상 인터프리팅 하지 않고 캐싱해 두었다가 기계어로 직접 실행하는 방식이다.
하나씩 인터프리팅하여 실행하는것이 아니라, 컴파일된 기계어를 실행하는 것이기 때문에 전체적인 실행 속도는 인터프리팅 방식보다 빠르다.
하지만 바이트코드를 기계어로 변환하는 데에도 비용이 소요되므로, JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고 인터프리터 방식을 사용하다 일정 기준이 넘어가면 JIT 컴파일 방식으로 명령어를 실행하는 식으로 진행한다.
☑️ Garbage Collector (GC)
heap 영역에서 더는 사용하지 않는 메모리를 자동으로 회수
- GC가 자동으로 메모리를 회수하는데, 메모리가 정확히 언제 해제되는지 알 수가 없고, 이를 제어할 수도 없음
- Full GC가 발생하는 경우, GC를 제외한 모든 스레드가 중지되기 때문에 장애가 발생할 수 있다.
⭐ STW (Stop The World)
- 가비지 컬렉션을 하는 동안은 GC 관련 thread를 제외한 모든 thread는 멈추게 되어 있어 서비스 이용에 차질이 생길 수 있다. (이를 Stop-The-World, STW라고 한다.)
- 이 시간을 최소화 하는 것이 쟁점이다. (익스플로러는 STW가 자주 발생해서 문제가 있었음)
⭐ Garbage Collecton 동작 과정
- 가비지 컬렉션 대상
- Reachable : 객체가 참조되어 있는 상태
- Unreachable : 객체가 참조되고 있지 않은 상태 (GC의 대상이 됨)
- 가비지 컬렉션 청소 방식 (Mark and Sweep)
- 가비지 컬렉션이 될 대상 객체를 식별(Mark)하고 제거(Sweep)
- Minor GC
- Young 영역은 짧게 살아남는 메모리들이 존재하는 공간이다. 모든 객체는 처음에는 Young에 생성되는데, 이 공간은 Old에 비해 ‘상대적으로 작기 때문에 메모리를 제거하는데 적은 시간’이 걸린다. 따라서 이 공간에서 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸린다.
- 처음 생성된 객체는 Eden에 위치
- Eden 영역이 꽉 차게 되면 Minor GC 실행
- Mark 동작을 통해 reachable 객체 탐색 (mark)
- Eden 영역에서 살아남은 객체는 1개의 Survivor 영역으로 이동
- Eden영역에서 unreachable 상태의 객체의 메모리 해제 (sweep)
- 살아남은 객체들 Survivor 영역에 존재하며, age 값 1 증가
- 또 다시 Eden영역에 새로운 객체들로 가득 차면 다시 한번 minor GC 발생하고 mark한다.
- Eden와 d에서의 Survivor에서 mark가 된 객체들은 비어있는 Survivor(d가 아닌 Survivor)으로 이동하고 sweep
- 다시 살아남은 모든 객체들은 age가 1씩 증가, 이 과정 반복
- Young 영역은 짧게 살아남는 메모리들이 존재하는 공간이다. 모든 객체는 처음에는 Young에 생성되는데, 이 공간은 Old에 비해 ‘상대적으로 작기 때문에 메모리를 제거하는데 적은 시간’이 걸린다. 따라서 이 공간에서 메모리 상의 객체를 찾아 제거하는데 적은 시간이 걸린다.
- Major GC (Full GC)
- Old는 길게 살아남은 메모리들이 존재하는 공간이다. 이들은 Young에서 시작해서 age가 임계값을 달성하여 Old로 이동한 객체들이다.
- Major GC는 객체들이 계속 쌓이다가 Old에서 메모리가 부족해지면 발생한다.
- Old는 Young보다 상대적으로 큰 공간을 가지고 있어 객체 제거에 많은 시간이 걸린다. (여기서 STW문제가 발생)
GC 종류 | Minor GC | Major GC |
대상 | Young Generation | Old Generation |
실행 시점 | Eden 영역이 꽉 찬 경우 | Old 영역이 꽉 찬 경우 |
실행 속도 | 빠름 | 느림 |
3️⃣ Runtime Data Area
자바 프로그램이 실행되면, JVM은 OS로부터 메모리를 할당받고, 그 메모리를 용도에 따라서 여러 영역으로 나누어 관리를 한다.
☑️ Method(Static) 영역
JVM이 시작될 때 생성되는 공간으로 바이트 코드(.class)를 처음 메모리 공간에 올릴때 초기화되는 대상을 저장하기 위한 메모리 공간
- 프로그램 시작~종료까지 메모리에 남아 있음. 따라서 static 데이터를 무분별하게 많이 사용할 경우 메모리 부족 현상이 일어날 수 있다.
- JVM이 읽어들인 클래스와 인터페이스에 대한 클래스(static) 변수, 메서드, 생성자, 메서드 바이트 코드, 런타임 상수 풀 등을 저장
- 모든 Thread가 공유하는 영역이라 다음과 같이 초괴화 코드 정보가 저장된다.
(static 필드와 클래스 구조 저장)- Field info : 멤버 변수의 이름, 데이터 타입, 접근 제어자의 정보
- Method info : 메소드 이름, return 타입, 매개변수 타입, 접근 제어자의 정보
- Type info : Class 인지 Interface 인지 여부 저장, Type의 속성, 이름 Super Class의 이름
⭐ Runtime Constant Pool (런타임 상수 풀)
- 메서드 영역에 존재하는 별도의 관리 영역
- 각 클래스/인터체이스 마다 별도의 Constant Pool 테이블이 존재하는데, 클래스 생성할 때 참조해야할 정보들을 상수로 가지고 있다.
- JVM은 이 Constant Pool을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
- 정리하면 상수 자료형을 저장하여 참조하고 중복을 막는 역할을 수행한다.
☑️ Heap 영역
JVM이 관리하는 프로그램 상에서 데이터를 저장하기 위해 런타임 시 동적으로 할당하여 사용하는 영역
- 참조 타입 데이터 타입을 갖는 객체(인스턴스), 배열 등이 저장
- 단, heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수는 stack에 적재
- stack과 달리 메모리가 호출이 끝나더라도 삭제되지 않고 유지된다.
그러다 어떤 참조 변수도 heap 영역에 있는 인스턴스를 참조하지 않게 되면, GC(Garbage Collector)에 의해 메모리에 청소된다.
☑️ Stack 영역
메소드 내에서 정의하는 기본 자료형에 해당하는 지역변수, 매개변수의 데이터 값이 저장되는 공간
- 메소드가 호출될 때 스택 영역에 스택 프레임(LIFO)이 생기고, 그 안에 메소드를 호출
⭐ 스택 프레임 (Stack frame)
하나의 메서드에 필요한 메모리 덩어리
- 하나의 메서드 당 하나의 스택 프레임이 필요
- 메서드를 호출하기 직전, 스택 프레임을 자바 stack에 생성한 후 메서드를 호출한다.
- 스택 프레임에 쌓이는 데이터는 메서드의 매개변수, 지역변수, 리턴값 등이 있다.
- 메서드 호출 범위가 종료되면 스택에서 제거된다.
☑️ PC 레지스터
쓰레드가 시작될 때 생성되며, 현재 수행중인 JVM 명령어 주소를 저장하는 공간
JVM 명령의 주소는 쓰레드가 어떤 부분을 무슨 명령으로 실행해야할 지에 대한 기록을 가지고 있다.
자바는 OS나 CPU의 입장에서는 하나의 프로세스이기 때문에, CPU는 가상 머신(JVM)의 리소스를 이용해야 한다. (⭐ JVM은 Java 바이트코드를 해석하고 관리하는 역할을 하지만, 최종적으로는 CPU가 모든 연산을 처리한다)
그래서 자바는 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 PC Register라는 메모리 영역을 만들게 된 것이다.
따라서 JVM은 스택에서 비연산값 Operand를 뽑아 별도의 메모리 공간인 PC Register에 저장하는 방식을 취한다.
실행 흐름:
- JVM은 바이트코드를 해석하여 명령어를 실행한다.
- JVM의 PC 레지스터가 업데이트되면, JVM은 해당 주소에 있는 바이트코드를 읽어와 CPU에 전한다.
- CPU는 이 바이트코드를 실행하고, 실행이 완료되면 다시 JVM으로 제어가 돌아온다.
- JVM은 다음 명령어를 실행하기 위해 PC 레지스터를 업데이트하고, 이 과정을 반복한다.
만약에 스레드가 자바 메소드를 수행하고 있으면 JVM 명령(Instruction)의 주소를 PC Register에 저장한다. 그러다 만약 자바가 아닌 다른 언어(C언어, 어셈블리)의 메소드를 수행하고 있다면, undefined 상태가 된다. 왜냐하면 자바에서는 이 두 경우를 따로 처리하기 때문이다. 이 부분이 바로 뒤에 언급하게 될 Native Method Stack 공간이다.
☑️ 네이티브 메서드 스택 (Native Method Stack)
자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌 실제 실행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역
네이티브 코드(기계어)를 실행하기 위한 공간이다. JIT 컴파일러에 의해 변환된 기계어가 여기에서 실행이 된다.
일반적으로 메소드를 실행하는 경우 JVM 스택에 쌓이다가 해당 메소드 내부에 네이티브 방식을 사용하는 메소드가 있다면 해당 메소드는 네이티브 스택에 쌓인다. 그리고 네이티브 메소드가 수행이 끝나면 다시 자바 스택으로 돌아와 다시 작업을 수행한다. 이때, JNI(Java Native Interface, 네이이브 코드를 호출할 수 있게 해주는 프레임워크)를 사용한다.
네이티브 메소드 호출 전에 현재 Java 바이트코드의 실행 상태(즉, 다음에 실행할 바이트코드의 주소)가 스택 프레임에 저장된다. 이 저장된 정보는 네이티브 메소드가 완료된 후, 다시 Java 바이트코드의 실행 흐름으로 복귀하는 데 사용된다. (PC 레지스터의 업데이트 등)
💡 참고
https://st-lab.tistory.com/176
https://inpa.tistory.com/entry/JAVA-☕-그림으로-보는-자바-코드의-메모리-영역스택-힙
https://inpa.tistory.com/entry/JAVA-☕-JVM-내부-구조-메모리-영역-심화편
https://inpa.tistory.com/entry/JAVA-☕-JDK-JRE-JVM-개념-구성-원리-💯-완벽-총정리
https://inpa.tistory.com/entry/JAVA-☕-가비지-컬렉션GC-동작-원리-알고리즘-💯-총정리
https://velog.io/@taebong98/자바-컴파일-과정-소스코드부터-실행까지
'☕ Java' 카테고리의 다른 글
PriorityQueue, Comparator (0) | 2024.12.12 |
---|---|
스트림, BufferedReader vs. Scanner (0) | 2024.12.12 |