▩ 목 차 ▩
1. 기본자료형의 값 비교
1-1. 기본자료형의 값 저장은 Stack 영역(메모리)에서만 한다.
1-2. 메모리 관점에서의 기본자료형 할당 정리
2. 참조자료형의 값 비교
2-1. 참조자료형의 실제값은 Heap 영역, 실제값을 가르키는 레퍼런스 변수는 Stack 영역
2-2. Stack 영역에서 Heap 영역의 값을 참조하는 변수가 없다면 Garbage Colletion을 이용해 처리한다.
2-3. 위에서 배운 stack 영역과 heap영역을 생각하고 참조자료형의 값 비교를 해보자.
2-4. 메모리 관점에서의 passByReference()의 원리를 이해해보자.
2-5. 메모리 관점에서의 참조자료형 할당 정리
저번에 기본자료형, 참조자료형에 대한 "=="과 eqauls() 비교에 대해서 알아보았다.
나는 더욱더 근본적으로 기본자료형과 참조자료형이 메모리에 어떻게 할당이 되었고, 그 메모리에서 어떻게 값을 비교하는지를 알고 싶었다.
[ 자바에서는 JVM이 운영체제(os) 로부터 프로그램을 수행하는데 필요한 메모리를 할당을 한다. JVM의 구조같은 경우에는 메모리 할당 도 하고 하는일이 다양하지만, 내가 여기서 다루는 내용은 할당되는 영역에 대해 간단히 배우고 그 영역에 기본자료형과 참조자료형이 어떻게 저장되는지를 다루도록한다. [JVM의 구조를 조금 더 자세히 알고 싶다면 아래의 링크를 참고해라.] ]
■ 1. 기본자료형의 값 비교 ■
기본자료형을 저장하는 메모리 영역은 할당받은 Runtime Data Areas 중 Stack 영역이다.
==> 그렇기에 간단히 Stack 영역에 대해 공부를 하고 예시를 보며 이해하자.
■ 1-1. 기본자료형의 값 저장은 Stack 영역(메모리)에서만 한다.
Stack
- 함수의 호출과 관계되는 지역변수와 매개변수가 저장되는 영역.
- Heap 영역에 생성된 Object타입의 데이터의 참조값이 할당된다.
- 기본자료형의 데이터가 값과 함께 할당된다.
[ 기본자료형의 데이터들에 대해서는 참조값을 저장하는 것이 아니라 실제 값을 Stack에 직접 저장하게 된다. ] - 지역변수들은 scope[ 변수를 사용할 수 있는 범위 ]에 따른 visibility를 가진다.
- heap에 저장된 값들을 Stack영역이 참조해 사용한다
- 각 Thread는 자신만의 stack을 가진다.
- Stack 영역은 후입 선출(LIFO, Last-In-First-Out)의 방식으로, 가장 나중에 들어온 데이터가 가장 먼저 인출된다.
기본자료형의 값 비교이므로 일단 heap부분의 설명은 뒤로 가서 하겠다.
==> 왜냐하면 기본자료형의 값 비교같은 경우에는 heap이 필요없고, stack에 저장이 되고, stack에 저장된 값을 비교를 하기 때문이다.
[EX] - 기본자료형의 Stack 영역 저장
public class Main {
public static void main(String[] args) {
int argument = 4;
argument = someOperation(argument);
}
private static int someOperation(int param){
int tmp = param * 3;
int result = tmp / 2;
return result;
}
}
위의 코드를 보면 이야기 해보자.
전체적인 코드의 움직임은 argument에 4라는 값을 최초로 할당했고, 이 argument 변수를 SomeOperation메소드의 매개변수로 넘겨주고 결과값을 다시 argument로 할당한다. 구체적으로 하나하나 살펴보자.
1. int argument= 4; 에 의해 스택에 argument라는 변수명으로 공간이 할당되고, argument 변수의 타입은 기본자료형이므로 이 공간에는 실제 4라는 값이 할당된다.
2. someOperation()메소드가 호출된다. 호출될 때 인자로 argument 변수를 넘겨주며 scope가 someOperation()메소드로 이동한다. [ scope가 바뀌면서 기존의 argument 라는 값은 scope에서 벗어나므로 사용할 수 없다. ]
이 때 매개변수로 넘겨받은 argument는 param이라는 변수로 복사되어 전달되고, param 또한 기본자료형이므로 Stack에 할당된 공간에 값이 할당된다.
3. 다음으로 메소드 안에서의 코드가 실행되는데 이 코드들은 int tmp = param *3; int result = tmp / 2; 이다
이 tmp 변수와 result 변수는 기본자료형이므로 Stack에 쌓이게 된다.
4. 그 다음, 닫는 중괄호 } 가 실행되어 someOperation() 메소드가 종료되면서 호출한 함수 scope에서 사용되었던 모든 지역변수들은 stack에서 pop된다.(위에서 부터 사라짐) 그 후 다시 main함수로 scope가 이동하게 되고 argument의 값은 someOperation()의 결과값을 리턴받고 값이 변하여 저장된다.
5. main() 함수가 종료되는 순간 stack에 있는 모든 데이터들은 pop 되면서 프로그램이 종료된다.
■ 1-2. 메모리 관점에서의 기본자료형 할당 정리
기본자료형은 JVM이 운영체제로부터 할당받은 영역 Runtime Data Areas 중 Stack 영역에 쌓이게 된다.
또한 Stack에 변수명으로 할당이 되고, 이 변수명에 할당해주려던 실제 값이 직접 저장된다.
스택에서 scope(변수를 사용할 수 있는 범위)의 개념은 매우 중요하다.
==> 왜냐하면 scope의 유무에 따라 변수가 pop되어 사라져 사용하지 못하기 때문이다.
기본자료형에서 "==" 비교는 Stack에 저장되어 있는 변수명을 비교하는것이다.
==> 변수명을 비교하더라도 이 변수명에는 실제 값이 들어있기 때문에 비교가 가능한것이다.
■ 2. 참조자료형의 값 비교 ■
참조자료형을 저장하는 메모리 영역은 할당받은 Runtime Data Areas 중 Heap 영역이다.
하지만 신기하게, 실제값은 Heap영역에 있고 Heap영역에 있는 오브젝트들을 가리키는 레퍼런스 변수가 Stack에 올라간다.
==> 즉, "new" 예약어를 사용하여 만든 참조자료형은 실제값은 Heap 영역, 이 실제값을 가르키는 레퍼런스 변수는 Stack 영역에 있다고 생각하자.
■ 2-1. 참조자료형의 실제값은 Heap 영역, 실제값을 가르키는 레퍼런스 변수는 Stack 영역
Heap
- Heap 영역에는 주로 긴 생명주기를 가지는 데이터들이 저장된다.
- 참조형(Reference Type)의 데이터 타입을 갖는 객체(인스턴스)와 배열 등은 Heap 영역에 데이터가 저장되며 이러한 Heap 영역에 있는 값을 가르키는 레퍼런스 변수는 Stack 영역에 저장된다. Heap 영역에 있는 실제 값들만이 GC의 주 대상이 된다.
- 애플리케이션의 모든 메모리 중 stack에 있는 데이터를 제외한 부분이라고 보면 된다.
- 모든 Object 타입(Integer,String,ArrayList,객체)는 heap 영역에 생성된다.
- 몇 개의 스레드가 존재하든 상관없이 단 하나의 heap 영역만 존재한다.
- Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수가 stack에 올라가게 된다.
- Heap 영역은 선입선출(FIFO, First-In-First-Out)의 방식으로, 가장 먼저 들어온 데이터가 가장 먼저 인출된다.
[EX] - 실제값은 Heap 영역 / 실제값을 가르키는 레퍼런스는 Stack 영역
public class Main {
public static void main(String[] args) {
int port = 4000;
String host = "localhost";
}
}
1. int port = 4000;에 의해서 기존처럼 4000 이라는 값이 port라는 변수명으로 할당되어 stack에 쌓인다.
2. String은 Object를 상속받아 구현되었으므로 String은 heap 영역에 할당되었고, stack에 host라는 이름으로 생성된 변수는 heap 에 있는 "localhost"라는 String을 레퍼런스(참조)하게 된다.
[EX] - Object(참조자료형)의 stack과 heap영역의 메모리 할당과 해제 예시(scope의 개념 중요성)
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> listArgument = new ArrayList<>();
listArgument.add("yaboong");
listArgument.add("github");
print(listArgument);
}
private static void print(List<String> listParam) {
String value = listParam.get(0);
listParam.add("io");
System.out.println(value);
}
}
1. main() 함수의 시작부분은 List<String> listArgument = new ArrayList<>(); 로 시작한다.
"new" 예약어를 사용하여 생성하려는 오브젝트를 저장할 수 있는 충분한 공간이 heap영역에 있는지 먼저 찾은 다음, 빈 List를 참조하는 listArgument라는 지역변수를 스택에 할당한다.
2. 다음으로 listArgument.add("yaboong"); 구문이 실행되고 이 구문은 listArgument.add(new String("yaboong")); 과 같은 역할을 한다.
즉, "new"예약어에 의해 heap 영역에 충분한 공간이 있는지 확인한 후 "yaboong"이라는 문자열을 할당하게 된다. 이때 새롭게 생성된 문자열인 "yaboong"을 위한 변수는 stack에 할당되지 않는다.
List내부의 인덱스에 의해 하나씩 add()된 데이터에 대한 레퍼런스 값을 갖게 된다. [ List의 add()를 통해 heap영역의 List내에 String 타입으로 yaboong을 저장하였고, listArgument는 heap에 저장된 List의 실제값을 참조하는 레퍼런스 변수이므로 List의 add()를 한 것 "yaboong"도 접근할 수 있다. 왜냐하면 "yaboong"이라는 String을 새로운 heap영역에 공간을 할당한 것이 아닌 List에 추가한 것이기 때문 ]
3. 다음으로 listArgument.add("github"); 구문이 실행되고 List에서 레퍼런스 하는 문자열이 하나 더 추가된다.
4. 다음으로 print(listArgument); 구문에 의해 함수 호출이 일어난다. 이때 listArgument라는 참조자료형을 매개변수로 넘겨준다.
이 메소드에서 listArgument를 listParam이라는 변수로 바꾸어 사용한다.
또한 함수 호출시 인자가 가지고 있는 값이 그대로 파라미터에 복사된다.
print()메소드 내부에서 listArgument는 scope 밖에 있게 되므로 접근할 수 없는 영역이 된다.
5. print()메소드 내부에서는 String value = listParam.get(0); 구문을 통해 List에 있는 데이터에 접근하여 값을 value라는 변수에 저장한다. 이 때 print()메소드의 scope에서 stack에 value가 추가되고, 이 value는 listParam을 통해 List의 0번째 요소[ List[0] ]에 접근하여 그 참조값을 가지게 된다. 그리고나서 listArgument.add("io"); 구문을 통해 데이터를 추가하고 출력함으로서 print()메소드는 종료된다.
6. 메소드가 닫는 중괄호에 의해 종료되면 print()메소드의 지역변수는 모두 stack에서 pop되어 사라진다.
이때, List는 Object 타입이므로 지역변수가 모두 stack에서 pop되더라도 heap 영역에 그대로 존재한다.
즉, 함수호출시 레퍼런스 값을 복사하여 가지고 있던 listParam과 메소드 내부의 지역변수인 value만 스택에서 사라지고 나머지는 모두 그래도인 상태로 함수 호출이 종료된다.
[EX] - Integer[ Immutable Object 타입 ]의 연산 Stack, Heap 메모리 영역 저장
public class Main {
public static void main(String[] args) {
Integer a = 10;
System.out.println("Before: " + a);
changeInteger(a);
System.out.println("After: " + a);
}
public static void changeInteger(Integer param) {
param += 10;
}
}
- Integer는 Object 타입이므로, Integer a = 10;에서 10은 heap 영역에 할당되고, 10을 가리키는 레퍼런스 변수 a가 스택에 할당된다.
- 다음 구문인 ChangeInteger(a); 구문에 의해, Integer 오브젝트 a는 매개변수로 쓰이고, param이라는 레퍼런스 변수가 스택에 할당되고, 이 param은 main() 함수에서 a를 가리키던 곳을 똑같이 가르키고 있다. [ ChangeInteger()메소드에서 a는 scope 밖에 있게 되므로 접근할 수 없는 영역이 된다. ]
- main() 함수에서 레퍼런스하던 a와 같은 곳을 param 이 가리키고 있으므로 param 에 10을 더하면 , changeInteger()함수가 종료되고 a의 값을 출력했을 때 바뀐 값이 출력될 것이다.
==> 결과는 20이 아니다. 즉 값이 안바뀐다 왜 그럴까? [ 밑에서 배우겠지만, 불변객체일 경우에는 어떤 연산을 수행할때마다 기존 오브젝트를 변경하는 것이 아니라 새로운 오브젝트를 생성하는 것이라고 알고 있을 것이다. 그렇기에 값을 변경이 아닌, heap에 새로운 오브젝트를 생성한 것이다. ]
[EX] - String 불변객체(immutable)의 연산 Stack, Heap 메모리 영역 저장
public class Main {
public static void main(String[] args) {
String s = "hello";
changeString(s);
System.out.println(s);
}
public static void changeString(String param) {
param += " world";
}
}
- main() 함수의 s변수가 레퍼런스하는 "hello" 오브젝트를 매개변수로 param에 복사하면서 changeString() 메소드가 시작된다.
[ ChangeString()메소드에서 s는 scope 밖에 있게 되므로 접근할 수 없는 영역이 된다. ] - param += "world"; 를 실행하는 것은 heap에 "hello world" 라는 String 오브젝트가 새롭게 할당되는 작업이다
[ 즉, heap에서 기존 오브젝트를 hello->hello world 라고 바꾸는게 아닌 새로운 hello world 오브젝트를 생성한 것이다. 왜? 불변객체를 연산했기 때문에~ ] - 기존에 "hello"오브젝트를 레퍼런스하고 있던 param(Stack영역)으로 새롭게 생성된 String 오브젝트(Heap영역)인 "hello world"를 레퍼런스 하도록 만드는 것이다. [ 즉, Stack영역(param)은 새롭게 할당되지 않으며 Heap 영역은 새롭게 할당("hello" -> "hello world")된다. ]
- changeString()함수가 종료되면 새롭게 생성된 "hello world" 오브젝트를 레퍼런스 하는 param이라는 변수는 스택에서 pop 되므로 어느것도 레퍼런스 하지 않는 상태가 된다. [ changeString()함수가 종료되면 param이라는 변수는 스택에서 pop되고 scope가 main()함수로 옮겨지기 때문이다. changeString()함수 안에 있는 변수들을 사용할 수 없게 되는 것이다. ]
- Object 타입은 지역변수가 모두 stack에서 pop되더라도 heap 영역에 그대로 존재하는 개념이 있다.
==> 여기서 생각해야 되는것은 main()메소드의 s라는 변수는 heap영역에서 "hello"를 가르키고있고, changeString()메소드는 불변객체(Immutable Object)와의 연산과정에서 s라는 변수가 가르키는 heap 영역의 "hello"를 "hello world"로 변경하지 않고 새로운 공간을 할당하여 "hello", "hello world" 두 공간이 있는것이다. 그러므로 changeString()메소드가 종료해도 s의 경우에는 새롭게 할당된 공간 "hello world"에 접근하지도 못하고 자신이 참조하고 있는 "hello"의 값을 참조하는것이다. - 이러한 경우 "hello world" 오브젝트는 garbage로 분류된다.
==> changeString()함수가 종료되면 그 누구도 참조하지 않기 때문
결과값으로 changeString()메소드를 수행하고 돌아가도 기존에 "hello"를 레퍼런스 하고 있던 변수의 값은 그대로이다.
==> Immutable Object는 불변객체로써, 값이 변하지 않는다. 변경하는 연산이 수행되면 변경하는 것처럼 보이더라도 실제 메모리에는 새로운 객체가 할당되기 때문이다.
[ 쉽게 자세히 풀어서 말한다면 , Object 타입은 지역변수가 모두 stack에서 pop되더라도 heap 영역에 그대로 존재하는 개념이 있다.
==> 여기서 생각해야 되는것은 main()메소드의 s라는 변수는 heap영역에서 "hello"를 가르키고있고, changeString()메소드는 불변객체(Immutable Object)와의 연산과정에서 s라는 변수가 가르키는 heap 영역의 "hello"를 "hello world"로 변경하지 않고 새로운 공간을 할당하여 "hello", "hello world" 두 공간이 있는것이다. 그러므로 changeString()메소드가 종료해도 s의 경우에는 새롭게 할당된 공간 "hello world"에 접근하지도 못하고 자신이 참조하고 있는 "hello"의 값을 참조하는것이다. ]
■ 2-2. Stack 영역에서 Heap 영역의 값을 참조하는 변수가 없다면 Garbage Colletion을 이용해 처리한다.
[EX] - Garbage Collection
public class Main {
public static void main(String[] args) {
String url = "https://";
url += "yaboong.github.io";
System.out.println(url);
}
}
1. 위 코드에서 String ulr = "https://"; 구문이 실행된 뒤 스택과 힙은 아래와 같다.
2. 다음 구문인 url += "yaboong.github.io"; 구문을 실행한다. 불변객체(String)의 연산이므로 "https://"String에 "yaboong.github.io"를 덧 붙인 "https://yaboong.github.io"로 바뀌는것이 아니라
새로운 String "https://yaboong.github.io"이 할당되는 것이다.
위의 사진을 보게되면 Stack에는 새로운 변수가 할당되지 않는다.
즉, 문자열 더하기 연산의 결과인 "https://yaboong.github.io"가 새롭게 heap 영역에 생성되고, 기존에 "https://"를 레퍼런스 하고 있던 url 변수는 새롭게 생성된 문자열을 레퍼런스(참조)하게 되는것이다!!!!
==> 여기서 기존의 "https://"라는 문자열을 레퍼런스 하고 있는 변수는 아무것도 없으므로 Unreachable 오브젝트가 된다.
JVM의 Garbage Collector 는 Unreachable Object를 우선적으로 메모리에서 제거하여 메모리 공간을 확보한다.
Unreachable Object란 Stack에서 도달할 수 없는 Heap 영역의 객체를 말하는데, 위에 있는 예제에서 "https://" 문자열과 같은 경우가 되겠다.(불변객체에서의 연산작업을 할 때 남은 껍데기라고 생각하자.)
즉, Garbage Collection이 일어나면 Unreachable 오브젝트들은 메모리에서 제거된다.
Garbage Collection 과정은 Mark and Sweep 이라고도 한다.
JVM의 Garbage Collector가 스택의 모든 변수를 스캔하면서 각각 어떤 오브젝트를 레퍼런스 하고 있는지 찾는 과정이 Mark 이다.
그리고 나서 mark 되어있지 않은 모든 오브젝트들을 힙에서 제거하는 과정이 Sweep 이다.
Garbage Collection 이라고 하면 garbage들을 수집할 것 같지만,
실제로는 gabage를 수집하여 제거하는것이 아니라, gabage가 아닌 것을 따로 mark 하고 그 외의 것은 모두 지우는 것이다.
위의 예제(불변객체에서의 연산작업)에서 Garbage Collection 이 일어난 후의 메모리 상태는 아래 사진과 같다.
■ 2-3. 위에서 배운 stack 영역과 heap영역을 생각하고 참조자료형의 값 비교를 해보자.
저번 시간에는 메모리 영역(Stack, Heap)을 생각하지 않고 값 비교를 하였었다.
다시한번 메모리 영역(Stack, Heap)을 생각을 하고 이 문제를 이해해보자.
[EX] - 메모리 관점에서의 참조자료형의 "==", equals() 비교
String text1= "Java Basic";
String text2= "Java Basic";
String text3= new String("Java Basic");
String text4= new String("Java Basic");
MemberDTO obj1 = new MemberDTO("Sangmin");
MemberDTO obj2 = new MemberDTO("Sangmin");
System.out.println(text1==text2);
System.out.println(text1==text3);
System.out.println(text3==text4);
System.out.println(text1.equals(text2));
System.out.println(text1.equals(text3));
System.out.println(text3.equals(text4));
System.out.println(obj1.equals(obj2));
true
false
false
true
true
true
false
위에 있는 코드들의 살펴보자.
- String text1 = "Java Basic"; 의 구문을 통해 Heap영역내의 String constant pool 부분에 "Java Basic"값을 저정한다.
Stack영역에서는 text1의 레퍼런스 변수가 String constant pool 부분에 "Java Basic"의 값을 가르킨다. - String text2 = "Java Basic"; 의 구문을 통해 Heap영역내의 String constant pool 부분에 이미 "Java Basic"의 공간이 할당되어 있다. String constant pool영역에서 동일한 값("Java Basic")이 있기때문에 stack영역의 text2는 같은 위치에 있는 String constant pool영역의 "Java Basic" 객체를 참조한다.
- String text3 = new String("Java Basic"); 의 구문을 통해 new라는 예약어에 의해 heap 영역에 충분한 공간이 있는지 확인한 후 새로운 "Java Basic"이라는 공간을 할당한다. Stack영역에서는 text3 이라는 레퍼런스 변수(객체를 가르키는 변수)로 할당되며, 혼자 Heap영역에 있는 "Java Basic"의 값을 가르킨다.
- String text4 = new String("Java Basic"); 의 구문을 통해 new라는 예약어에 의해 heap 영역에 충분한 공간이 있는지 확인한 후 새로운 "Java Basic"이라는 공간을 할당한다. Stack영역에서는 text4 이라는 레퍼런스 변수(객체를 가르키는 변수)로 할당되며, 혼자 Heap영역에 있는 "Java Basic"의 값을 가르킨다.
- MemberDTO obj1 = new MemerDTO("Sangmin"); 의 구문을 통해 new라는 예약어에 의해 heap 영역에 충분한 공간이 있는지 확인한 후 MemberDTO의 생성자를 이용해 "Sangmin" 이라는 공간을 할당한다. Stack영역에서는 obj1 이라는 레퍼런스 변수(객체를 가르키는 변수)로 할당되며, 혼자 Heap영역에 있는 "Sangmin"의 값을 가르킨다.
- MemberDTO obj2 = new MemerDTO("Sangmin"); 의 구문을 통해 new라는 예약어에 의해 heap 영역에 충분한 공간이 있는지 확인한 후 MemberDTO의 생성자를 이용해 "Sangmin" 이라는 공간을 할당한다. Stack영역에서는 obj2 이라는 레퍼런스 변수(객체를 가르키는 변수)로 할당되며, 혼자 Heap영역에 있는 "Sangmin"의 값을 가르킨다.
위에서 말로 한 것을 그림으로 표현한것은 아래 그림과 같다.
- Stack 영역 : 후입 선출(LIFO, Last-In-First-Out)의 방식으로, 가장 나중에 들어온 데이터가 가장 먼저 인출된다.
- Heap 영역 : 선입선출(FIFO, First-In-First-Out)의 방식으로,가장 먼저 들어온 데이터가 가장 먼저 인출된다.
위에서 String 참조자료형 경우를 생각해보자.
- 1. "new"라는 예약어를 사용하지 않고 객체를 생성하는 경우[리터럴을 이용한 방식](String만 가능, String은 신기하다.)
String 객체를 생성할 때 "new" 예약어를 사용하지 않고 직접 문자열을 넣은 경우를 리터럴을 이용한 방식이라고 한다. 이 리터럴을 사용하게 되면 Heap영역내에 있는 String constant pool이라는 영역에 존재하게 되고, 예시로 String을 리터럴로 선언할 경우 내부적으로 String intern() 메소드가 호출되게 된다. 이 intern()메소드를 통해 주어진 문자열이 String constant pool에 존재하는지 검색하여 없다면 String constant pool에 값을 할당하고 새로운 주소값을 반환한다. 만약 string constatnt pool에 이미 동일한 문자열이 이미 존재한 상태에서 똑같은 값(문자열)을 또 생성한다면 같은 위치에 객체를 저장하게된다. 즉 stack영역에서 string constatnt pool의 같은 위치의 값을 참조하는 것이다.
정리하자면, String에서의 리터럴을 이용한 방식에서
"==" 연산자를 이용한 비교에서 String constant pool영역의 같은 주소에 있는 문자열의 값을 비교하는 것이고,
equals() 메소드를 이용한 비교에서는 equals()를 오버라이딩하여 비교하고자 하는 두개의 대상의 값 자체를 비교한다는 것이다. - 2. "new"라는 예약어를 사용하고 객체를 생성하는 경우(String을 제외한 모든 객체는 이 방법으로 생성한다.)
"new" 예약어를 통해 객체를 생성하면 아무리 같은 문자열(값)이라도 heap 영역에서는 새로운 공간이 할당되게 된다.
==> 그렇기에 객체끼리 "==",equals() 비교를 하게 되면 레퍼런스 변수(객체를 가르키는 변수)가 누구를 가르키냐(참조하는 것)가 동일하는 것을 비교하기 때문에 비교를 하면 항상 false 가 나온다.
[EX] - String literal 방식과 new 예약어 차이
String str1 ="One"; //String literal을 사용하는 방법
String str2 ="One";
String str3 = new String("Two"); // new 연산자를 사용하는 방법
String str4 = new String("Two");
String str5 = new String("One");
위 코드는 String을 리터럴 방식과 new 예약어 방식으로 만들었다.
- 리터럴 방식에 대한 "=="연산자 비교와 equals() 메소드 비교를 설명을 한다면,
str1, st2처럼 string constatnt pool에 같은 값이 존재한다면, 스택 영역의 레퍼런스변수는 다른 공간을 할당할 지 언정 string constatnt pool에서 같은 값을 가르키기 때문에 "==" 연산자 비교에서 같은 값을 가르키고, equals()비교에서는 비교하고자 하는 두개의 대상의 값 자체가 같기 때문에 같다고 말할 것이다. - new 예약어 방식에 대한 "=="연산자 비교와 equals() 메소드 비교를 설명을 한다면,
new 예약어를 통해 객체를 생성하면 아무리 같은 문자열(값)[위의 코드에서는 값이 다르긴 함]이라도 heap 영역에서는 새로운 공간이 할당되게 된다.
그렇기에 객체끼리 "==",equals() 비교를 하게 되면 레퍼런스 변수(객체를 가르키는 변수)가 누구를 가르키냐(참조하는 것)가 동일하는 것을 비교하기 때문에 비교를 하면 항상 false가 나온다.
■ 2-4. 메모리 관점에서의 passByReference()의 원리를 이해해보자.
[EX] - 참조자료형은 Pass by Reference
package Part8;
public class ReferencePass {
public static void main(String[] args) {
ReferencePass reference = new ReferencePass();
// reference.callPassByValue();
reference.callpassByreference();
}
public void callPassByValue() {
int a=10;
String b= "b";
System.out.println("before passByValue");
System.out.println("a="+a);
System.out.println("b="+b);
passByValue(a, b);
System.out.println("after passByValue");
System.out.println("a="+a);
System.out.println("b="+b);
}
public void passByValue(int a, String b) {
a=20;
b="z";
System.out.println("in passByValue");
System.out.println("a="+a);
System.out.println("b="+b);
}
public void callpassByreference() {
MemberDTO member = new MemberDTO("Sangmin");
System.out.println("before passByReference");
System.out.println("member.name="+member.name);
passByReference(member);
System.out.println("after passByReference");
System.out.println("member.name="+member.name);
}
public void passByReference(MemberDTO member) {
member.name ="SungChoon";
System.out.println("in passByReference");
System.out.println("member.name"+member.name);
}
}
모든 참조 자료형은 Pass by Reference이며, 매개변수로 참조 자료형을 넘길 경우에는 메소드 안에서 객체의 상태를 변경한 결과에 영향을 받는다.
메모리관점에서 Pass by Reference를 접근해보자,
1. callpassByreference()메소드에서 객체를 생성하여 Heap영역에 값(Sangmin)과 공간이 할당되는데 stack 영역에는 member 래퍼런스가 생성되고 Heap영역에 값(Sangmin)을 가르킨다(참조)
2. passByReference() 메소드가 시작이 되고 callpassByreference()에서 생성된 member 래퍼런스를 매개변수로 준다. 여기서 memeber 변수 인자 그대로 전해지게 되고 stack영역에 member라는 레퍼런스 변수가 새롭게 할당이 된다. 이 새롭게 stack영역에 member라는 래퍼런스 변수가 할당이 되었는데 이것이 heap 영역의 어떤 값을 가르키나면 매개변수로 물려받았던 callpassByreference()에서 생성된 member 래퍼런스가 가르키는 heap 영역의 값을 가르킨다. 즉, 동일한 객체를 참조하다는 말이다.
3. passByReference() 메소드내에서 member 래퍼런스가 가르키는 memberDTO 객체의 String name의 값을 "SungChoon"으로 변경한다.
4. passByreference() 메소드가 종료가 되고 passByReference() 메소드에 할당된 member 래퍼런스 변수는 pop이 되었고, 다시 callpassByreference() 메소드로 돌아오게 된다. 처음에 callpassByreference()에서 생성되었는 member 래퍼런스 변수에 접근이 가능해졌다. callpassByreference()에서 member 래퍼런스가 가르키는 객체의 값(String name)을 찍게 되면 "SungChoon"이 나오게 된다.
왜냐하면, PassByreference()에서의 member 래퍼런스변수와 callpassByreference()에서의 member 래퍼런스가 다른 stack 영역에 할당되었지만 참조하는 값이 같기 때문이다. 즉, Heap 영역에서 동일한 memberDTO객체를 가르키기 때문이다.
위에 있는 말을 글로 자세히 풀어쓴다면,
callpasByreference()메소드에서 처음에 new를 통해 MemberDTO클래스의 객체를 생성하면 heap영역에 MemberDTO 클래스의 객체의 값과 String name = "Sangmin"의 값과 공간이 할당되어 있고 stack영역에는 "member"로 레퍼런스 변수로 heap영역에 생성된 MemberDTO 클래스의 객체의 값과 String name = "Sangmin"의 값과 공간이 할당된 곳을 참조(가르킨다)한다.
그 후 passbyValue() 메소드를 실행시키는데 매개변수로 member 레퍼런스 변수를 넣어주었고, member변수에 있는 인자 그대로 stack영역에서의 passbyValue() member 레퍼런스 변수를 새로 할당한다.
하지만 새로 할당된 member래퍼런스 변수가 heap영역의 실제값을 가르키는것은 callpassByreference()메소드에서의 member 레퍼런스가 가르키는 것과 동일한다.
또한 스코프에 의해 stack 영역에서callpasByreference()메소드에서의 meber 래퍼런스 변수는 접근할 수 없게 된다.
그 후 callpassByreference()메소드에서의 member 레퍼런스가 가르키는 것과 동일한 passbyValue() member 레퍼런스 변수는 heap에 저장되어 있는 MeberDTO 클래스의 name을 바꾼다.( member.name = "SungChoon";)
그 후 passbyValue()가 종료되는데 passbyValue()의 member 레퍼런스가 pop 되서 사라지고 callpassByreference()메소드에서의 member 레퍼런스가 나타내게 되고 이것이 참조하는 heap영역의 값(name)을 찍으면 SungChoon이라고 찍힌다.
==> 왜냐하면 callpassByreference()메소드에서의 member 레퍼런스가 가르키는 것(참조하는것)과 passbyValue()메소드에서의 member 레퍼런스 변수가 가르키는 것(참조하는것)은 동일하기 때문에 passbyValue()메소드 내에서 참조하는 것이 바꼈다면 callpassByreference()메소드에서의 member 레퍼런스가 가르키는 것(참조하는것)도 바꼈기 때문이다.
■ 2-5. 메모리 관점에서의 참조자료형 할당 정리
여기서 String을 제외한 참조자료형에서 "==", eqauls() 비교는 주소값을 비교한다고 했다.
즉, Stack에 있는 레퍼런스 변수(객체를 가르키는 변수)가 heap영역에 있는 누구를 가르키냐(참조하는 것이 동일하냐)를 비교하는 것이다.
String을 제외한 참조자료형에서 "==", eqauls() 비교는 옳지 못한다고 생각한다.(그렇기에 오버라이딩하여 사용해야 한다.)
==> 왜냐하면 "new" 연산자를 통해 객체를 생성하면(객체를 생성하려면 new 예약어를 통해 생성해야하기때문) heap 영역에 충분한 공간이 있는지 확인한 후 새로운 공간이 할당되어야만 하고, stack영역에서는 그 새로운 공간이 할당된 것을 참조하는 레퍼런스 변수가 생긴다. 때문에 같은 값을 가진 객체를 "new"예약어를 통해 만든다면, 변수(객체를 가르키는 변수)가 누구를 가르키냐(참조하는 것이 동일하냐)를 비교하기 때문에 이 값을 비교하면 false로 나온다는 것이다.(새로운 값을 할당하여 heap에 새롭게 저장하기 때문에)
하지만 String같은 경우 "new" 예약어를 사용하지 않고 바로 값을 할당하고 만약 그 값이 같은 문자열이라면 heap영역에 그 문자열을 하나로 할당하고, Stack에서는 변수명은 다르지만 서로 같은 주소를 할당하는 레퍼런스 변수가 될 것이다.
또한 String에서의 equals()의 비교는 2개의 객체(stack영역에서)가 가리키는 곳(heap영역을)이 동일한 메모리 주소이냐를 비교하는것이 아닌 동일한 문자열(값)을 가지냐를 비교한다.(오버라이딩을 이용해서)
new를 통해 생성한 객체의 내용을 비교하기 위해선 equals()메소드를 오버라이딩하여 사용하면 좋을 것이다.
왜냐하면 new를 통해 생성한 객체의 equals() 메소드는 2개의 객체가 가리키는 곳이 동일한 메모리 주소일 경우에만 ture가 되는데, new 예약어를 통한 객체 생성은 아무리 같은 값을 가지는 객체라도 heap 영역에 새로운 공간을 할당하여 그 주소를 비교하기 때문이다.
또한 Heap 영역에는 있지만, Stack에서 도달할 수 없는 Heap 영역의 객체(Unreachable Object)는 JVM의 Garbage Collector를 이용하여 우선적으로 메모리에서 제거하여 메모리 공간을 확보한다.
'JAVA > 자바의 신 의문 해결' 카테고리의 다른 글
자바의 자료구조 정리 [ List, Set, Queue, Map ] (0) | 2022.09.19 |
---|---|
Garbage Colletion의 개념 및 동작원리 (1) | 2022.09.16 |
자바) JVM이 운영체제로부터 할당받는 대표적인 메모리 공간(RAM) [ Runtime Data Areas ] 및 구조 + C에서 운영체제로부터 할당받는 메모리 공간[ Code, Data, Stack, Heap ] (0) | 2022.09.14 |
기본자료형, 참조자료형(모든 클래스, String 클래스) "==", eqauls() 비교 의문 해결 (0) | 2022.09.11 |