[Java] 메모리 구조

Swim Lee·2021년 1월 11일
7

Java

목록 보기
1/3
post-thumbnail

Java는 .java 파일을 javac 컴파일러를 통해 .class 바이트코드로 컴파일한 후, 클래스 로더에 의해 .class 파일을 JVM 위의 Runtime Data Area에 올려서 실행시키기에 OS 독립적으로 개발할 수 있다는 것은 다들 잘 알고 계실 것입니다.

따라서 JVM이 메모리를 할당하는 방식은 굉장히 중요한데, 여러 글들을 보다보면 조금씩 서로 달라서 한번 정리해보고자 합니다!

Java 메모리구조

자바 메모리 구조는 크게 3가지로 나눌 수 있습니다.
글마다 5개 까지도 나누는 것도 있지만, 대표적인 영역 3가지만 살펴보도록 하겠습니다.

  1. Method 영역
  2. Stack 영역
  3. Heap 영역

이는 운영체제에서 프로세스의 메모리를 할당하는 방식과 비슷합니다.
운영체제의 경우도 크게 데이터, 코드, 힙, 스택 영역 으로 나누어서 프로세스의 할당하는데 JVM의 메모리 할당방식도 이와 유사합니다.

그렇다면 이제부터 각각의 영역에 대해서 자세히 알아보도록 하겠습니다.

Method 영역

제가 이 글을 작성하게 된 가장 큰 이유중 하나입니다.
제가 보는 참고서를 보면 메소드 영역의 경우는 메소드의 바이트 코드 그리고 static 변수가 적재되는 공간이라고 적혀있습니다. (윤성우의 열혈 Java 프로그래밍)
static 변수는 인스턴스와 상관없는 클래스 전체에서 공유되는 변수니까 그렇다면 메소드 영역은 전역변수가 저장되는 프로세스의 데이터 영역과 비슷한 역할이구나!
그렇다면 메소드의 바이트코드는 뭐지? 바이트 코드는 .java파일를 컴파일한 결과로 나오 .class 파일인데 왜 "메소드"의 바이트 코드라고 하는 것일까? 하는 생각이 들었습니다.
따라서 먼저 바이트코드가 구체적으로 무엇인지부터 알아보도록 하겠습니다.

자바 바이트 코드 (.class)

자바 바이트코드를 구글링해보면 다음과 같이 정의가 됩니다.

자바 가상 머신(JVM)이 실행하는 명령어의 집합

그렇다면 이 명령어들은 어떤 식으로 구성되어있을까요?
아래는 자바 바이트코드의 형식입니다.

<index><opcode> [<operand1> [<operand2>...]] [<comment>]

그렇다면 실제 클래스가 어떻게 바이트코드로 컴파일되는지 살펴보도록 하겠습니다.

public class Person {
    int age = 10;
    
    public int getAge() {
        return age;
    }
    
    public void setAge(int age) {
        this.age = age;
    }
}

위와 같은 클래스를 컴파일해보도록 하겠습니다.

javap -c Person

위와 같이 기본 자바 클래스파일 Diassembler 프로그램인 javap를 실행시키면 다음과 같은 코드가 나타납니다.

C:\Users\sylee\IdeaProjects\javaCodingTest\out\production\javaCodingTest\com\syleemk\javabasic>javap -c Person
Warning: File .\Person.class does not contain class Person
Compiled from "Person.java"
public class com.syleemk.javabasic.Person {
  int age;

  public com.syleemk.javabasic.Person();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: bipush        10
       7: putfield      #2                  // Field age:I
      10: return

  public int getAge();
    Code:
       0: aload_0
       1: getfield      #2                  // Field age:I
       4: ireturn

  public void setAge(int);
    Code:
       0: aload_0
       1: iload_1
       2: putfield      #2                  // Field age:I
       5: return
}

대충 살펴보면 각 메서드별로 바이트코드가 생성된 것을 확인할 수 있습니다!
아무래도 JVM이 실행시키는 것은 결국 메서드의 흐름이기 때문에 메서드단위로 바이트코드가 생성되는게 아닐까 생각해봅니다.

각각의 바이트 코드중 일부만 한번 보도록 하겠습니다.

5: bipush 10

위 코드는 인덱스 5에서 bipush라는 opcode가 10이라는 피연산자를 가지고있습니다.

7: putfield #2 // Field age:I

