[Java] JVM과 메모리 구조

공부하는 감자·2024년 1월 25일
0

Java

목록 보기
3/5

들어가는 말

Reference의 참고 서적과 우테코의 테코톡 영상을 참고하여 정리한 내용입니다.

JVM이란?

현실 세계와 자바의 가상 세계

자바 가상 머신(Java Virtual Machine)을 줄여서 JVM이라고 부른다. JVM은 자바 코드를 실행하는 환경을 말한다.

좀 더 쉽게 이해하기 위해, 현실 세계와 함께 보겠다.

현실 세계에서는 컴퓨터를 구동하기 위해 아래의 요소들이 필요하다

  • 물리적 컴퓨터인 하드웨어
  • 운영체제
    • 소프트웨어는 운영체제 위에서 구동된다.
  • 소프트웨어를 개발하는 개발 도구

자바의 가상 세계는 이러한 현실 세계를 그대로 모방하고 있다고 보면 된다.

현실 세계자바의 가상 세계
개발 도구JDK (자바 개발 도구)
운영체제 (실행 환경)JRE
하드웨어JVM

즉, 자바의 가상 세계에서는 자바 개발 도구인 JDK를 이용해 개발한 프로그램은 JRE에 의해 가상의 컴퓨터인 JVM 상에서 구동된다.

기존 컴파일의 문제

C/C++는 컴파일 플랫폼과 타겟 플랫폼이 다를 경우 프로그램이 동작하지 않는 문제가 있었다.

💡 여기서 플랫폼이란, 운영체제와 CPU 아키텍처를 말한다. 환경이라고도 한다.

  • CPU별로 기계어가 달라서 각 기계에 맞는 기계어 목적 파일을 만들기 위해 컴파일러를 사용했다.
  • 그러나 운영체제(OS) 또한 각기 다른 특성이 있기 때문에 같은 소스로 운영체제 맞게 컴파일을 해줘야 한다는 문제가 있다.
    • 맥용 C 컴파일러로 맥용 목적 파일 생성
    • 윈도우용 C 컴파일러로 윈도우용 목적 파일 생성
    • 이렇게 타켓 플랫폼에 맞춰 컴파일하는 것을 크로스 컴파일(Cross Cimpile)이라고 한다.

JVM의 해결

💡 Write Once, Run Anywhere (WORA) - Sum Microsystems

자바는 네트워크에 연결된 모든 디바이스에서 작동하는 것이 목적인데, 플랫폼마다 컴파일해줘야 하는 크로스 컴파일을 하기에는 디바이스가 너무 다양했다.

따라서 플랫폼과 관련된 작업을 대신 해주는 JVM을 만든 것이다.

개발자는 소스를 컴파일한 후 윈도우용 JVM이나 맥용 JVM 등 해당 운영체제에 맞는 JVM을 설치하기만 하면 된다.

  • javac라는 컴파일러로 자바 소스코드를 자바 바이트 코드로 변환
  • 자바 바이트 코드는 JVM이 설치된 플랫폼이라면 어떤 플랫폼이든 상관없이 잘 동작한다.

JVM의 구조

프로그램의 메모리 사용 방식

JVM의 구조를 알기 전에, 프로그램이 메모리를 사용하는 방식을 알아야 한다.

하나의 프로그램이 실행될 때, 기계어를 포함한 모든 프로그래밍 언어는 두 영역으로 메모리를 나누어 사용한다.

  • 코드 실행 영역
  • 데이터 저장 영역

여기서 코드 실행 영역은 다루지 않을 것이다. 이는 운영체제나 언어 자체를 개발하려는 것이 아니면 깊게 볼 필요가 없다.

객체 지향 프로그램에서는 데이터 저장 영역을 다시 세 개의 영역으로 분할해서 사용한다.

  • 스태틱 영역: 클래스들의 놀이터
  • 스택 영역: 메서드들의 놀이터
  • 힙 영역: 객체들의 놀이터

『스프링 입문을 위한 자바 객체 지향의 원리와 이해』에서는 이 영역들을 T 영역이라고 명명했다.

응용 프로그램이 실행되면, JVM은 시스템으로부터 프로그램을 수행하는데 필요한 메모리를 할당받는다.

그리고 JVM은 이 메모리를 용도에 따라 여러 영역으로 나누어 관리한다.

JVM 내부 영역

JVM의 내부 영역은 아래 영역으로 나뉜다.

  • Class Loader System
  • Runtime Data Areas
    • Method Area
    • Heap
    • JVM Stacks
    • PC Registers
    • Native Method Stacks
  • Execution Engine
    • Interpreter
    • JIT Compiler
    • Garbage Collector
  • Native Method Interface
  • Native Method Library

