[JVM] 작성한 코드의 정보를 담는 메모리 영역

NoFlue·2023년 1월 20일
0

JVM

목록 보기
2/2
post-thumbnail

⛔️ 본 포스팅은 JVM을 공부한 내용을 바탕으로 작성한 글입니다. 틀린 내용 및 피드백이 필요한 부분을 발견하신다면 지적해주시면 감사드리겠습니다 :)

코딩을 입문할 때 "Hello, World!" 만 출력해도 신기하던 시절이 있었다. 내가 입력한 문자열이 그대로 출력돼서 라는 이유도 있지만 어떤 과정을 거쳐 내가 작성한 코드가 실행되는지 궁금했다(물론 지금도 안드로이드 개발을 하다보면 신기할 때가 많다). 그래서 JVM 공부를 하며 궁금증을 해결하고자 한다.

이전에 자바 코드를 바이트 코드로 변환한 후 로드하는 클래스 로더에 대해 작성했다면 이번엔 로딩한 내용을 적재할 메모리 공간인 Runtime Data Area 에 대해 알아보려고 한다.

메모리 영역에 대해 알아보기 전에 클래스 로더를 간단하게 요약해보자.

클래스 로더의 간단한 요약

  • 클래스 로더는 클래스 파일(.class) 을 런타임 때 클래스 사용할 경우 메모리에 동적으로 로딩하는 역할을 한다.
  • 메모리 영역의 Method Area에 클래스의 정보를 전달하는 Loading, 클래스를 사용하기 위한 검증을 하는 Verifying, 확보한 메모리 공간에 클래스의 정적값을 저장하는 Initializing 세 단계를 거쳐서 로딩한다.
  • 클래스 로더의 종류는 기본으로 제공받는 클래스 파일인지 개발자가 정의한 클래스 파일인지에 따라 3가지로 분류되며, 위임 계층 구조로 이루어져 있다.

Runtime Data Area

Runtime Data Area 는 JVM에서 애플리케이션을 실행하기 위해 OS로 부터 할당받은 메모리 공간이다. 쉽게 생각하면 JVM이 사용하는 가상 메모리 공간이다.
바이트 코드, 객체, 매개 변수, 로컬 변수, 반환 값 및 계산의 중간 결과 를 저장하기 위한 메모리를 제공하며, 이 메모리들은 역할에 따라 5가지로 나뉜다.
Runtime Data Area

Heap

Heap은 JVM의 주 메모리이며, 모든 클래스의 인스턴스배열, 문자열의 정보를 가진 String pool 을 저장한다. 힙은 하나만 생성되며, JVM 실행 시 생성되며, 종료 전까지 유지된다.
힙이 가진 데이터들은 JVM의 Stack 영역에 참조되기 때문에 모든 쓰레드에서 공유한다.

힙에 저장된 인스턴스들이 더이상 쓰이지 않거나 값을 null로 명시할 경우 GC(Garbage Collection)에 의해 제거가 된다. 제거되는 기준과 GC의 알고리즘은 GC 내용에 대해 작성할 때 더 자세히 알아볼 예정이다. (내용 작성시 링크 올라올 곳)

힙은 모든 쓰레드에서 공유되기 때문에 동시성 문제가 존재한다. 쓰레드 메모리가 따로 관리되는 것과 달리 쓰레드에 의해서 공유되기 때문에 Thread Safe 하지 않기 때문이다. 이러한 문제는 synchronaized, volatile 등과 같은 키워드를 사용하여 막아야한다.
이와 관련된 문제는 우리가 아는 싱글톤 패턴에서 볼 수 있다.

// Singleton 패턴

class Singleton {
	private volatile static Singleton instance;
    
    public static Singleton getInstance() {
    	if(instance == null) {
        	synchronized(Singleton.class) {
            	if(instance == null() {
                	instance = new Singleton()
                }
            }
        }
        return instance
    }
}

Method Area

Method AreaStatic Area 라고 불린다. 각 클래스 별로 인스턴스 생성을 위한 객체 구조, 생성자, 필드클래스의 메타 데이터 를 저장 및 관리한다.
클래스의 메타 데이터 는 다음과 같은 정보를 가지고 있다.

Type Information : 클래스와 인터페이스 정보

  • Type 명 : Pakage name + Class name
  • Type 종류 : 타입이 Class 인지 Interface 판별
  • Type 제어자 : 접근 제어자(public, protected, private 등)그 외 제어자(abstract, final 등)
  • Type의 직계 하위 클래스 리스트
  • 연관된 인터페이스 리스트

Runtime Constant Pool : 타입의 상수 정보를 저장하는 Pool

  • 객체 접근을 위한 Type, Field, Method 의 레퍼런스 정보 저장

Field Information : 필드의 정보

  • Type 명 : 필드의 타입
  • Type 제어자 : 접근 제어자(public, protected, private 등)그 외 제어자(static, fianl, volatile 등)

Method Information : 메소드 정보

  • Method 이름, 반환 타입, 매개 변수 정보들
  • 생성자를 포함한 모든 메소드 정보

Class Variable : static 으로 선언된 변수들 저장

