메서드가 끝나면 스택에서 사라지는 것과 힙에 남는 것

동딩
2026.10.12·2분 읽기·

호출한 쪽 값은 반환받아 바꾼다

메서드 안의 한 줄이 호출한 쪽에 남는지는 그 줄이 무엇을 바꾸는지로 갈립니다. 매개변수에 =로 새 값을 넣으면 그 메서드의 복사본만 바뀌고 메서드와 함께 사라집니다. 점을 찍어 매개변수가 가리키는 객체를 고치면 호출한 쪽도 같은 객체를 보고 있으니 남습니다. n++도 n = n + 1이라 앞의 경우입니다.

bump 안의 줄바뀌는 것main에 남나
n++스택의 복사본 n안 남는다
c = new Counter()스택의 복사본 c안 남는다
c.value++힙의 Counter #1남는다

호출한 쪽 변수를 바꿔야 하면 메서드가 새 값을 반환하고, 호출한 쪽이 그 값을 받아 넣습니다. 지금 고치는 메서드에서 매개변수에 =로 새 값을 넣는 줄을 찾아, 그 값이 호출한 쪽에 필요했다면 이렇게 고칩니다.

java
static int bump(int n, Counter c) {
c.value++;
return n + 1;
}
public static void main(String[] args) {
int n = 1;
Counter c = new Counter();
n = bump(n, c);
System.out.println("n = " + n); // n = 2
System.out.println("c.value = " + c.value); // c.value = 2
}

인자는 값이, 객체는 주소가 복사된다

아래 bump는 n과 c.value를 하나씩 올립니다. 돌아와 보면 n은 1 그대로이고 c.value만 2입니다.

java
static class Counter { int value = 1; }
static void bump(int n, Counter c) {
n++;
c.value++;
c = new Counter();
c.value = 100;
}
int n = 1;
Counter c = new Counter();
bump(n, c);
System.out.println(n + " " + c.value); // 1 2

bump(n, c)를 부르면 자바는 인자의 값을 bump의 매개변수로 복사합니다. 그 안의 n은 main의 n과 처음 값만 같은 다른 변수입니다.

복사본은 이 메서드의 프레임에 놓입니다. 프레임은 메서드 호출 하나가 쓰는 칸이고, 매개변수와 지역 변수가 여기 들어갑니다. 호출 순서대로 스택에 쌓이고, 호출이 끝나면 안의 변수와 함께 통째로 사라집니다.

new로 만든 객체는 힙에 놓이고, 메서드가 끝나도 남습니다. 객체 변수 c에 든 값은 힙에 있는 객체의 주소입니다. c를 넘기면 이 주소가 복사되어, 두 프레임의 c가 힙의 같은 객체를 가리킵니다.

bump가 끝나면 주소로 고친 힙 객체만 남는다

스택힙bump 프레임끝나면 사라짐n1 → 2c#1 → #2main 프레임n1c#1값 1주소 #1Counter #2value 100아무도 안 가리킴GC가 거둔다Counter #1value 1 → 2c = newc.value++같은 객체main의 n은 1 그대로c.value는 2
main에 남는 변화bump와 함께 사라짐
main이 bump(n, c)를 부르고 돌아왔을 때
람다 안에서 바깥 지역 변수를 바꾸려다 컴파일 오류가 났다면

람다도 만들어질 때 쓰는 지역 변수를 복사해 갑니다. 람다가 실행될 때는 그 변수가 있던 프레임이 이미 사라졌을 수 있기 때문입니다. 복사본을 바꿔 봐야 바깥 변수는 그대로이니, 자바는 이런 대입을 컴파일 단계에서 막습니다.

java
int total = 0;
prices.forEach(p -> total += p); // 컴파일 오류
text
error: local variables referenced from a lambda expression must be final or effectively final

오류 문구의 effectively final은 선언한 뒤 다시 대입하지 않는 변수를 말합니다. 합계는 바깥 변수에 쌓지 말고 결과를 반환받아 씁니다.

java
int total = prices.stream().mapToInt(Integer::intValue).sum();

람다 안에서 객체 필드를 고치는 줄은 복사해 간 주소로 힙의 같은 객체를 고치므로 통과합니다. 여러 스레드가 함께 올리는 카운터는 지역 변수 대신 힙에 두되, 서로 덮어쓰지 않게 AtomicInteger로 씁니다. 스레드는 스택을 저마다 따로 갖고 힙만 함께 쓰기 때문입니다.

Comments

댓글

powered by giscus ↗