프로그래밍 일기 — Stack & Heap
JVM이 변수를 저장하는 방법
JVM은 최적화된 동작을 위해 메모리를 Stack과 Heap으로 나누어 관리한다. Java 프로그램에서 새로운 변수나 객체, 메서드를 선언할 때 JVM은 Stack혹은 Heap 메모리에서 이러한 동작을 위한 공간을 지정한다.
Stack
동적메모리할당(Static Memory Allocation) 및 쓰레드의 실행을 위해 활용한다. 특정 메서드에 대한 기본값(primitive values)을 가지며, 특정 메서드에서 참조하는 Heap에 저장된 객체들을 참조한다.
Stack은 LIFO(Last-in-First-out)의 방식으로 참조된다. 새로운 메서드를 호출하면, Stack위에 새로운 층이 만들어지며 해당 메서드에 활용되는 값을 저장한다. 대표적으로 객체에 대한 기본형 자료와 참조값을 저장한다.
메서드 호출이 끝나면, 해당 Stack 프레임의 데이터는 사라지며, 다음 호출 메서드를 위한 공간으로 활용될 수 있게된다.
Stack 메모리의 주요 기능
- 메서드가 호출됨에 따라, 해당 메서드가 활용하는 변수 및 데이터의 양과 크기에 따라 Stack이 늘어나거나 줄어들 수 있다.
- Stack내 변수는 이를 생성한 메서드가 동작할 때만 존재한다.
- 메서드 실행 후 자동으로 메모리가 할당되고 할당해제된다.
- 메모리가 다 소비되면, Java는
java.lang.StackOverFlowError
- Heap에 비해 읽기 속도가 더 빠르다.
- 각 쓰레드가 각각의 Stack에서 동작하므로 메모리는 쓰레드에서 안전(threadsafe)하다.
Heap
런타임시 Java객체 및 JRE클래스에 대한 동적메모리할당(Dynamic Memory Allocation)을 위해 활용한다. 새로운 객체(데이터)는 항상 Heap에서 생성되며, 이 객체를 참조하는 값들은 Stack 메모리에 저장된다.
이러한 객체들은 전역에서 접근할 수 있다. 따라서, 어플리케이션내 그 어느 곳으로 부터도 접근이 가능하다. 이러한 메모리 모델은 아래와 같이 더 작은 부분들로 나뉘어질 수 있다.
- Young Generation(차세대): 새로운 객체들이 할당되는 곳이다. 이 부분이 채워지면 작은 Garbage Collection이 작동한다.
- Old/Tenured Generation(구세대): 오래 저장되어야할 객체들이 저장되는 곳이다. 차세대 모델에 객체가 저장되면, 객체의 나이에 대한 Threshold(임계치)가 설정되고, 이 임계치에 도달하면 객체는 구세대 모델로 옮겨진다.
- Permanent Generation(영속세대): 런타임 클래스 및 어플리케이션 메서드에 대한 JVM메타데이터로 구성되어있다.
Java Heap 메모리의 주요 기능
Heap의 추가적인 기능은 아래와 같다.
- 차세대, 구세대, 영속세대 등을 포함한 복잡한 메모리 관리 기법을 통해 접근할 수 있다.
- Heap 공간이 다 차면, Java는
java.lang.OutOfMemoryError
에러를 던진다. - 이 메모리는 Stack에 비해 접근이 느리다.
- Stack에 비해 자동적으로 할당해제되지 않으며, 사용되지 않는 객체를 삭제하여 메모리 관리를 효율적으로 하기위해 Garbage Collector를 필요로한다.
- Stack과는 다르게, Heap은 쓰레드에서 안전하지않으며, 따라서 코드를 적절하게 동기화하여 보호해야한다.
예제를 통한 이해
예제를 통해 Java내에서 메모리를 어떻게 관리하는지 알아보자. 아래 코드를 분석해 보면 아래와 같은 과정을 거친다.
main()
메서드를 호출하면, Stack 메모리에 기본(primitive) 데이터와 메서드에 대한 참조값들을 저장하기위한 공간을 생성한다. 예를 들어 Stack 메모리는int
타입id
의 기본값을 직접 저장한다. 참조 변수Person
타입의person
변수에 대한 참조값이 Stack 메모리에 생성되고, 이는 Heap에 존재하는 실제 값을 가리킨다.main()메서드내 Person(int, String)
이라는 두 타입의 변수를 가진 생성자를 호출할 때, 이전 Stack에서 추가적인 메모리를 할당한다. 여기에는 호출하려는 객체(Stack에 저장된 데이터)에 대한this
참조값을 저장한다. Stack 메모리에 대한id
의 기본값도 저장하며,name
이라는String
인자에 대한 참조값을 저장해, Heap내 존재하는 String 자료들 중 해당 데이터에 대한 실제값을 가리킬 수 있다.main()
매서드는buildPerson()
static 메서드를 추가적으로 호출하여 이전 Stack 메모리에 추가적인 공간을 지정한다. 이 역시 위person
객체와 같이 이에 대한 참조값을 Stack에 저장하고 실제값은 Heap에 저장한다. 주의할 것은 이후Person
타입의person
객체가 새롭게 생성될시에, 해당 객체에 필요한 모든 인스턴스 및 변수는 Heap에 저장될 것이다.
class Person {
int id; // 기본형 자료는 실제값을 Stack에 저장
String name; // 참조값은 Stack에 실제값을 Heap에 저장
public Person(int id, String name) {
this.id = id; // Heap에 실제값을 저장
this.name = name; // Heap에 참조값과 실제값(String Pool에)을 저장
}
}
public class PersonBuilder {
private static Person buildPerson(int id, String name) {
return new Person(id, name);
}
public static void main(String[] args) {
int id = 23; // 기본형 자료의 실제값은 Stack에 저장
String name = "John"; // 참조형 자료의 참조값은 Stack에, 실제값은 Heap에 저장
Person person = null; // 최초 선언시 Stack에 this로 참조값이 저장됨(현재 실제값은 없는 상태)
// 이후 id의 실제값, name의 참조값 및 person에 대한 참조값이 Stack에 저장되고,
// 해당 person내 변수들의 id 실제값 및 name에 대한 참조값을 Heap에 가져오고,
// name에 대한 참조값으로 Heap내 String Pool에 있는 name의 실제값을 가리킨다.
person = buildPerson(id, name);
}
요약
- Application: Stack은 쓰레드 하나가 실행되는 동안 부분적으로 활용된다. Heap공간은 런타임시 어플리케이션 전체에 의해 사용된다.
- Size: Stack은 OS에 따라 크기 제한이 있고, Heap보다 작다. Heap은 크기에 제한이 없다.
- 저장: Stack은 기본형 변수에 대한 값과 Heap에 객체(데이터혹은 실제값)를 가지고 있는 참조형 변수에 대한 참조값을 저장한다. Heap은 새롭게 생성된 모든 객체를 저장한다.
- 순서: Stack은 LIFO 메모리 할당 방식으로 접근된다. Heap은 차세대, 구세대, 영속세대 모델을 포함한 복잡한 메모리 관리 기법으로 접근할 수 있다.
- 수명: 할당된 Stack공간은 현재 메서드가 실행되는 동안에만 존재하고, Heap공간은 어플리케이션이 실행되는 동안 내내 존재한다.
- 효율성: Stack은 Heap에 비해 훨씬 더 빨리 할당될 수 있다. Heap은 Stack에 비해 할당속도가 더 느리다.
- 할당/할당해제: 메서드가 호출될 때 Stack은 자동적으로 할당되거나 할당해제된다. Heap은 새로운 객체가 생성될때 할당되고, Garbage Collector에 의해 더 이상 해당 객체가 사용되지 않을 때 할당해제된다.
참조:
(1) https://pixabay.com/photos/candy-jelly-beans-confectionery-3200857/