위 코드는 해시(#)가 달린 피연산자를 가지고 있습니다.
이것은 상수풀(constant pool)에서의 인덱스를 나타냅니다.
그리고 해당 피연산자가 가리키는 아이템을 나타내는 주석이 따라옵니다.
해당 주석은 javap가 생성한 주석입니다. 앞서 말한 바이트 코드 형식에서 <comment>에 해당하는 부분입니다.

마지막으로 컴파일러가 자동으로 생성해준 Person 클래스의 기본 생성자의 바이트 코드를 살펴보겠습니다.
제일 첫 줄의 0: aload_0의 의미에 대해 알아보겠습니다.
해당 명령어는 지역변수 배열의 0번째 있는 값을 피연산자 스택(Operand Stack)으로 로드하는 명령입니다.

이것이 무슨 말인지 이해하기 위해 JVM이 바이트코드를 실행하는 방식을 이해할 필요가 있습니다.

JVM은 스택기반 머신입니다. 각 스레드는 JVM 스택을 가지고 이 스택에는 프레임(Frame)들이 저장됩니다. 프레임은 생성자 혹은 함수가 실행될 때 생성되는 것으로 아래 그림과 같이 지역변수 배열피연산자 스택 그리고 상수풀 에 대한 참조로 구성되어있습니다.

그림 상단에서 볼 수 있는 지역변수 배열에는 객체에 대한 참조(this), 메소드의 인자 그리고 지역변수들이 0번부터 차례대로 들어갑니다.
static함수의 경우에는 파라미터가 0번부터 들어갑니다. 그리고 static함수에서는 당연히도 this를 쓸 수 없습니다. (객체에 속하는 것이 아니기 때문)


지금까지 자바 클래스의 바이트코드에 대해 알아보았는데요. 바이트코드는 클래스의 메소드 단위로 생성되기에 메서드 바이트코드라고 표현한 것 같습니다.

앞에서 알아본 내용을 바탕으로 다시한번 정리해보면, 메소드 영역은 JVM 클래스 로더에 의해 컴파일된 바이트 코드들이 로드되는 공간으로

  1. 자바 프로그램은 먼저 .java파일을 javac 컴파일러를 통해 .class 바이트코드로 컴파일합니다.

  2. JVM은 컴파일된 바이트코드를 JVM 메모리의 메소드 영역(이제 왜 메소드 영역이라고 하는지 아시겠죠? 메소드의 바이트코드들이 적재되기 때문!)에 로드함으로써 자바 프로그램을 실행합니다.

메소드영역에는 메소드의 바이트 코드 뿐만아니라, static 변수도 포함된다고 앞서 말씀드렸는데요. 이러한 이유는 static 변수또한 클래스에 포함되는 정보이기 때문 아닐까 생각해봅니다.

메소드 영역에 대해서 자세히 알아보았고 이제 다음 영역들에대해 차례로 알아보겠습니다.


Stack 영역

위에서 바이트 코드에 대해 설명하면서 나와버렸는데요.
다시한번 간단히 집고 넘어가겠습니다.

JVM 메모리의 Stack 영역에는 지역변수와 매개변수들이 저장됩니다.

앞서 말했듯이 JVM은 스택기반 머신이기 때문에 메소드나 생성자가 호출되면 스택에 프레임이 생성되고, 각 프레임은 지역변수 배열, 피연산자 호출스택, 상수풀에대한 참조로 구성되어있습니다.

멀티쓰레드 환경에서 각 쓰레드 단위로 호출스택이 존재하고 따라서 앞서 나온 메소드 영역이나 앞으로 나올 힙 영역과 다르게 각 쓰레드의 고유한 공간이라고 할 수 있습니다.

Stack 영역에 할당된 매개변수, 지역변수들은 메소드의 호출이 끝나면 소멸됩니다.


Heap 영역

Heap 영역은 생성된 인스턴스들이 동적으로 할당되는 메모리 공간입니다.

Heap 영역은 GC(Garbage Collector)에 의해 메모리 관리되고 있기 때문에, 할당된 인스턴스의 소멸시기는 GC에 의해 결정됩니다.
Java는 C언어와 달리 동적 할당된 인스턴스들을 직접 소멸시키지 않아도 GC에 의해 자동으로 관리되기 때문에 메모리 관리에 덜 신경써도 된다는 특징이 있습니다.

인스턴스들은 참조변수에 의해 참조가 되는데, 보통 이러한 참조관계가 소멸되면 GC의 소멸대상에 포함되게됩니다.


지금까지 자바의 메모리 영역에 대해 알아보았습니다.
JVM의 각 메모리 영역의 특징과 할당 방식에 대해 알아두는 것은 굉장히 중요하므로 한번쯤 정리하는 것을 추천드립니다.

출처 : https://iamsang.com/blog/2012/08/19/introduction-to-java-bytecode/

profile
백엔드 꿈나무 🐥

2개의 댓글

comment-user-thumbnail
2021년 1월 11일

헷갈리던 개념을 정리하는 계기가 되었어요!

답글 달기
comment-user-thumbnail
2021년 1월 17일

정말 정리 잘하셨네요! 잘 보고 갑니다 ㅎㅎ 총총총 @))))))))))

답글 달기