[Java] 런타임 데이터 영역(Runtime Data Area)에 대해

땡글이·2023년 3월 31일
5

Java

목록 보기
3/6

자바 가상 머신(JVM)의 런타임 데이터 영역은 자바 애플리케이션을 실행할 때 사용되는 데이터들이 저장되는 메모리 공간입니다. 런타임 데이터 영역은 크게 다섯 가지 영역으로 나뉘어집니다.

  • 메서드 영역(Method Area)
  • 힙 영역(Heap)
  • 스택 영역(Stack)
  • PC 레지스터(Program Counter Register)
  • 네이티브 메서드 스택(Native Method Stack)

그리고 Java에서 Thread가 공유하는 영역과 공유하지 않는 영역은 다음과 같습니다.

  • Thread가 공유하는 영역 (Java)
    • 힙 영역
    • 메서드 영역
  • Thread가 공유하지 않는 영역 (Java)
    • Stack 영역
    • PC 레지스터 영역
    • 네이티브 메서드 스택

이제 각각의 영역들에는 어떤 데이터들이 저장되는지 확인해보겠습니다.

PC 레지스터(Program Counter Register)

PC 레지스터 영역 은 각 스레드마다 현재 수행 중인 JVM 명령의 주소가 저장되는 영역입니다. Thread 는 각자의 메소드를 실행하게 됩니다. 이때, Thread 별로 동시에 실행하는 환경이 보장되어야 하므로 최근에 실행 중인 JVM 에서는 명령어 주소값을 저장할 공간이 필요합니다.

이 부분을 PC 레지스터 영역이 관리하여 추적이 가능하게 만들어줍니다. PC 레지스터 영역이 있음로써, Thread 들은 각각 자신만의 PC 레지스터들을 가지고 동작할 수 있습니다.

만약 실행했던 메소드가 네이티브하다면, 해당 명령어의 위치를 알 수 없기 때문에 PC 레지스터에 undefined 값을 기록하게 됩니다. 실행했던 메소드가 네이티브하지 않다면, PC 레지스터는 JVM 에서 사용된 명령의 주소 값을 저장하게 됩니다.

네이티브하다는 것은 Java가 아닌 다른 언어(C/C++ 등)으로 실행된 메서드를 의미합니다.

네이티브 메서드 스택(Native Method Stack)

위의 PC 레지스터 영역에서 네이티브 메서드들은 PC 레지스터에 저장이 되지 않는다고 했습니다. 즉, 네이티브 메서드들을 위한 영역이 별도로 필요하다는 것을 알 수 있습니다. 바로 이 네이티브 메서드 스택(Native Method Stack) 영역이 네이티브 메서드들을 실행하기 위한 스택 영역입니다.

네이티브 메서드 스택(Native Method Stack) 영역 또한 스택 영역과 마찬가지로 각 Thread 마다 개별적으로 생성되며, 자바 외부의 네이티브 코드를 호출할 때마다 생성되었다가 호출이 완료되면 사라집니다.

스택 영역(Stack)

이제 Runtime Data Area에서 중요한 3가지 영역을 살펴보겠습니다. 첫 번째로 스택 영역입니다. 스택 영역에는 메서드 호출 시 지역 변수, 매개변수, 함수 호출내역 등이 저장되는 영역입니다. 각 스레드마다 개별적으로 생성되며, 메서드 호출 시 생성되었다가 메서드가 종료되면 사라집니다.

스택에 저장되는 데이터들은 Frame 이라는 자료구조로 저장이 됩니다. 그리고 Frame 에는 아래와 같은 데이터들이 저장됩니다.

  • Local Variables
  • Operand stack
  • Frame data

Local Variables

Local Variables 에는 이름에서 알 수 있듯이, 함수에서 쓰이는 매개변수 혹은 지역변수들이 저장됩니다. 함수 내에 몇 개의 지역변수가 있을지에 대해서는 컴파일 시점에 정해지고, 실행 시점에 메모리가 할당됩니다.

Operand Stack

Operand Stack은 피연산자들을 stack 자료구조로 저장해두는 것을 의미합니다. 예시로, 곱하기 연산이 로직에 있다면 Operand Stack에서 두 개의 피연산자를 꺼내어 계산하고 다시 Operand stack에 결과를 저장합니다.

컴퓨터구조 수업에서 배운 어셈블리어를 보면 피연산자는 레지스터에 저장되어 있어서 "add $1 $2" 이런 식으로 구성되지만, JVM은 레지스터를 이용하지 않고 스택에 피연산자들을 저장합니다. 그래서 "add" ,"mul" 과 같은 명령어가 실행되면 Operand Stack에서 피연산자들이 pop 되어 연산이 수행됩니다.

Frame data

