- 학습 목표
- JVM,JDK,JRE - 뭐가 다른지 이해하기
- JVM 구조 파악하기
- 클래스로더 구조 파악하기
- 메모리 구조 파악하기
- 실행엔진 구조 파악하기
자바를 배움에 앞서서 먼저 코딩으로 쳐보면서 파악하시는 분도 있고, 자바라는 언어의 특징이 다른 언어들과 뭐가 다른지, 자바 언어 프로그램은 어떻게 돌아가는지 궁금해 하는 사람들이 있을거 같아 해당 글을 정리합니다.
먼저 들어가기에 앞서서 JVM과 JRE, JDK 가 무엇인지 간략하게 특징을 파악해보겠습니다.
JVM
Java Virtual Machine
- 자바 가상 머신으로 자바 바이트코드인 .class 파일을(어떻게 실행 시킬지에 대한 스펙) OS에 특화된 코드로 변환한다. (인터프리터와 JIT 컴파일러)를 통해 실행한다.
- 구현체는 다양하다
- 특정 플랫폼에 종속적이다.
- 왜냐면 네이티브 코드에 맞춰서 실행을 해야하는데 네이티브 코드라는게 OS에 맞춰서 실행되어야 하기 때문에
JRE
Java Runtime Enviornment (JVM + 라이브러리)
- JRE는 자바 애플리케이션을 실행할 수 있는 목적으로 구성된 배포판이다.
- 실행을 바이트코드로 하기 때문에 JRE는 JVM을 포함하고 있다.
- Java의 핵심 라이브러리를 포함하고있다.
- 자바를 실행하는데 필요한 것들을 포함하고 있으며, 개발을 하는데 필요한 것들은 포함하고 있지 않다.
- 개발 관련 도구는 JDK에서 제공한다.
JDK
Java Development Kit : JRE + 개발 툴
- JRE + 개발에 필요한 관련 툴을 가지고 있다.
- 소스 코드를 작성 할 때 사용하는 자바 언어는 플랫폼에 독립적이다.
- Oracle은 자바 11부터 JDK만 제공하며 JRE를 따로 제공하지 않는다.
- Write Once Run Anywhere 특징을 가지고 있다.(자바는 어디선가 한번 컴파일 했으면, OS를 옮겨가도 다시 컴파일 할 필요가 없음.)
최초의 JVM은 원래 자바 언어 만을 지원하도록 만들어 졌었다.
허나 JVM이 사실상 자바 언어가 직접적인 연관관계가 있는 것이 아니라, 중간에 .class 파일로 실행을 시켜주기 때문에 Java와 의존성이 그리 깊게 종속적이지 않다.
그래서 자바외에 다른 언어로 소스코드를 작성했을 때 해당 언어가 .class 파일을 제공한다면 JVM으로도 실행 시킬 수 있다는 것이다.
그래서 이들의 관계를 봐보면
자바 런타임 환경인 JRE가 바이트 코드를 실행하기 위해 JVM을 포함하고 있으며, Library를 포함하고 있는 이러한 관계가 나온다.
JVM 구조
클래스 로더 시스템
자바언어에서는 개발자가 작성한 소스코드를 실행시킬때 확장자명이 .java 파일에서 실행시키고 난 후에는 컴파일이 완료된 .class 파일로 확장자명이 바뀌된다.
그럼 그 바이트코드를 메모리에 적절히 배치하는게 클래스 로더가 하는 일이다.
먼저 간단하게 java 파일이 class 파일로 바뀌면서 소스코드가 바이트 코드로 변화는 과정을 살펴보도록 하겠다.
이 때 .java 파일에는 개발자가 작성한 소스코드가 있지만 .class 파일에는 JVM위에서 실행 될 바이트코드로 작성이 되어있다.
public class CardGameMain {
public static void main(String[] args) {
GameManager gameManager = new GameManager();
Player player1 = new Player("플레이어1");
Player player2 = new Player("플레이어2");
List<Card> shuffleCards = gameManager.getShuffleCards();
player1.pickCard(shuffleCards);
player2.pickCard(shuffleCards);
Player winner = gameManager.processDecidingWinner(player1, player2);
System.out.println(winner);
}
}
이러한 소스코드가 있다고 가정 시 현재 시점에서 해당 파일의 확장자 명은 .java
이다.
터미널(MacOS의 커맨드)에서 명령어로 컴파일 후 .class
파일이 생성되는데 해당 파일을 확인해 보자.
- javac {컴파일할 .java 파일이 있는 경로를 설정해준다.}
- javap -c {1번 과정을 거쳤다면 .java -> .class 파일이 생겼을 것이다.}
결과
해당 사진이 위의 CardGameMain.java
소스코드를 컴파일하여 생성된 .class 파일의 바이트코드를 확인한 것이다.
클래스 로더 시스템이 하는일
크게 3가지의 일을 한다고 볼 수 있다.
- 로딩:
- JVM은 .class 파일을 메모리로 로드한다.
- 클래스 로더가 .class 파일을 읽고 메모리에 적재하며, 필요한 다른 클래스들도 로드.
- 링킹 및 초기화:
- 로드된 클래스의 바이트코드를 검증하고 필요한 준비 작업을 수행.
- 클래스의 정적 필드가 메모리에 할당되고 초기화.
- 메서드 호출 시 필요한 심볼릭 참조를 해결.
- 실행:
- JVM의 인터프리터 또는 JIT(Just-In-Time) 컴파일러가 바이트코드를 기계어로 변환하여 실행.
- JIT 컴파일러는 바이트코드를 기계어로 실시간으로 컴파일하여 성능을 최적화.
.java 파일이 컴퓨터에서 실행되는 흐름은 대략 이러하다.
.java 파일 컴파일 -> .class 바이트코드 변환 -> JIT 컴파일러가 기계어로 변환하여 실행.
메모리
메모리는 총 5개의 영역을 가진다.
Stack 영역, PC register, 네이티브 메소드 스택, 힙, 메소드 영역으로 나뉘어져 있다.
그리고 각 영역에서 어떠한 일들을 하는지 살펴보겠다.
Method
메소드 영역에는 클래스 수준의 정보들이 포함된다.
예를 들어 클래스의 이름, 부모 클래스의 이름(부모 클래스가 없다면 모든 클래스의 상위 객체인 Object), 메서드, 그리고 해당 클래스에서 사용된 변수들을 저장하며, 공유자원으로써 메모리의 메서드 영역에 올라가게 된다.
Heap
힙 영역에는 객체들이 저장되게 된다. 이 역시 공유자원에 속하게 된다.
Stack
스택 영역에는 쓰레드 마다 Runtime Stack을 만들고, 그 안에 메소드 호출을 Stack Frame이라 부르는 블럭으로 쌓는다. 쓰레드를 종료하면 런타임 스택도 함께 사라진다.
Stack Frame 이란 쉽게 말해 : Method Call을 뜻합니다.
예를 들어 아래 사진과 같은 에러 메시지 같은 것들이 Call Stack이며, Stack 영역안에 이러한 쓰레드를 하나씩 생성한다.
그리고 그렇게 만들어진 Stack 영역에 Method Call을 쌓았는데, 현재 어느 위치를 실행시키고 있는지를 가리키는 PC register
가 생긴다. 쓰레드를 만들 때마다 생성이 된다.
PC(Program Counter) registers
쓰레드 마다 쓰레드 내 현재 실행할 스택 프레임을 가리키는 포인터가 생성이 된다.
(현재 실행 중인 JVM 명령의 주소를 저장.)
Native Method Stack
네이티브 메서드 라이브러리의 기능을 사용하기 위해서는 네이티브 메소드 인터페이스(JNI)를 호출하는데 네이티브 메서드 사용시에 쌓이게 되는 영역이다.
자바 애플리케이션에서 C, C++ 언어로 작성된 함수를 사용할 수 있는 방법을 제공한다.
예를 들어 위 사진과 같이 작성된 코드 들을 일컫는다.
해당 코드는 Java로 구현된 것이 아닌 C,C++로 구현된 것이다.
그리고 메모리 영역에서 힙과 메서드 영역은 공유자원에 속하게 되고, 나머지 Stack, PC, Native Method Stack은 특정 쓰레드에 국한된 것이다.
그리고 하나의 쓰레드에는 이러한 정보가 담긴다.
여기서 이제 더이상 참조하지 않는 값이 나오게 된다면 GC가 가져가는 것이다.
실행엔진
실행과 관련된 JVM의 구조에는 총 3가지 영역이 존재한다.
인터프리터
바이트 코드를 한줄 씩 실행 하는 역할을 담당한다.
위에 사진에서 보았듯이 컴파일을 통해 바이트 코드로 변환된 .class 파일을 실행해보았다.
그럼 저기서 인터프리터가 해당 바이트 코드를 한줄 씩 실행하게 된다.
JIT 컴파일러
인터프리터의 효율을 높이기 위해 사용되는데, 인터프리터는 바이트 코드를 한줄 씩 실행하게 된다.
근데 만약 코드가 중복이 된다면 이미 읽은 코드를 한번 두번... N번 반복해서 일겍되는데, 효율이 좋지 못하다.
그래서 JIT 컴파일러는 반복되는 코드를 모두 Native 코드로 바꿔둔다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용함으로써 성능을 최적화 시킨다.
GC
Garbage Collector 영어 단어 그대로 쓰레기를 모으는 녀석이다.
실행엔진 부분에 속하는 GC는 더 이상 참조하지 않는, 즉 사용이 되지 않는 객체를 모아서 정리하는 역할을 수행한다.(GC가 있다고 해서 Java에서 메모리 누수가 안일어나는 것은 아니다.)
정리 및 요약
크게 흐름만 보자면
클래스로더 : 파일을 읽는다.
메모리 : 클래스 로더가 읽은 정보를 토대로 데이터를 메모리의 각 영역에 배치한다. 실행 시 쓰레드가 생성이 된다.(메서드, 힙은 공유자원으로써, Stack, PC, 네이티브는 해당 쓰레드에 생성.)
실행 엔진 : 메모리의 각 영역에 배치된 정보를 가지고 인터프리터가 바이트 코드를 한줄 씩 읽어 실행한다. 그리고 중복된 코드는 성능의 효율을 위해 미리 네이티브 코드로 변환하여 JIT 컴파일러가 수행한다. 남는 리소스는 GC가 처리한다.
A.java 파일 실행시
A.java
파일에 소스 코드 작성.javac A.java
명령어로 소스 코드 컴파일,A.class
파일 생성.java A
명령어로 JVM에서 바이트코드 실행.- 클래스 로더가
A.class
파일 로드. - 바이트코드 검증.
- 실행 엔진이 바이트코드 실행 (인터프리터와 JIT 컴파일러 사용).
- "Hello, World!" 출력.
그래서 JVM은 이러한 구조를 가지게 된다.
맨 처음에 자바라는 언어를 배울 때 코드를 치기만 할 뿐 내부적으로 어떻게 코드가 실행되는지 궁금해하지 않았던거 같다.
그래도 자바라는 언어를 배움으로써 이러한 기본적인 과정을 이해하고, 자바가 어떻게 나의 운영체제에 맞게 코드를 실행시키는지 이해하는데 도움이 되었으면 한다.
'Java' 카테고리의 다른 글
Reflection API 학습후, DI 프레임워크 만들기 (0) | 2024.07.13 |
---|---|
[Java] 자바의 제네릭이란, Generic, 타입 매개변수 (0) | 2024.05.21 |
[Java] StreamAPI (0) | 2024.04.29 |
[Java] Optional (0) | 2024.04.02 |
[Java]Interface란? 그리고 interface와 Abstract의 차이. (0) | 2024.03.16 |