Stack과 Heap은 프로그래밍 언어에서 메모리를 사용할 때 알아야 하는 매우 중요한 개념이다
보통 Call by value, Call by reference등과 같은 말이 다 여기에서 나온 개념이며 C언어의 포인터 또한 이에 매우 관련이 있다
Stack은 위와같이 FILO방식으로 동작하는 자료구조인데 이를 메모리 공간에서도 적극 활용한다
대표적으로 자바스크립트의 Execution Context, 자바의 Stack Frame, Go의 Goroutines 등이 이와같은 구조로 프로그램을 순차적으로 실행해 간다
Go에서 Stack은 값이 사용되어지고 나서 바로 해제되는 것이아닌 추 후 값이 다시 실행될 때 해당값을 초기화 한 뒤 덮어씌우는 방식으로 메모리를 sweep한다 따라서 GC의 대상이 되지 않는다
Heap은 Shared memory영역으로 여러 Stack에서 특정 값을 참조하고자 할 때 사용된다
보통이를 Stack에서 Heap으로 Escape했다고 한다
따라서 Heap영역이 비대해질 수록 메모리 누수가 발생할 확률이 높아지며 장애로까지 이어질 수 있다
그래서 이를 관리해주는 Garbage Collector가 등장하였고 포인터에 대한 기능을 언어레벨에서 차단하였다
자바나 자바스크립트와 같은 언어들은 기본적으로 포인터라는 개념을 지원하지 않는다 따라서 포인터를 사용하지 않는거 같으나 내부적으로는 다 사용하고있다 보통 상수와같은 기본 Primitive한 타입들은 전부 Call by value이며 Object같은 Reference타입들은 Call by reference로 동작한다
쉽게 말해 값을 직접 넘겨주냐 혹은 주소값을 넘겨주냐의 차이이다
포인터는 위에서 말했듯이 값이 아닌 주소값을 넘겨주어 하나의 메모리 공간을 공유할 때 사용한다
Go나 C와같은 언어들은 그래서 일반적으로 사용되는 변수들은 Value자체를 넘겨주고 포인터 변수를 이용해야 Reference 주소값을 넘겨주는 것 이다
type Object struct {
a, b, c int64
}
func copy() Object {
return Object{
a: 1, b: 2, c: 3,
}
}
//go:noinline
func Stack() {
obj := copy()
fmt.Println(obj)
}
//go:noinline
func Heap() Object {
obj := copy()
return obj
}
func main() {
memory.Stack()
fmt.Println(memory.Heap())
}
위 코드는 단순하게 copy struct를 두가지 방식으로 print하는 경우이다
go run -gcflags="-m=1" main.go
위 커맨드를 통해 실행시켜보자
다음과같이 Stack은 독자적인 Stack영역에서 obj변수가 사용되어 바로 실행되는 반면
Heap은 main goroutine에서 Heap 함수와 main 함수에서 같이 사용되어지고 있다 따라서 이는 Stack -> Heap으로 Escape되어 같은 값을 바라볼 수 있게 작동이 된다
만약 포인터 변수를 Return시킨 상태로 컴파일 단계를 추적해보면 일반 변수만 escape되는걸 볼 수 있다 즉 pointer변수는 애초부터 heap영역에 저장된다는걸 알 수 있다
func Copy() Object {
return Object{
a: 1, b: 2, c: 3,
}
}
func Pointer() *Object {
return &Object{
a: 1, b: 2, c: 3,
}
}
func BenchmarkStack(b *testing.B) {
f, _ := os.Create("trace_stack.out")
trace.Start(f)
defer trace.Stop()
for i := 0; i < b.N; i++ {
Stack()
}
}
func BenchmarkHeap(b *testing.B) {
f, _ := os.Create("trace_heap.out")
trace.Start(f)
defer trace.Stop()
for i := 0; i < b.N; i++ {
Heap()
}
}
go test -count 10 -benchmem -bench=Benchmark > bench.txt
benchstat으로 통계를 내본결과 위와같이 stack과 heap 실행시간부터 메모리에 할당되는거까지 차이가 나는걸 볼 수 있다
실제 사용내용을 추적해보면 Stack은 1개의 P와 1개의 G에서만 로컬스택을 유지하면서 실행되는 반면
Heap은 여러 P, M, G가 사용되며 무수히많은 STW가 일어나는 걸 볼 수 있다
따라서 무분별한 Heap사용보다 Stack을 사용해야 하는것이 성능면에서나 동시성 문제에서 자유롭기에 데이터를 Shared해야하지 않다면 Stack을 활용하는것이 좋다