JDK -> 자바 개발 도구
JRE -> 자바 실행 환경
JVM -> 자바 가상 기계
JDK를 이용해서 개발된 프로그램은 JRE에 의해 JVM 상에서 구동됩니다. JDK는 자바 소스 컴파일러인 javac.exe를 포함하고 있습니다. JRE는 자바 프로그램 실행기인 java.exe를 포함하고 있습니다. 자바 개발자는 본인이 사용중인 플랫폼에 설치된 JVM용으로 프로그램을 작성하고 배포하면 각 플랫폼에 맞는 JVM이 중재자로서 각 플랫폼에서 프로그램을 구동하는 데 아무 문제가 없게끔 만들어줍니다.
이러한 자바의 특성을 write once run anywhere라고 합니다.
JVM 구성
Class Loader
.class(byte code)를 실제 메모리에 적재하는 역할을 합니다.
Excution Engine
바이트 코드를 읽고 CPU가 해석 가능한 기계어로 번역해서 실행하는 역할을 합니다. 이때 사용하는 방식이 2가지인데, 인터프리터와 JIT 컴파일러라고 부릅니다.
Runtime Data Area (Memory)
JVM이 OS 위에서 실행되면서 할당받는 메모리 영역입니다. pc 레지스터, JVM 스택, 네이티브 메서드 스택은 스레드마다 하나씩 생성되고, 힙과 메서드 영역은 모든 스레드가 공유해서 사용합니다.
JNI (Java Native Interface)
Java에서 C, C++, 어셈블리로 작성된 함수를 사용할 수 있는 방법을 제공합니다.
JVM 동작과정
- 프로그램이 실행되면 JVM은 OS로부터 메모리를 할당받습니다.
- 자바 컴파일러가 자바 소스코드를 자바 바이트코드로 컴파일합니다.
- Class Loader가 class 파일을 JVM으로 적재합니다.
- 적재된 class 파일을 Excution engine을 통해 해석합니다.
- 해석된 바이트코드는 Runtime Data Area에 배치되고 실질적인 수행이 이루어집니다.
- 동작과정 중 JVM은 필요에 따라 Thread Synchrronization, Garbage Collectior 같은 관리 작업을 수행합니다.
메모리 사용 방식
하나의 프로그램이 실행될 때, 프로그램은 메모리의 코드 실행 영역과 데이터 저장 영역을 사용하게 됩니다. 데이터 저장 영역은 다시 3가지로 나뉘어집니다. static 영역, stack 영역, heap 영역으로 나뉘어집니다.
콘솔에 간단하게 "Hello World"를 찍는 프로그램이 있다고 생각해봅시다.
public class Hello {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
JRE는 프로그램 안에 main 메서드를 확인합니다. main 메서드를 발견하게 돼면 JRE는 가상 머신인 JVM을 부팅합니다. JVM은 class 파일을 받아 실행합니다. 이때 JVM은 전처리 과정을 진행합니다. java.lang 패키지를 static 영역에 적재합니다. 그 후 개발자가 작성한 모든 클래스와 import 패키지들을 static 영역에 적재합니다. 위 예시에서는 Hello 클래스 하나뿐이기 때문에, Hello 클래스만 static 영역에 적재됩니다. 이렇게 전처리 과정이 끝납니다.
그 다음 main 메서드가 stack 영역에 할당됩니다. 그리고 메서드의 인자 args를 저장할 변수 공간을 main 스택 프레임 밑에 확보해줍니다. 그러면 드디어 코드 실행 영역에서 System.out 구문이 실행되고 콘솔에 "Hello World"가 찍히는 것을 확인할 수 있습니다. 해당 구문을 실행하고 메서드를 닫는 중괄호를 만나게 되면 stack 영역에서 main 스택 프레임이 소멸됩니다. main 메서드가 끝나면 JRE는 JVM을 종료하고 JRE 자체도 운영체제 상의 메모리에서 사라집니다.
그러면 여기서 main 메서드 안에 지역 변수가 있다면 어떻게 메모리를 사용할까?
main 스택 프레임 안에 해당 변수 공간을 마련합니다. 그런데 초기화 되지 전에는 해당 변수에 알수 없는 값이 들어갑니다. 이전에 해당 공간의 메모리를 사용했던 다른 프로그램이 청소하지 않고 간 값을 그대로 가지고 있게 됩니다. 그 후 해당 공간에 변수를 초기화해주게 됩니다.
다음으로 생각해볼 수 있는 것이 블록 구문입니다. main 메서드 안에 블록 구문이 있다면?
블록구문을 만나게 되면 main 스택 프레임 안에 해당 블록 스택 프레임이 만들어집니다. 그리고 그 안에 지역 변수는 main 스택 프레임과 같은 방식으로 만들어지게 됩니다. 그 후 블록 구문을 닫는 중괄호를 만나게 되면 해당 블록 스택 프레임은 main 스택 프레임 안에서 사라지게 됩니다.
다음은 메서드를 호출할 경우입니다.
public class Hello {
public static void main(String[] args) {
int a = 1;
int b;
b = test(a);
}
}
private static int test(int a) {
int result;
a = 5;
result = a;
return result;
}
3번째 줄까지 이전 설명과 같습니다. 다시 돌아가면 JVM에서 전처리 과정으로 java.lang 패키지와 개발자가 작성한 class와 import 패키지를 static 영역에 적재합니다. 그후 stack 영역에 main 스택 프레임이 생기고 그 안에 args 변수를 담을 공간이 생깁니다. 마지막으로 지역 변수 a와 b의 공간이 만들어집니다.
6번째 줄에서 함수를 호출합니다. 그러면 stack 영역에 test 스택 프레임이 만들어집니다. 그 안에 반환값을 저장할 변수 공간이 가장 아래에 만들어지고, 인자를 저장할 변수 공간, 메서드의 지역 변수 공간이 만들어집니다.
여기서 중요한 것은 main 스택 프레임에 있는 a 변수와 test 스택 프레임에 있는 a 변수는 다른 변수 공간이라는 것입니다.
test 스택 프레임도 닫는 중괄호를 만나게 되면 stack 영역에서 사라지고 반환값을 넘겨주게 됩니다.
여기서 알 수 있는 것은 main 메서드와 test 메서드는 서로 각 메서드의 지역 변수에 접근할 수 없습니다. 입력 값들과 반환값에 의해서만 메서드 사이에서 값이 전달될 뿐 서로 내부의 지역 변수를 볼 수 없습니다. 이것을 블랙박스화라고 합니다.
다음으로 만약 main 메서드 안에 static 변수가 있다면 어떻게 메모리에 저장될까?
static 변수는 static 영역에 변수 공간이 할당됩니다. class가 정의 될 때 static 변수 공간이 만들어집니다. static 변수는 전역 변수로서 활용할 수 있습니다. 읽기 전용으로 값을 공유해서 전역 상수로 쓸 때 추천합니다.
다음으로 멀티 스레드와 멀티 프로세스일 때 메모리 구조를 확인해보겠습니다.
멀티 스레드는 stack 영역을 스레드 개수만큼 분할해서 씁니다. 이와 다르게 멀티 프로세스는 다수의 데이터 저장 영역을 갖습니다. 즉, 멀티 스레드는 데이터 저장 영역이 하나이지만, 멀티 프로세스는 데이터 저장 영역이 여러 개입니다.
멀티 프로세스는 각 프로세스마다 고유의 데이터 저장 영역을 갖고 있기 때문에 서로 참조할 수 없습니다. 하지만 멀티 스레드는 하나의 데이터 저장 영역을 사용하는데 stack 영역만 분할해서 사용하는 구조입니다. 즉, static 영역과 heap 영역은 공유가 가능합니다. 그렇기 때문에 멀티 프로세스보다 메모리를 적게 사용할 수 있습니다.
- 출처
메모리 동작 원리:
김종민, 스프링 입문을 위한 자바 객체 지향의 원리와 이해
JVM 구성에 대해서 :
'JAVA' 카테고리의 다른 글
객체 지향의 4대 특성 (0) | 2021.10.03 |
---|---|
객체 지향 설계의 5가지 원칙 (SOLID) (0) | 2021.10.03 |
Java 변수 (0) | 2021.09.10 |
클래스와 객체 (0) | 2021.09.09 |
객체 지향 언어란? (Object Oriented Language) (0) | 2021.06.17 |