이 글에서는 메모리 영역만 볼 것이기 때문에, 위의 두 가지 영역만 확인한다.

글로 봐서는 알기 힘드니 그림으로도 확인하자.

💡 출처: The JVM Architecture Explained - DZone
이 이미지가 가장 디테일해서 가져왔는데, 출처는 아무리 찾아봐도 좀 불분명했다. 모두 위 링크에서 이미지를 가져왔다고 하는데, 접속해보면 이미지가 뜨지 않는다.
이미지가 삭제된 것일까…

1. Class Loader System

Class Loader System은 클래스 파일 정보를 메모리에 올리고 검증하고 Static 변수들을 초기화하는 등의 역할을 한다.

  • 자바 소스코드를 컴파일 하여 Class 파일 생성
  • java 명령어로 클래스 파일을 실행
  • JVM은 Class Loader를 통해 클래스 파일을 읽어 Method Area 영역에 올린다.

이때, 클래스 파일 안에는 클래스에 대한 모든 정보가 들어있다.

  • 클래스 안에 어떤 필드가 몇 개 선언되어 있는지
  • 메소드는 몇 개이고 이름은 무엇인지
  • 바이트 코드

💡 바이트 코드란
JVM이 알아들을 수 있는 명령어 집합을 말한다. JVM의 목적은 바이트 코드를 기계어로 번역하여, CPU에게 일을 시키는 것이다.

이 번역을 인터프리터가 수행하게 된다.

2. Runtime Data Area

JVM이라는 가상 머신이 사용하는 메모리 공간으로, 다섯 가지 영역으로 구성되어 있다.

  • Method Area
  • Heap
  • JVM Stacks
  • PC Registers
  • Native Method Stacks

Method Area

클래스에 대한 모든 정보가 저장되는 영역이다.

프로그램 실행 중 어떤 클래스가 사용되었을 경우, 클래스 로더가 해당 클래스 파일(*.class)을 읽어 분석한 뒤 클래스에 대한 정보를 이곳에 저장한다.

이 때, 그 클래스의 클래스 변수(class variable, static으로 선언된 변수)도 이 영역에 함께 생성된다.

Heap

프로그램 실행 중 생성되는 인스턴스는 모두 이곳에 생성된다. 즉, 인스턴스 변수(instance variable)들이 생성되는 공간이다.

가비지 콜렉터가 주로 여기서 동작한다.

💡 Method Area와 Heap은 모든 스레드가 공유하는 영역이다. 따라서, Multi-Thread 환경에서는 동기화에 주의해야 한다.

JVM Stacks

호출 스택(call stack 또는 execution stack)이라고 한다. 메소드의 작업에 필요한 메모리 공간을 제공한다.

호출 스택의 특징

  • 메소드가 호출되면 수행에 필요한 만큼의 메모리를 스택에 할당받는다.
  • 메소드가 수행을 마치고 나면 사용했던 메모리를 반환하고 스택에서 제거된다.
  • 호출스택의 제일 위에 있는 메소드가 현재 실행 중인 메소드이다.
  • 아래에 있는 메소드가 바로 위의 메소드를 호출한 메소드이다.

호출 스택의 동작은 다음과 같다.

  • 메소드가 호출되면, 호출 스택에 ‘호출된 메소드를 위한 메모리'가 할당된다.
  • 이 메모리는 메소드가 작업을 수행하는 동안 지역변수(매개변수 포함)들과 연산의 중간결과 등을 저장하는데 사용된다.
  • 메소드가 작업을 마치면 할당되었던 메모리 공간은 반환되어 비워진다.

💡 Frame
호출된 메소드를 위해 할당된 메모리를 Frame이라는 자료구조라고 설명한다.
Frame은 메소드가 호출될 때마다 하나 생성되고 메소드가 하나 끝나거나 예외가 발생하면 사라진다.

각 메소드를 위한 메모리 상의 작업 공간은 서로 구별된다.

그림과 함께 예를 들어 보겠다.

  1. 첫 번째 메소드 호출 시, 호출 스택에 맨 밑에 메소드1을 위한 작업 공간이 마련된다.

  1. 메소드1 수행 중 메소드2 를 호출하면, 메소드1 바로 위에 두 번째로 호출된 메소드를 위한 공간이 마련된다.

    이때, 메소드1 은 수행을 멈추고, 메소드2 가 수행되기 시작한다.

  2. 메소드2 가 수행을 마치게 되면, 메소드2 의 메모리 공간이 반환되며, 메소드1 은 다시 수행을 계속하게 된다.

  3. 메소드1 도 수행을 마치면, 역시 제공되었던 메모리 공간이 호출 스택에서 제거되며 호출 스택은 완전히 비워지게 된다.