  • static 변수는 레퍼런스 변수만 저장되고 인스턴스는 Heap에 저장
  • static 변수들은 미리 메모리를 할당 받음

메소드 영역은 하나만 생성되며, 인스턴스 생성에 필요한 정보도 갖고 있기 때문에 모든 쓰레드와 공유한다. JVM 실행 시 생성되고, 종료 전까지 유지된다.

오라클 문서에 따르면 메소드 영역은 논리적으로 힙에 포함되어 있다고 한다.

Although the method area is logically part of the heap, simple implementations may choose not to either garbage collect or compact it.
-Chapter 2.5.4 Method Area 중

이에 대한 내용은 Java 8 이전 PermanentJava 8 이후 Metaspace 의 차이점을 설명하는 포스트에서 더 자세하게 알아볼 예정이다. (내용 작성시 링크 올라올 곳)

PC Register

JVM은 멀티 쓰레드를 지원하며, 각 쓰레드 마다 JVM이 실행하는 명령어 주소값을 저장하는 Program Counter Register 가 있다.
만약 현재 쓰레드에서 실행중인 메소드가 네이티브인 경우, pc 레지스터의 값은 정의되지 않는다(Undefined). 반대로 네이티브가 아닌 경우, pc 레지스터의 값은 JVM이 실행한 명령어 주소값을 저장한다.

Stack

Stack
Stack은 각 쓰레드 별로 하나씩 생성된다.
Stack Frame은 메소드가 호출될 때 마다 스택에 push 되며, 메소드가 종료될 경우 pop 되어 스택에서 제거된다.
스택 프레임은 Local variables array, Operand stack, Frame data 를 담고 있고, Frame dataConstant pool, 이전 스택 프레임에 대한 정보, 현재 메서드가 속한 클래스에 대한 참조 등 정보를 담고 있다.

스택의 Local variables arrayOperand Stack 을 알아보기 위해 자바 코드를 바이트 코드로 변환한 후 어떻게 실행되는지 확인하며 알아보자.

public class Main {
	public static void main(String[] args) {
    	int a = 1;
        int b = 2;
        int c = a + b;
    }
}

자바로 위와 같은 간단한 코드를 작성해보고, 바이트 코드로 변환해보았다.

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 3 L0
    ICONST_1
    ISTORE 1
   L1
    LINENUMBER 4 L1
    ICONST_2
    ISTORE 2
   L2
    LINENUMBER 5 L2
    ILOAD 1
    ILOAD 2
    IADD
    ISTORE 3
   L3
    LINENUMBER 6 L3
    RETURN
   L4
    LOCALVARIABLE args [Ljava/lang/String; L0 L4 0
    LOCALVARIABLE a I L1 L4 1
    LOCALVARIABLE b I L2 L4 2
    LOCALVARIABLE c I L3 L4 3
    MAXSTACK = 2
    MAXLOCALS = 4

Local variables array 는 메소드 내 지역 변수들 갖고 있는 배열이다. 위 코드의 지역 변수는 args a b c 가 존재하며, 이 지역 변수에는 index 가 지정되어 있다.

Local Variables Array

Operand Stack 은 지역 변수들을 사용해서 연산을 처리하는 스택을 담당한다. 바이트코드에 작성되어 있는 명령어들을 가르키는 것이 위에서 본 Program Counter이고, 이 명령어들은 다음과 같은 기능을 갖는다.

  • ICONST : 해당 값을 스택에 Push 한다.
  • ISTORE : 해당 인덱스 로컬 변수에 값을 저장하고 Pop 한다.
  • ILOAD : 해당 인덱스 로컬 변수의 값을 스택에 Push 한다.
  • IADD : 스택에 존재하는 두 값을 더해준다.

위 바이트 코드가 실행되는 순서를 그림으로 표현하면 아래와 같다.

순서는 왼쪽에서 오른쪽으로 흘러간다.
1. ICONST_1 을 실행해 스택에 1을 추가하고, ISTORE 1 을 실행해 인덱스 1에 존재하는 로컬 변수 a 에 1을 저장하고 스택에서 pop을 한다.
2. ICONST_2 을 실행해 스택에 2를 추가하고, ISTORE 2 을 실행해 인덱스 2에 존재하는 로컬 변수 b 에 2를 저장하고 스택에서 pop을 한다.
3. ILOAD_1 과 ILOAD_2 을 실행해 스택에 1, 2를 추가하고, IADD 를 실행해 두 값을 더해주고, ISTORE_3 를 실행해 인덱스 3에 존재하는 로컬 변수 c 에 3을 저장하고 스택에서 pop을 한다.

위 바이트 코드의 L4 를 보면 MAXSTACK = 2MAXLOCALS = 4 가 보이는데 위 글을 보고 추정할 수 있듯이 해당 메소드에서의 최대 스택과 최대 로컬 변수 수를 나타낸다.

Native Method Stack

성능 향상을 위해 자바 코드 대신 JNI를 통해 네이티브 코드로 작성된 메소드를 호출하는 경우가 있다. Java Stack과 비슷하게 네이티브 메소드를 호출하면 C Stack 에 push 된다.
Native Method Stack 또한 각 쓰레드 별로 하나씩 생성된다.

참고 문헌

[Oracle] Chapter.2 The Structure of the Java Virtual Machine
[10분 테코톡] 어썸오의 JVM Memory Layout
[10분 테코톡] 🎅무민의 JVM Stack & Heap
[Tecoble] JVM에 관하여 - Part 3, Run-Time Data Area
Java 런타임 데이터 영역

profile
앱 개발에 호기심이 많은 대학생 개발자 :3

0개의 댓글