메모리 할당
- 어떤 변수 선언 = 메모리 할당
- 변수를 선언하기 위해 할당되는 메모리로는 크게 스택과 힙이 있음
스택(Stack) 영역
- 함수의 호출과 함께 지역 변수 / 매개 변수 등이 할당됨
- 정렬된 방식으로 메모리 할당 및 해제됨
힙(Heap) 영역
- 클래스 변수나 인스턴스 변수 / 객체 등이 할당됨
- 우연하고 무질서하게 메모리 할당됨
- ➡️ JVM 은 무질서하게 관리되는 힙 영역을 위주로, GC 통해 메모리 해제 관리함
- (인스턴스 변수로 존재하는 원시 변수는 힙 영역에서 관리됨)
원시 변수(Primitive Value)의 메모리 할당
- 자바에서 변수는 객체가 아닌 실제 값들인 int, double, float boolean 등과 같은 원시 값(Primitive Value)들이 존재
public void test() {
// Primitive Value
int x = 3;
float y = 10.012f;
boolean isTrue = true;
}
- 메모리에서는 아래처럼 스택 영역에 할당됨
- 스택 영역에 실제 값들이 저장되는 것
객체(Object)의 메모리 할당
- 원시 변수는 스택 영역에 실제 값들이 할당됨
- 그러나 객체는 원시 변수와 다른 방식으로 값이 할당됨
public void test() {
// Primitive Value
int x = 3;
float y = 101.012f;
boolean isTrue = true;
// Object
String name = "MangKyu";
String[] names = new String[3];
names[0] = "I";
names[1] = "am";
names[2] = new String("MangKyu");
}
- 먼저 객체의 경우 힙 영역에 실제 값이 할당됨
- 그리고 이에 접근하기 위해 스택 영역에는 힙 영역에 존재하는 값의 실제 주소가 저장됨
- (C/C++에서는 이를 포인터(pointer)라 부름)
- 즉, 스택 영역에는 실제 값이 존재하는 힙 영역의 주소가 저장됨
- 힙 영역은 스택 영역과 다르게, 무질서하게 메모리 공간을 활용함
- 객체의 메모리 할당인 경우, 스택 영역에 실제 값을 참조하기 위한 Reference(참조값)이 저장됨 ➡️ 이를 참조하여 실제 값에 접근
- 배열의 경우, 그 배열의 인덱스마다 참조값이 할당되며, 이를 통해 실제 값에 접근
테스트 코드
class Dog {
private String name;
public Dog (String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
public class Test {
public static void main(String[] args) {
int x = 10;
int[] y = {2, 3, 4};
Dog dog1 = new Dog("강아지1");
Dog dog2 = new Dog("강아지2");
// 함수 실행
foo(x, y, dog1, dog2);
System.out.println("x = " + x);
System.out.println("y = " + y[0]);
System.out.println("dog1.name = " + dog1.getName());
System.out.println("dog2.name = " + dog2.getName());
}
public static void foo(int x, int[] y, Dog dog1, Dog dog2) {
x++;
y[0]++;
dog1 = new Dog("이름 바뀐 강아지1");
dog2.setName("이름 바뀐 강아지2");
}
}
테스트 결과
x = 10
y = 3
dog1.name = 강아지1
dog2.name = 이름 바뀐 강아지2
- 위 결과의 근본적인 이유 : 자바는 Pass By Value 로 변수를 전달하기 때문
foo() ➡️ int x
- 스택에 파라미터가 할당됨 = 즉, 새로운 x 가 스택에 10의 값으로 생성됨
- foo 는 x 값 증가시킴 = 기존의 x 가 아닌, 새로운 x 값을 증가시킴
- ➡️ 기존의 x 는 10 으로 값 유지
- ➡️ 새로운 x 에 10 할당된 후, 11 로 증가
- ➡️ foo 함수 종료 시, 새로운 x 는 스택에서 해제됨
- 그러므로 foo 호출한 후 x 출력 시, 기존의 x 출력됨
foo() ➡️ int y
- 스택에 새로운 파라미터인 int[] y 할당
- y 의 0 번째 인덱스에 존재하는 값을 1 증가
- 새로운 y 가 할당되었으나, x 와 달리 배열의 주소를 가리킴
- 해당 y 가 기존의 y 이든 새로운 y 이든, 실제 배열의 주소로 접근하여 값을 1 증가시키기에, foo가 종료된 후에도 배열의 값은 변화하게 됨
foo() ➡️ Dog dog1
- 스택에 새로운 파라미터인 dog1 생성
- y 와 마찬가지로 dog1 은 객체이므로 dog1 의 주소를 값으로 가짐
- foo 함수는 dog1 을 새로 생성
- ➡️ 힙에는 새로운 dog1 이 생성됨
- ➡️ 스택에서 가리키는 주소값은 새로운 dog1 을 가리키게 됨
- ➡️ foo 함수 종료 시, 새로운 dog1 은 소멸 및 기존의 dog1 에는 변화 X
foo() ➡️ Dog dog2
- 네 번째 파라미터로 Dog 클래스의 인스턴스인 dog2 를 받아옴
- dog1 때와 마찬가지로 스택에는 새로운 dog2 할당됨 ➡️ 실제 dog2 의 주소값 가리킴
- 그러나 dog2 가 수행하는 연산 != dog1 연산
- dog2 는 새로운 값 할당 X, 주소값 통해 힙에 존재하는 객체에 접근 ➡️ set() 으로 값 변화시킴
- foo() 함수 종료 시, 스택에 할당된 새로운 dog2 소멸됨
- 그러나 힙에 존재하는 값은 변하였기에 foo 함수가 종료된 후 dog2 의 name 출력 시 "이름 바뀐 강아지2"임을 확인
foo() 종료 결과
Pass by Value (값에 의한 전달)
- 복사된 데이터를 전달하여 구성
- 값을 수정해도 원본 데이터에는 영향 주지 않음
- process() 내에서 해당 값을 수정해도, 해당 해당 함수가 종료되면 원본 값은 기존 상태 유지
- 먼저 main() 호출 ➡️ 스택에 main() 과 그 지역 변수로 선언한 someValue 존재
- 그 위로 process() 의 Return Address 가 스택에 쌓임
- 그 위로 process() 의 파라미터인 int 변수 value 가 스택에 쌓임
- 메모리에 할당된 main 함수의 someValue는 그대로 유지된 상태
- 7이라는 값만 복사하여 새롭게 메모리를 할당함
- main() 의 someValue 주소값 = 0x07040
- process() 의 value 주소값 = 0x07000
- 두 변수는 값만 같을뿐 다른 존재
Q. process() 로 전달받은 파라미터 value 에 어떤 연산이 수행된 후 process() 가 종료되면?
- process() 위해 할당된 Call Stack 이 pop 되고 ➡️ 해당 메모리는 모두 소멸
- 그렇기에 process() 종료된 후 main() 에서 someValue 를 다시 출력해도 7이라는 기존 값 유지
https://mangkyu.tistory.com/107
Pass by Reference (참조에 의한 전달)
- Pass by Reference 가 어떤 Alias 를 구성하여 실제 값에 접근한다는 관점에서
- Pass by Pointer 는 Alias 로 주소값을 넘겨주었을 뿐이기 때문에 동일한 기술이라고 볼 수 있음
- Pass By Reference 는 주소값을 전달해 실제 값에 대한 Alias 를 구성
- ➡️ 값 수정 시, 원본 데이터가 수정되게 함
- Pass by Value 에서는 실제 값이 쌓인 것과 다르게, Pass by Reference 에서는 실제 값의 주소가 쌓임
- process() 에서 파라미터로 전달받은 value 는 someValue 가 저장된 값을 참조하는 Alias
- value 를 10 으로 변경 = value가 가리키는 메모리(실제 someValue 가 저장된 메모리)의 값을 7에서 10으로 변경
- 그래서 process() 종료 후 someValue 출력 시, 10 으로 변경되어 있음
자바에서 Pass by Value
- 이론적으로 원시 값은 스택 메모리에 실제 값이 저장되고, 객체는 실제 메모리를 참고하기 위한 값이 저장됨
- 그렇기에 자바에서는 객체에 한해 확장된 규칙이 적용됨
- 객체 복사 후 전달되는 값 = 실제 메모리를 가리키는 Reference(참조값)인 포인터
자바에서 객체 생성 시
Dog dog = new Dog();
- dog 은 실제로 생성된 Dog 클래스의 객체를 저장하는 것이 아니고,
- Dog 클래스의 객체가 저장된 주소값(포인터값)을 가지고 있는 것
- 자바에서의 객체 전달 = 이런 dog 변수가 복제되어 전달
- 자바에서 어떤 객체가 파라미터로 전달되면, 필드값에 접근해 해당 값 수정은 가능하나, 그 객체 자체는 변경 불가능
예시
class Foo {
private int value;
public Foo(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
}
public class Main {
public static void main(String[] args) {
Foo someFoo = new Foo(7);
process(someFoo);
System.out.println(someFoo.getValue());
}
public static void process(Foo foo) {
// 주소 값을 통해 필드 값을 변경
foo.setValue(10);
// 객체 자체를 변경하는 것은 영향 X
foo = new Foo(15);
}
}
- 이번에는 main() 에서 Foo 클래스에 대한 객체 생성 후 이를 process() 의 파라미터로 넘김
- 자바에서는 Pass by Value 에 따라 Foo 객체가 존재하는 주소(0x07070)를 갖는 someFoo 변수를 복사하여 전달
- ➡️ process() 가 주소값을 통해 필드값에 접근하게 해줌
- 그러나 process() 종료 시, copied pointer(0x07000) 는 소멸됨
- Foo 객체 자체에 변경사항이 있었다면 이는 반영되지 않음
Pass by Reference(참조에 의한 전달)
- 주소값을 전달하여 실제 값에 대한 Alias 를 구성 = 값 수정 시 원본의 데이터가 수정되는 방식
- 자바에서 객체를 전달하는 방식에는 분명 주소값이 전달되고 있지만, 이는 someFoo 에 대한 복사본일 뿐
- process() 의 파라미터로 전달받은 foo 역시 실제 주소값을 참조하고 있기에, foo 의 setValue 를 통해 실제 객체의 필드값 수정 시 결과에 반영됨
- 그러나 이는 객체의 주소값으로 객체의 필드값에 접근하여 값을 변경하는 것일 뿐, 실제 객체 자체에 변화 X
정리
전달 방식 | Pass by Value | Pass by Reference |
의미 | 복사된 데이터를 전달해 구성하는 방식 | 주소값을 전달해 실제 값에 대한 Alias 구성하는 방식 |
원본 데이터 변화 | 값을 수정해도 원본 데이터는 변화 X | 값 수정 시, 원본 데이터 수정됨 |
출처
https://mangkyu.tistory.com/105
'Backend > Java' 카테고리의 다른 글
[Java] Exception (0) | 2023.12.05 |
---|---|
[Java] Enum (0) | 2023.12.05 |
[Java] 기본 자료형 & 참조 자료형 (0) | 2023.12.05 |
[자바의 신] 29장 (ongoing) (0) | 2022.08.31 |
[자바의 신] 28장 (ongoing) (0) | 2022.08.31 |
댓글