즉, 호출 스택의 제일 상위에 위치하는 메소드가 현재 실행 중인 메소드이며, 나머지는 대기 상태에 있게 된다.

따라서 호출 스택의 제일 상위에 위치하는 메소드 간에 호출관계와 현재 수행 중인 메소드가 어느 것인지 알 수 있다.

Frame의 내부 구조

  • Frame Data
    • Current Class Constant Pool Reference

      : 현재 클래스의 Constant Pool에 대한 참조, 이전 스택 프레임에 대한 정보, 현재 메소드가 속한 클래스/객체에 대한 참조 등의 정보를 갖는다.

  • Local Variables Array : 지역 변수 배열
  • Operand Stack

Local Variables Array

말 그대로, 메소드 내의 지역 변수들을 담고 있는 배열이다. 인덱스로 빠르게 접근할 수 있다.

인스턴스 메소드는 항상 첫 번째 인덱스이다.

  • 즉 0번째 인덱스에는 현재 인스턴스에 대한 참조를 가지고 있다.

예를 들어, 다음과 같은 메소드가 있을 때 지역 변수 배열에는 총 4가지가 들어간다.

private int methodA(int param) { // param으로는 호출 시 3을 넣어준다고 하자.
	int localVariable = 1;
	int sum = localVariable + param;
	methodB();
	return sum;
}

private void methodB() {}
  • 0번 인덱스: this (자기 자신에 대한 참조)
  • 1번 인덱스: param 3 (argument로 3이 넘어왔기 때문에, 3이 저장되어 있다)
  • 2번 인덱스: localVariable
  • 3번 인덱스: sum

Operand Statck

JVM은 스택 기반으로 연산을 수행한다. 피연산값 혹은 연산의 중간값들을 저장하기 위한 자료구조이다.

위에서 예로 들었던 methodA 메소드를 실행한다고 했을 경우엔, 아래와 같이 동작한다.

  1. 스택에 정수값 1을 올려라.
  2. 스택에서 정수값 1을 꺼내 2번 인덱스(localVariable)에 저장하라
  3. 지역 변수 배열의 2번 인덱스의 값(1)을 스택에 올려라
  4. 지역 변수 배열의 1번 인덱스의 값(3)을 스택에 올려라
  5. 스택 상단의 두 정수 값을 더한 후 스택에 넣어라 (4가 들어감)
  6. 스택에서 값을 꺼내 지역 변수 배열의 3번 인덱스에 넣어라

PC Registers

PC는 Program Counter를 말한다. 현재 실행되고 있는 명령어의 추적, 주소를 저장하고 있는 곳이다.

멀티 스레드 환경에서 한 스레드가 작업을 하다가 다른 스레드로 잠시 CPU 점유를 넘겨주고, 다시 돌아왔을 때 이전에 어떤 명령을 수행하고 있었는지를 기억하고 있어야 다음 작업을 이어서 실행할 수 있다.

이를 위해 준비한 영역이다.

Native Method Stacks

C나 C++로 작성된 명령어를 실행할 때 사용하는 스택이다.

💡 JVM Stacks, PC Registers, Native Method Stacks는 스레드가 생성될 때마다 같이 생성되고, 서로 다른 스레드가 침범할 수 없는 영역이다.
따라서 지역변수의 동시성 문제를 걱정하지 않아도 된다.

동작 예제와 T 메모리 구조

💡 javap 명령어를 사용하면 .class 파일을 디어셈블리하여, Constance Pool에 어떤 정보가 들어가 있는지 확인할 수 있다.

프로그램 실행

JRE는 프로그램 안에 main() 메서드가 있는지 확인한 후, JVM에 전원을 넣어 부팅한다. 부팅된 JVM은 목적 파일을 받아 그 목적 파일을 실행한다.

전처리 과정

JVM이 맨 먼저 하는 일은 전처리라고 하는 과정이다.

  1. 모든 자바 프로그램이 반드시 포함하게 되는 패키지를 스태틱 영역에 가져다 놓는다.

    : java.lang 패키지

  2. 개발자가 작성한 클래스와 import한 패키지를 스태틱 영역에 가져다 놓는다. (실행 중)

Current Class Constant Pool Reference