스택 Frame 에는 지역변수, Operand stack 이외에도 반환 타입, 상수 풀(Constant Pool) 등과 같은 데이터들이 포함됩니다. 이런 데이터들이 Frame Data에 저장됩니다.

만약 어떤 로직에서 상수 풀(Constant Pool)을 데이터를 사용하는 로직이 있다면, 해당 로직을 수행하기 위해 Frame data의 Constant Pool을 참조하는 포인터를 이용하게 된다.


메서드 영역(Method Area) 혹은 PermGen

두 번째로 메서드 영역(혹은 PermGen, Permanent Generation)입니다. 메서드 영역은 쉽게 이해하자면, 일반적인 메모리 구조에서의 코드 영역과 유사하다고 볼 수 있습니다.

JVM의 메서드 영역에 대해 자세히 알아보겠습니다. 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성되고, 클래스 로더에 의해 로드된 클래스 정보를 저장합니다.

즉, 메서드 영역엔 JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, 생성자, Static 변수, 메서드의 바이트코드 등을 보관합니다.

  • 상수 풀(Constant Pool)
  • 클래스 및 인터페이스의 필드
  • 메서드
  • 생성자
  • static 변수
  • static 메서드

메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있습니다. 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불리기에 Method area 와 혼용되어 쓰입니다.

JVM 벤더는, JVM specification 을 지키며 JVM을 구현하는 개발자를 칭하는 용어입니다. 누구나 JVM Spectification을 지켜서 구현한다면 JVM 벤더가 될 수 있습니다.

즉, 메서드 영역은 클래스의 바이트 코드, 상수, 필드, 메서드 등 클래스 정보와 관련된 모든 내용이 저장되는 영역입니다. 또한, 메서드 영역(Method Area)은 모든 Thread에서 공유되는 특징을 가지고, JVM이 시작될 때 생성되고 JVM이 종료되면 사라진다는 특징도 가집니다. 그리고 메서드 영역에서 더이상 데이터를 저장할 공간이 없으면 OutOfMemoryError를 발생시킵니다.

  • 모든 Thread에서 공유
  • JVM이 시작될 때 생성되고 JVM이 종료되면 사라짐
  • 공간 부족하면, OutOfMemoryError 발생

Method Area와 메모리 관리 (feat. GC)

Method Area 에는 어플리케이션에서 사용되는 모든 클래스의 메타데이터가 저장되는 것이 아닙니다. 자바는 동적 로딩을 활용하기 때문에 컴파일 시점에도 로딩되는 클래스가 있고, 런타임 시점 혹은 로드 타임에 로딩되는 클래스가 있습니다. 이것은 클래스의 메타데이터를 저장하는 것도 효율적으로 저장하기 위함입니다.
그렇기 때문에 Method Area에서는 Method Area에 저장되어 있는 클래스의 메타데이터가 Heap 영역에 있는 객체들과 연결되어 있지 않은 데이터라면, GC가 Heap영역에서 객체의 메모리를 회수하고, 해당 클래스의 인스턴스가 Heap 영역에 없을 때, Method Area에서도 클래스의 메타데이터가 삭제됩니다.
다만 공식문서에 따르면, JVM Vendor에 따라서 Method Area에서 GC가 동작할 수도 있고 안할수도 있다는 것이 JVM Specification의 내용입니다. 즉, Method 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.
"... simple implementations may choose not to either garbage collect or compact it ... "

Runtime Constant Pool

런타임 상수 풀(Runtime Constant Pool) 영역은 사실 메서드 영역에 포함됩니다. 하지만, 상수 풀(Constant Pool)은 JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM specification 에서도 자세히 기술하고 있기에 따로 짚고 넘어가고자 합니다.

런타임 상수 풀은 클래스 로더가 메서드 영역에 클래스를 로딩할 때, 같이 메서드 영역에 적재되는 부분입니다. 즉, 클래스 별로 런타임 상수 풀을 가지고, 런타임 상수 풀에는 클래스 및 인터페이스의 상수 뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스에 대한 정보를 가지고 있습니다.

그리고 런타임 상수 풀(Constant Pool) 영역은 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역입니다.

