Reference의 참고 서적과 우테코의 테코톡 영상을 참고하여 정리한 내용입니다.
자바 가상 머신(Java Virtual Machine)을 줄여서 JVM이라고 부른다. JVM은 자바 코드를 실행하는 환경을 말한다.
좀 더 쉽게 이해하기 위해, 현실 세계와 함께 보겠다.
현실 세계에서는 컴퓨터를 구동하기 위해 아래의 요소들이 필요하다
자바의 가상 세계는 이러한 현실 세계를 그대로 모방하고 있다고 보면 된다.
현실 세계 | 자바의 가상 세계 |
---|---|
개발 도구 | JDK (자바 개발 도구) |
운영체제 (실행 환경) | JRE |
하드웨어 | JVM |
즉, 자바의 가상 세계에서는 자바 개발 도구인 JDK를 이용해 개발한 프로그램은 JRE에 의해 가상의 컴퓨터인 JVM 상에서 구동된다.
C/C++는 컴파일 플랫폼과 타겟 플랫폼이 다를 경우 프로그램이 동작하지 않는 문제가 있었다.
💡 여기서 플랫폼이란, 운영체제와 CPU 아키텍처를 말한다. 환경이라고도 한다.
💡 Write Once, Run Anywhere (WORA) - Sum Microsystems
자바는 네트워크에 연결된 모든 디바이스에서 작동하는 것이 목적인데, 플랫폼마다 컴파일해줘야 하는 크로스 컴파일을 하기에는 디바이스가 너무 다양했다.
따라서 플랫폼과 관련된 작업을 대신 해주는 JVM을 만든 것이다.
개발자는 소스를 컴파일한 후 윈도우용 JVM이나 맥용 JVM 등 해당 운영체제에 맞는 JVM을 설치하기만 하면 된다.
JVM의 구조를 알기 전에, 프로그램이 메모리를 사용하는 방식을 알아야 한다.
하나의 프로그램이 실행될 때, 기계어를 포함한 모든 프로그래밍 언어는 두 영역으로 메모리를 나누어 사용한다.
여기서 코드 실행 영역은 다루지 않을 것이다. 이는 운영체제나 언어 자체를 개발하려는 것이 아니면 깊게 볼 필요가 없다.
객체 지향 프로그램에서는 데이터 저장 영역을 다시 세 개의 영역으로 분할해서 사용한다.
『스프링 입문을 위한 자바 객체 지향의 원리와 이해』에서는 이 영역들을 T 영역이라고 명명했다.
응용 프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받는다.
그리고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.
JVM의 내부 영역은 아래 영역으로 나뉜다.
이 글에서는 메모리 영역만 볼 것이기 때문에, 위의 두 가지 영역만 확인한다.
글로 봐서는 알기 힘드니 그림으로도 확인하자.
💡 출처: The JVM Architecture Explained - DZone
이 이미지가 가장 디테일해서 가져왔는데, 출처는 아무리 찾아봐도 좀 불분명했다. 모두 위 링크에서 이미지를 가져왔다고 하는데, 접속해보면 이미지가 뜨지 않는다.
이미지가 삭제된 것일까…
Class Loader System은 클래스 파일 정보를 메모리에 올리고 검증하고 Static 변수들을 초기화하는 등의 역할을 한다.
이때, 클래스 파일 안에는 클래스에 대한 모든 정보가 들어있다.
💡 바이트 코드란
JVM이 알아들을 수 있는 명령어 집합을 말한다. JVM의 목적은 바이트 코드를 기계어로 번역하여, CPU에게 일을 시키는 것이다.이 번역을 인터프리터가 수행하게 된다.
JVM이라는 가상 머신이 사용하는 메모리 공간으로, 다섯 가지 영역으로 구성되어 있다.
클래스에 대한 모든 정보가 저장되는 영역이다.
프로그램 실행 중 어떤 클래스가 사용되었을 경우, 클래스 로더가 해당 클래스 파일(*.class)을 읽어 분석한 뒤 클래스에 대한 정보를 이곳에 저장한다.
이 때, 그 클래스의 클래스 변수(class variable, static으로 선언된 변수)도 이 영역에 함께 생성된다.
프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 즉, 인스턴스 변수(instance variable)들이 생성되는 공간이다.
가비지 콜렉터가 주로 여기서 동작한다.
💡 Method Area와 Heap은 모든 스레드가 공유하는 영역이다. 따라서, Multi-Thread 환경에서는 동기화에 주의해야 한다.
호출 스택(call stack 또는 execution stack)이라고 한다. 메소드의 작업에 필요한 메모리 공간을 제공한다.
호출 스택의 동작은 다음과 같다.
💡 Frame
호출된 메소드를 위해 할당된 메모리를 Frame이라는 자료구조라고 설명한다.
Frame은 메소드가 호출될 때마다 하나 생성되고 메소드가 하나 끝나거나 예외가 발생하면 사라진다.
각 메소드를 위한 메모리 상의 작업 공간은 서로 구별된다.
그림과 함께 예를 들어 보겠다.
메소드1
을 위한 작업 공간이 마련된다.메소드1
수행 중 메소드2
를 호출하면, 메소드1
바로 위에 두 번째로 호출된 메소드를 위한 공간이 마련된다.
이때, 메소드1
은 수행을 멈추고, 메소드2
가 수행되기 시작한다.
메소드2
가 수행을 마치게 되면, 메소드2
의 메모리 공간이 반환되며, 메소드1
은 다시 수행을 계속하게 된다.
메소드1
도 수행을 마치면, 역시 제공되었던 메모리 공간이 호출 스택에서 제거되며 호출 스택은 완전히 비워지게 된다.
즉, 호출 스택의 제일 상위에 위치하는 메소드가 현재 실행 중인 메소드이며, 나머지는 대기 상태에 있게 된다.
따라서 호출 스택의 제일 상위에 위치하는 메소드 간에 호출관계와 현재 수행 중인 메소드가 어느 것인지 알 수 있다.
Current Class Constant Pool Reference
: 현재 클래스의 Constant Pool에 대한 참조, 이전 스택 프레임에 대한 정보, 현재 메소드가 속한 클래스/객체에 대한 참조 등의 정보를 갖는다.
말 그대로, 메소드 내의 지역 변수들을 담고 있는 배열이다. 인덱스로 빠르게 접근할 수 있다.
인스턴스 메소드는 항상 첫 번째 인덱스이다.
예를 들어, 다음과 같은 메소드가 있을 때 지역 변수 배열에는 총 4가지가 들어간다.
private int methodA(int param) { // param으로는 호출 시 3을 넣어준다고 하자.
int localVariable = 1;
int sum = localVariable + param;
methodB();
return sum;
}
private void methodB() {}
JVM은 스택 기반으로 연산을 수행한다. 피연산값 혹은 연산의 중간값들을 저장하기 위한 자료구조이다.
위에서 예로 들었던 methodA
메소드를 실행한다고 했을 경우엔, 아래와 같이 동작한다.
PC는 Program Counter를 말한다. 현재 실행되고 있는 명령어의 추적, 주소를 저장하고 있는 곳이다.
멀티 스레드 환경에서 한 스레드가 작업을 하다가 다른 스레드로 잠시 CPU 점유를 넘겨주고, 다시 돌아왔을 때 이전에 어떤 명령을 수행하고 있었는지를 기억하고 있어야 다음 작업을 이어서 실행할 수 있다.
이를 위해 준비한 영역이다.
C나 C++로 작성된 명령어를 실행할 때 사용하는 스택이다.
💡 JVM Stacks, PC Registers, Native Method Stacks는 스레드가 생성될 때마다 같이 생성되고, 서로 다른 스레드가 침범할 수 없는 영역이다.
따라서 지역변수의 동시성 문제를 걱정하지 않아도 된다.
💡 javap 명령어를 사용하면 .class 파일을 디어셈블리하여, Constance Pool에 어떤 정보가 들어가 있는지 확인할 수 있다.
JRE는 프로그램 안에 main() 메서드가 있는지 확인한 후, JVM에 전원을 넣어 부팅한다. 부팅된 JVM은 목적 파일을 받아 그 목적 파일을 실행한다.
JVM이 맨 먼저 하는 일은 전처리라고 하는 과정이다.
모든 자바 프로그램이 반드시 포함하게 되는 패키지를 스태틱 영역에 가져다 놓는다.
: java.lang
패키지
개발자가 작성한 클래스와 import한 패키지를 스태틱 영역에 가져다 놓는다. (실행 중)
Constant Pool은 클래스 내에서 사용되는 상수들을 담은 테이블이다.
JVM은 메소드 영역에 저장되는 바이트 코드를 해석한다. 즉, main()
를 실행한다.
main()
를 실행하면서, JVM은 Constant Pool에 대한 포인터를 하나 유지시킨다.
스택 프레임은 여는 중괄호를 만날 때마다 하나씩 생기고, 닫는 중괄호를 만나면 소멸된다.
int value = 10;
위와 같은 코드는 사실 하나의 명령문이 아닌 두 개의 명령문이다.
int value ;
value = 10;
위에서는 개발자가 작성한 클래스를 모두 스태틱 영역에 가져다 놓는 것처럼 말했지만, 사실 클래스는 필요할 때 동적으로 클래스 정보가 로드된다.
예를 들어, main()
에서 객체를 하나 생성하는 코드가 있다고 하자.
public class Main {
public static void main(String[] args) {
CustomObject obj = CustomObject();
}
}
CustomObject
클래스의 정보를 읽어 Method Area에 올린다.CustomObject
클래스를 직접 가리키는 참조로 변경한다.CustomObject
객체를 할당하는데 Heap 공간이 얼마나 필요한 지를 알아내기 위해 다시 한 번 Main
클래스의 Constant Pool을 바라본다.변수는 스태틱 영역과 스택 영역, 힙 영역 모두 존재할 수 있다. 그런데, 각 영역에 있는 변수는 각기 다른 목적을 가지며 이름도 다르게 부른다.
main() 메소드 안에서 변수를 하나 더 선언하면 args 위에 쌓는다. (스택)
변수를 선언만 하고 초기화하지 않은 상태에서는 메모리 공간 안에 알 수 없는 값이 들어가 있다. 이전에 해당 공간의 메모리를 사용했던 다른 프로그램이 청소하지 않고 간 값을 그대로 가지고 있는 것이다.
이는 우리에게 쓰레기 값일 뿐이므로, 사용을 시도하면 “초기화되지 않았을 수도 있습니다”라는 경고를 토해낸다.
클래스를 스태틱 영역에 배치하면, 클래스 정보 안에 변수의 이름이 존재한다. 하지만 변수 공간이 따로 존재하는 것이 아닌, 이름만 존재할 뿐이다.
객체가 생성되어야만 속성의 값을 저장하기 위한 메모리 공간이 힙 영역에 할당된다.
static 변수는 T 메모리의 스태틱 영역에 변수 공간이 할당된다.
전역변수는 여러 메서드에서 전역 변수의 값을 변경하기 시작하면 T 메모리로 추적하지 않는 이상 전역 변수에 저장돼 있는 값을 파악하기 쉽지 않다. 따라서, 쓰지 말아야 한다.
다만, 읽기 전용으로 값을 공유하는 경우(전역 상수)는 적극 추천한다.
if문이 있다고 했을 때, 조건이 참인 if의 여는 중괄호를 만나면 블록의 스택 프레임이 생긴다.
예를 들어 main() 스택 프레임
이 있다면, 그 안에 if(true) 스택 프레임
이 중첩되어 생기는 것이다.
if 블록 스택 프레임을 수행하는 중에, if 블록 스택 프레임 외부에 존재하는 변수에는 접근이 가능하다.
아직 메모리 상에 없어 접근할 수 없는 경우도 있지만, 메모리 상에 존재함에도 접근할 수 없다. 이는 자바 스펙에서 금지되어 있으며, 책의 저자는 아래와 같은 이유로 짐작했다.
자바에서는 포인터를 사용할 수 없으므로 결국 언어 스펙상으로도 메서드 스택 프레임 사이에 변수를 참조하는 것은 불가능하다는 결론에 도달할 수 있다.
절차적/구조적 프로그래밍에서 공유 변수를 필드라고 불렀고, 기능적 요소를 함수라고 불렀다. 객체 지향 프로그래밍에서 같은 일을 하지만 차별화를 하기 위해 전역 변수를 프로퍼티라 부르고, 함수를 메서드라 부르기 시작했다고 한다.
객체 지향에서 필드는 객체 변수 또는 정적 변수를 말하고, 속성은 필드를 외부에 노출시키는 메서드라고 하는 사람도 있다.
이 구분을 따른다면 자바에서의 속성은 get/set 메서드라고 할 수 있다.
📔 자바의 정석
📔 스프링 입문을 위한 자바 객체 지향의 원리와 이해
The Java® Virtual Machine Specification
Chapter 4. The class File Format
[Java] 많이 헷갈려하는 String constant pool과 Runtime Constant pool, Class file constant pool
[Java] 런타임 데이터 영역(Runtime Data Area)에 대해
[Java] JVM(Java Virtual Machine) 이해하기 -2 : 메모리 영역(Runtime Data Area)