Constant Pool은 클래스 내에서 사용되는 상수들을 담은 테이블이다.

  • 인덱스와 타입, 그리고 거기에 매핑되는 값을 가지고 있다.
  • 값에는 인덱스가 들어갈 수도 있고, 값이 들어갈 수도 있다.
  • Symbolic Reference: 참조하는 대상의 이름만을 지칭하는 것
    • Utf8이라는 타입에는 해당 클래스의 이름(fully qualified name) 혹은 메소드의 이름이 문자열로 들어가 있다.
    • 나중에 이 값이 클래스의 데이터를 가리키는 포인터로 변한다.

Method Area에 올라간 클래스 정보

  • Constant Pool
  • ByteCode

main() 메소드 실행

JVM은 메소드 영역에 저장되는 바이트 코드를 해석한다. 즉, main() 를 실행한다.

main() 를 실행하면서, JVM은 Constant Pool에 대한 포인터를 하나 유지시킨다.

메소드 실행: 중괄호를 만날 경우

스택 프레임은 여는 중괄호를 만날 때마다 하나씩 생기고, 닫는 중괄호를 만나면 소멸된다.

  1. main() 메소드의 중괄호를 만나 스택 프레임이 스택 영역에 할당된다.
  2. 메서드의 인자 args를 저장할 변수 공간을 스택 프레임의 맨 밑에 확보한다.
    • 메서드 인자(들)의 변수 공간을 할당한다.
  3. 첫 명령 문 실행
    • 이때, System.out.println처럼 코드 실행 영역이면 데이터 저장 공간인 T 메모리에는 아무런 변화가 없다.
  4. main() 메소드의 닫는 중괄호를 만나 스택 프레임이 소멸된다.
  5. main() 메소드가 끝나면 JRE는 JVM을 종료하고, JRE 자체도 운영체제 상의 메모리에서 사라진다.

참고

int value = 10;

위와 같은 코드는 사실 하나의 명령문이 아닌 두 개의 명령문이다.

int value ;
value = 10;

클래스 로드: 객체 생성

위에서는 개발자가 작성한 클래스를 모두 스태틱 영역에 가져다 놓는 것처럼 말했지만, 사실 클래스는 필요할 때 동적으로 클래스 정보가 로드된다.

예를 들어, main() 에서 객체를 하나 생성하는 코드가 있다고 하자.

public class Main {
	public static void main(String[] args) {
		CustomObject obj = CustomObject();
	}
}
  1. 이 객체를 생성하는 바이트 코드를 읽으면, JVM에게 Constant Pool에 있는 클래스를 위한 메모리 공간을 할당하도록 명령한다.
  2. 클래스 로더는 CustomObject 클래스의 정보를 읽어 Method Area에 올린다.
  3. 심볼릭 레퍼런스(utf8: 클래스 이름)를 올려진 CustomObject 클래스를 직접 가리키는 참조로 변경한다.
    • 이것을 Constant Pool Resolution이라고 한다.
  4. JVM은 CustomObject 객체를 할당하는데 Heap 공간이 얼마나 필요한 지를 알아내기 위해 다시 한 번 Main 클래스의 Constant Pool을 바라본다.
  5. JVM은 항상 Method Area 영역에 저장된 클래스 정보를 보고 객체를 식별하는데 필요한 메모리 크기를 결정할 수 있다.
  6. 이에 필요한 모든 정보가 Method Area에 있기 때문이다.
  7. JVM이 객체를 위해 필요한 Heap 공간의 크기를 결정하고 나면, Heap 공간에 할당을 하고 instance 변수 값을 초기화한다.
  8. 인스턴스의 참조값을 Stack에 푸시하면서 동작은 끝난다.

변수 사용 시

변수는 스태틱 영역과 스택 영역, 힙 영역 모두 존재할 수 있다. 그런데, 각 영역에 있는 변수는 각기 다른 목적을 가지며 이름도 다르게 부른다.

  • 스택 영역의 변수: 지역 변수
    • 스택 프레임 안에 있으며, 스택 프레임이 사라지면 함께 사라진다.
  • 힙 영역의 변수: 객체 멤버 변수
    • 객체와 함께 가비지 컬렉터라고 하는 힙 메모리 회수기에 의해 사라진다.
  • 스태틱 영역의 변수: 클래스 멤버 변수

지역 변수

main() 메소드 안에서 변수를 하나 더 선언하면 args 위에 쌓는다. (스택)