동작 과정은 다음과 같습니다.

  • A 클래스에서 B 클래스를 참조하고 있다면, 클래스 로더는 B 클래스가 메서드 영역에 로딩되어 있는지 확인
  • 만약 메서드 영역에 있다면 해당 클래스의 레퍼런스를 B 클래스를 참조하는 변수에 할당
  • 하지만, 메서드 영역에 없다면 클래스로더를 통해 B 클래스를 메서드 영역에 로딩
  • 그리고 A 클래스의 상수 풀에서는 B 클래스를 참조하는 변수에 B 클래스의 실제 레퍼런스를 할당
    • 위의 과정을 보면, 런타임 상수 풀에 레퍼런스를 담을 때, 처음에는 실제 레퍼런스를 가지고 있지 않습니다.
    • 즉, 클래스 이름으로 된 심볼릭 레퍼런스(symbolic reference)으로 참조하다가 클래스 로더에 의해 클래스가 로딩되면 실제 클래스의 레퍼런스(true class's reference)를 가리키게 됩니다.
    • 이 과정을 Constant Pool Resolution이라고 합니다.

또한 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블입니다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀(Runtime Constant Pool)을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조합니다.

앞서 얘기했듯이 상수 풀(Constant Pool)는 Method Area에 속하기 때문에, 상수 풀을 할당해줄 메모리가 없으면, OutOfMemory 에러가 발생합니다.

Runtime Constant Pool 과 String Pool의 차이

많은 블로그에서 Runtime Constant PoolString Pool의 의미를 혼용해서 사용하지만, 이 둘은 명백히 다른 것입니다. 차이에 대해 알아보겠습니다.

우선, 상수와 리터럴의 차이에 대해 알아야합니다. 상수는 '초기화 이후 값이 변하지 않는 수'를 의미하고, 리터럴도 상수의 일종인데, 말 그대로 선언없이 바로 사용할 수 있는, 문자 그대로의(=리터널한) 상수를 의미합니다.

그리고 Java는 기본자료형을 제외한 객체를 만들 때, new 키워드로 만들어 참조형으로 만듭니다. 하지만, String은 예외적으로 new 키워드 없이도 객체를 만들 수가 있습니다. 이를 문자열 리터널 생성 방식이라고 합니다.

String str1 = "madplay"; // 스트링 상수값으로 관리된다. String Constant Pool 이라는 영역에 저장
String str2 = "madplay"; // 기존 "madplay" 상수 재활용. str 1이랑 같은 메모리 참조

String str3 = new String("madplay"); // // 다른 객체와 마찬가지로 Heap 영역에 할당
String str4 = new String("madplay"); // 새로운 객체를 생성. str3이랑 다른 메모리 참조

실제로, Java 8 버전부터는 Runtime Constant PoolMetaspace 영역에 저장되고, String poolHeap 영역에 저장되는 방식을 취하니, 헷갈리지 말아야 한다!

java7 버전까지는 String poolPermGen 에 저장되었지만, 많은 String이 생성되는 것이 OOM을 발생하는 문제가 있었습니다. 그렇지만 java8 부터는 String Pool 또한 Heap 영역에 저장해둠으로써, GC가 동작할 수 있도록 구현해서 메모리를 최적화시켰습니다.

힙 영역(Heap)

힙 영역(Heap)은 객체와 배열이 할당되는 영역입니다. 자바에서는 new 키워드로 객체와 배열을 생성하며, 생성된 객체와 배열의 크기에 따라 크기가 동적으로 변하는 특징을 가지고, 모든 Thread에서 공유되는 특징을 가집니다.

그리고 힙 영역은 JVM이 실행 중인 시스템 메모리 중 가장 큰 부분을 차지하며, 가비지 컬렉션(Garbage Collection)이 동작하는 영역입니다. GC가 어떻게 동작하는지에 대해선 따로 포스팅할 예정이고, 이 글에선 힙 영역이 어떻게 세분화되어 있는지 살펴보겠습니다.

힙 영역의 세분화

힙 영역은 크게 Young 영역Old 영역으로 나뉩니다. Young 영역은 새로 생성된 객체 데이터와 배열 데이터가 저장되는 영역으로, Eden 영역2개의 Survivor 영역으로 나뉘어집니다. 해당 부분에 대해선 GC를 다루는 글에서 조금 더 자세히 다루겠습니다.

힙 영역 - java 7 이전 vs java 8 이후

Java 7 이전 버전의 JVM은 위의 그림처럼 PermGen 이 Heap 영역에 포함되었습니다. 하지만 Java8부터 JVM의 메모리 영역 중 Permanent Generation 메모리 영역이 사라지고 Metaspace 영역이 생겼습니다. 또한 Metaspace 영역은 Heap 영역에 포함되지 않으며, Native Memory에 속하게 되었습니다.

Native Memory란?

OS에 의해 관리되는 영역을 의미합니다. 즉, JVM이 관리하는 Runtime Data Area 와는 별도로 존재합니다.

Metaspace 영역에서는 Permanent Generation 영역과 마찬가지로 Classloader가 로드한 class들의 metadata(상수 풀, 생성자, 필드, 메서드 등)을 저장합니다. 그런데 여기서 주의할 점이 하나 있습니다. Metaspace에서는 Permanent Generation 과는 달리, static 변수들과 string pool을 관리하지 않습니다. 이것들은 Heap 영역에서 관리되도록 바뀌었습니다.

static 객체들을 Heap 영역으로 옮김으로써, static 변수들도 GC의 대상이 될 수 있게 되었습니다.

위의 내용은 오라클 문서에서, "The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap." 라고 언급되어 있다.

그리고 앞에서 Permanent Generation에 대해 얘기할 때, 메모리가 부족할 시에는 OutOfMemory 문제가 발생할 수 있다고 했습니다. 실제로 java 진영에서 알아본 결과, Heap 영역의 크기는 한정되어 있고 Permanent Generation 영역은 클래스 로딩과 언로딩, 리플렉션, 프록시 등의 기능을 사용할 때 메모리 누수와 OutOfMemory의 원인이 되는 경우가 많았습니다.

  • ex) static 키워드가 붙은 collection 변수에 계속해서 변수를 추가할 경우에도, PermGen 에 object의 refernece가 쌓이게 돼, OOM 발생
  • ex) string literal data를 저장하던 string pool도 permanent 영역에 쌓이게 돼, OOM 발생

