Machine/JVM

JVM의 Memory 할당 방식 : Stack과 Heap Memory가 동작하는 방법

JVM의 메모리 할당 방식

JVM은 기본적으로 Stack Memory 와 Heap Memory 라 불리는 두가지 저장 공간을 이용해 메모리를 할당한다. 이들에 메모리를 할당하는 방법은 바이트 코드를 한 줄 한 줄 읽는 것이다. 한 줄 한 줄 읽혀진 값들은 JVM의 Stack 영역에 차곡차곡 

 

 

메모리 할당 예제

예를 들어 다음과 같은 코드가 있다고 해보자.

fun main() {
    val numberA = 12
    val numberB = 15
    val numberC = numberA + numberB
}

fun add(numberA: Int, numberB: Int): Int {
    return numberA + numberB
}

 

위 코드에서 main function이 수행되면 다음과 같은 과정을 거쳐 메모리가 할당된다.

1. numberA를 읽는다

그림1. 과정1

 

2. numberB를 읽는다.

그림2. 과정2

 

3. number1에 numberA값인 12를 할당한다, number2에 numberB의 값인 15를 할당한다.

그림3. 과정3

 

4. temp 값을 생성하여 number1 + number2를 한 값을 할당한다.

그림4. 과정4

 

5. temp에 저장된 값을 numberC에 할당한 후 return하면 add function과 관련된 모든 값들이 사라진다.

그림5. 과정5

 

6. main function이 끝났으므로 모두 지워진다.

그림6. 과정6

 

 

 

JVM의 Stack과 Heap

 

위의 과정을 통해 우리가 작성한 코드들이 어떻게 메모리에 올라가는지 살펴보았다. 기본적으로 코드들은 한 줄 한 줄 읽혀서 Stack이라 불리는 공간에 차곡차곡 쌓인다. 마지막에 들어온 변수가 먼저 나간다고 해서 LIFO(Last In First Out) 구조를 가진다고도 한다. 하지만, 모든 변수를 Stack에 저장할 수 있는 것은 아니다. 간단한 변수인 Primitive Type의 변수들은 Stack에 저장이 가능하지만, 복잡한 변수는 Heap 공간에 저장되고 Stack에는 해당 Heap 공간을 가리키는 변수가 저장된다.

 

정리하면 한 줄을 읽었을 때 변수가 있다면 무조건 Stack에 저장되는데, 데이터도 Stack에 저장되는 경우는 간단한 데이터인 경우이며, 조금만 복잡해지더라도 Stack에는 변수의 주소값만 저장되고 실제 데이터는 Heap 영역에 저장된다.

 

예를 들어 다음과 같은 main function이 수행되었다고 해보자. 

fun main() {
    val number = 12
    val myData = MyData(15)
}

class MyData(val data : Int)

 

이때 MyData는 클래스이므로 복잡한 데이터이다. 따라서 다음과 같이 저장된다.

 

 

그림7. Heap에 저장되는 방식

 

 

Stack에 저장되는 방식

JVM의 Stack에는 Primitive 타입의 변수들만이 저장된다. Java를 기준으로는 int, boolean 등과 같은 Primitive 타입의 변수들이 저장되며, 이들을 Boxing 한 Integer, Boolean과 같은 Reference 타입의 변수는 저장하지 못한다. Kotlin을 기준으로는 이것에 대해 생각할 필요가 없다. Int나 Boolean 타입을 쓰면 Kotlin 바이트코드 컴파일러가 Primitive이 들어갈 수 있는 곳에는 Primitive 타입으로 최적화시켜서 저장시켜주며, Primitive 타입을 쓸 수 없는 경우에는 Reference 타입의 변수로 저장해준다.

 

Heap에 저장되는 방식

JVM의 Heap에는 복잡한 타입의 변수 클래스 타입, Interface 타입, ArrayType의 변수들이 저장된다. 복잡한 데이터여서 Heap 내부에 Heap를 Reference한다면 Heap의 데이터가 다시 Heap의 데이터를 참조할 수도 있다.

 

예를 들어 아래와 같이 Array내부에 MyData가 두 개 있는 경우를 살펴보자.

fun main() {
    val myDataArray = arrayOf(MyData(1), MyData(2)
}

class MyData(val data : Int)

 

그러면 Stack에서 myDataArray는 Heap의 Array<MyData>를 가리키고, 이 Array에는 주소값이 2개 있어서 각 주소값은 MyData(1)과 MyData(2)를 가리킨다.

그림8. Array 내부에 복잡한 데이터가 있는 경우

 

 

Heap의 한계점

Stack은 좁은 메모리 공간이지만, Heap은 넓은 메모리 공간이다. Stack 메모리에 값을 할당하고 해제하는 것은 많은 비용이 들지 않지만, Heap 메모리에 값을 할당하고 해제하는 것은 많은 비용을 요한다. 따라서 너무 Heap에 데이터를 자주 할당하고 해제하는 방식으로 코드를 짠다면 같은 방식을 Stack에 했을 때보다 수십배의 속도 차이가 날 수 있다. 이 점을 유의하면서 Heap을 사용할 때는 이 점을 유의해서 코드를 짜도록 하자.

 

 

정리

즉, Stack에는 정말 간단한 변수나 Heap을 가리키는 주소값 둘 중 하나만 저장된다. 만약 Heap에 저장되는 변수에 복잡한 데이터가 있을 경우에는 Heap 데이터 내부에 Heap의 메모리 공간을 가리키는 변수가 저장된다. 

반응형

 

이 글의 저작권은 Dev World 에 있습니다. 글, 이미지 무단 재배포 및 변경을 금지합니다.

 

 

Kotlin, Android, Spring 사용자 오픈 카톡

오셔서 궁금한 점을 질문해보세요!
비밀번호 : kotlin22

open.kakao.com