변수를 선언만 하고 초기화하지 않은 상태에서는 메모리 공간 안에 알 수 없는 값이 들어가 있다. 이전에 해당 공간의 메모리를 사용했던 다른 프로그램이 청소하지 않고 간 값을 그대로 가지고 있는 것이다.
이는 우리에게 쓰레기 값일 뿐이므로, 사용을 시도하면 “초기화되지 않았을 수도 있습니다”라는 경고를 토해낸다.

객체 멤버 변수

클래스를 스태틱 영역에 배치하면, 클래스 정보 안에 변수의 이름이 존재한다. 하지만 변수 공간이 따로 존재하는 것이 아닌, 이름만 존재할 뿐이다.

객체가 생성되어야만 속성의 값을 저장하기 위한 메모리 공간이 힙 영역에 할당된다.

클래스 멤버 변수 : 전역변수 (static 변수)

static 변수는 T 메모리의 스태틱 영역에 변수 공간이 할당된다.

전역변수는 여러 메서드에서 전역 변수의 값을 변경하기 시작하면 T 메모리로 추적하지 않는 이상 전역 변수에 저장돼 있는 값을 파악하기 쉽지 않다. 따라서, 쓰지 말아야 한다.

다만, 읽기 전용으로 값을 공유하는 경우(전역 상수)는 적극 추천한다.

블록 스택 프레임에서의 변수 접근

if문이 있다고 했을 때, 조건이 참인 if의 여는 중괄호를 만나면 블록의 스택 프레임이 생긴다.

예를 들어 main() 스택 프레임 이 있다면, 그 안에 if(true) 스택 프레임 이 중첩되어 생기는 것이다.

if 블록 스택 프레임을 수행하는 중에, if 블록 스택 프레임 외부에 존재하는 변수에는 접근이 가능하다.

  • 이는 메모리 상에 변수가 존재하니 당연한 것이다.
  • 외부 블록에서 내부 블록의 변수에는 접근할 수 없지만, 내부 블록에서 외부 블록의 변수에 접근하는 것은 가능하다.

한 스택 프레임에서 다른 스택 프레임의 변수에 접근할 수 없다.

아직 메모리 상에 없어 접근할 수 없는 경우도 있지만, 메모리 상에 존재함에도 접근할 수 없다. 이는 자바 스펙에서 금지되어 있으며, 책의 저자는 아래와 같은 이유로 짐작했다.

  1. 메서드는 서로의 고유 공간이다. 즉, 서로 침범하면 무단 침입으로 자바 월드에 문제를 유발할 수 있다.
  2. 타 메서드의 지역변수에 접근하려면 해당 변수의 위치를 명확히 알아야 하는데, 그러기 위해선 변수의 메모리 주소, 즉 포인터를 알아야 한다. 자바는 포인터가 없다.
  3. 호출하는 메서드 내부의 지역 변수를 호출당하는 쪽에서 제어할 수 있게 코드를 만들려면 결국 포인터를 주고 받아야 한다.

자바에서는 포인터를 사용할 수 없으므로 결국 언어 스펙상으로도 메서드 스택 프레임 사이에 변수를 참조하는 것은 불가능하다는 결론에 도달할 수 있다.

기타: 명칭에 대하여

필드 vs 속성, 함수 vs 메서드

절차적/구조적 프로그래밍에서 공유 변수를 필드라고 불렀고, 기능적 요소를 함수라고 불렀다. 객체 지향 프로그래밍에서 같은 일을 하지만 차별화를 하기 위해 전역 변수를 프로퍼티라 부르고, 함수를 메서드라 부르기 시작했다고 한다.

  • Feild = 필드 = 속성 = 프로퍼티 = Property
  • Function = 함수 = 메서드 = Method = 기능 = 행위

객체 지향에서 필드는 객체 변수 또는 정적 변수를 말하고, 속성은 필드를 외부에 노출시키는 메서드라고 하는 사람도 있다.

이 구분을 따른다면 자바에서의 속성은 get/set 메서드라고 할 수 있다.

Reference

참고 서적

📔 자바의 정석

📔 스프링 입문을 위한 자바 객체 지향의 원리와 이해

참고 사이트

우테코: 어썸오의 JVM Memory Layout

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] JIT 컴파일러란?

[Java] JIT 컴파일러가 대체 뭔데?

[Java] JVM(Java Virtual Machine) 이해하기 -2 : 메모리 영역(Runtime Data Area)

JVM Memory 메서드 영역과 metadata space 의 차이점 - 인프런

profile
책을 읽거나 강의를 들으며 공부한 내용을 정리합니다. 가끔 개발하는데 있었던 이슈도 올립니다.

0개의 댓글