하지만 Metaspace 를 Heap 영역에서 분리함으로써, OutOfMemory문제를 조금 덜 만날 수 있게 되었습니다. 기본적으로 JVM의 Heap 영역은 OS에 의해 관리되는 메모리(Native Memory)의 크기에 비해 작기 때문입니다. 그래서 “java.lang.OutOfMemoryError: PermGen space”과 같은 종류의 OOM은 더 이상 마주칠 일이 없어졌고, -Xmx option에 의해 설정되는 Heap사이즈가 아닌, Host 운영시스템에 의해서 그 사이즈가 제약됩니다.

그래도 Native Memory에서도 클래스의 메타데이터를 저장할 수 있는 공간이 없다면, “java.lang.OutOfMemoryError: Metaspace” 라는 OOM을 마주치게 됩니다.

그런데 위의 말대로라면, Metaspace 영역에서 메모리가 부족해진다면, 어플리케이션의 OOM 문제가 발생하는 것이 아니라, 전체 서버를 다운시킬 수도 있다는 이야기가 될 수도 있습니다. 그렇기에, 적절한 flag값(-XX:MaxMetaspaceSize or -XX:MetaspaceSize 등)을 설정해주어야 합니다.

Metaspace 는 언제 할당되고 언제 릴리즈되는가?


할당은, Class가 로드되고 런타임이 JVM에 준비될 때, class loader에 의해 class의 metadata 정보들이 저장되기 위해 metaspace 영역에 할당됩니다.

하지만 릴리즈는 어떤 live instance도 없고 어떤 참조도 없는 상태에서 GC가 일어난 이후에야 릴리즈 되는 것입니다.


정리

Thread 가 공유하지 않는 영역

  • 네이티브 메서드 스택(Native Method Stack)
    • java가 아닌 다른 소스코드(c/c++)이 실행될 때 사용되는 스택
  • PC 레지스터(Program Counter Register)
    • 쓰레드가 수행 중인 함수의 주소가 저장되는 공간
  • 스택 영역(Stack)
    • 함수와 관련된 정보(지역변수, 매개변수, 반환타입)이 저장되는 공간

Thread 가 공유하는 영역

  • 메서드 영역(Method Area, PermGen)
    • 클래스 로더에 의해 로딩된 클래스의 정보(상수 풀, 필드 및 메서드 데이터, 메서드 및 생성자 코드)들이 저장되는 공간
    • java 7까지는 Heap영역에 속했지만, java 8 이후로는 metaspace라는 이름으로 바뀌고 native memory에 속하게 됨 (다만, static 객체, string pool은 Heap 영역에서 관리)
  • 힙 영역(Heap)
    • 객체와 배열이 저장되는 공간
    • GC가 동작하며 객체들이 차지하고 있는 메모리를 회수해감

Reference

https://docs.oracle.com/javase/specs/jvms/se8/jvms8.pdf
https://www.cs.miami.edu/home/burt/reference/java/language_vm_specification.pdf
https://docs.oracle.com/javase/specs/jvms/se17/jvms17.pdf
https://openjdk.org/jeps/122
https://blog.knoldus.com/full-explanation-of-jvm-runtime-data-area-and-how-jvm-using-it/
https://huisam.tistory.com/entry/jvmgc
https://jaemunbro.medium.com/java-metaspace에-대해-알아보자-ac363816d35e
https://www.baeldung.com/java-string-pool
https://www.baeldung.com/java-jvm-run-time-data-areas
https://8iggy.tistory.com/229
https://www.artima.com/insidejvm/ed2/jvm8.html
http://honeymon.io/tech/2019/05/30/java-memory-leak-analysis.html
https://www.youtube.com/watch?v=GU254H0N93Y
https://jiwondev.tistory.com/114

profile
꾸벅 🙇‍♂️ 매일매일 한발씩 나아가자잇!

0개의 댓글