728x90

 

 다음 세 가지로 나눠서 JVM에게 메모리를 어떻게 할당되는지 살펴 볼 것이다.

 

  • 클래스 변수
  • 인스턴스 변수
  • 지역 변수

 

 

다음 코드를 예시로 그림으로 가시화 하여 실제로 메모리가 어떻게 올라가는지 살펴보겠습니다.

 

class My_Obj {
	Integer value;
	static Integer classVar;
}

public class Main {
	public static void main(String[] args) {
		My_Obj obj = new My_Obj();
		obj.value = 2;
		obj.classVar = 1;

		int a = 10;

		My_Obj obj2 = new My_Obj();
		obj2.value = 10;
		obj2.classVar = 9;
	}
}

 

 

들어가기 앞서 코드 설명을 하자면,

 

  • obj 객체를 생성한다.
  • 이 객체의 맴버 변수인 value, classVar 에 각각 2, 1를 할당한다.
  • obj 객체의 맴버 변수를 출력한다. 그 다음에, obj2 객체를 생성한다.
  • 그리고 이 객체의 맴버 변수인 value, classVar 에 각각 10, 11을 할당한다. 그 다음 obj2 객체의 맴버 변수를 출력한다.

 

그림으로 어떻게 변수들이 할당되는지 살펴보자.

 

 

크게 3가지 영역으로 나누어본다. Method, Heap, Stack 영역 으로 나뉜다. 이 중에서 클래스 변수는 클래스 파일을 읽어들이고 로더가 JVM 메모리에 올릴 때 메모리에 올라간다. 그리고 프로그램이 종료될 때 사라진다. 힙에 있는 인스턴스들은, GC의 대상이 될 때 처리가 된다. 마지막으로 지역 변수는 자신이 선언된 블록이 끝났을 때 사라진다.

 

 

처음 obj 객체의 출력 까지만 그림으로 표현해보았다. Heap 영역에는 GC가 있기 때문에 대상이 되는 인스턴스들과 Constant Pool 안에 있는 것들은 사용되지 않으면 정리가 된다. 변수에 할당된 모든 리터럴 값들은 Constant Pool 에 있고 Method 영역이나 Stack 영역에서 각 객체들은 Constant Pool 안의 해당 값의 참조 주소를 갖고 있다.

그 다음 obj2 객체가 생성이 되면 이렇게 된다.

 

classVar 클래스 변수가 갱신이 되면서 Constant Pool 내에 리터럴 값 9의 참조 주소를 참조하는 것으로 갱신된 것을 볼 수 있다.

 

그러면 My_Obj 객체의 value 필드의 타입을 참조타입에서 원시타입으로 변경 해서 살펴보자.

class My_Obj {
	int value; << 원시 타입으로 변경
}

public class Main {
	public static void main(String[] args) {
		My_Obj obj = new My_Obj();
		obj.value = 2;

		int a = 11;  << 새로 추가

		My_Obj obj2 = new My_Obj();
		obj2.value = 10;
	}
}

 

내부 메모리상으로는 다음과 같을 것이다.

 

 

여기서 알아가야 할게 Integer는 내부적으로 캐시가 된다는 점이다.

 

Integer 객체는 범위 -128에서 127 사이의 값일 경우 Constant Pool에서 캐싱된 값을 사용하겠지만, 그렇지 않다면 새로운 객체가 힙에 생성됨. 증거 자료는 다음과 같다. (Integer 래퍼 클래스 내부에 정적 프라이빗 클래스인 IntegerCachevalueOf 메소드이다.)

 

 

 

그러므로 예를 들어,

Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;

System.out.println(i1 == i2);  // true (캐싱된 객체를 재사용)
System.out.println(i3 == i4);  // false (새로운 객체가 각각 생성됨)

 

 

  • i1 == i2는 true를 반환함. 왜냐하면 100은 -128 ~ 127 사이에 있으므로 캐싱된 같은 객체를 사용하기 때문.
  • 반면, i3 == i4는 false를 반환함. 왜냐하면 200은 캐싱 범위 밖이므로 새로운 객체가 각각 생성되었기 때문.

 

 

그러면, 이점을 응용을 해보자. 다음 코드를 보자.

 

class My_Obj {
	int value;
}

public class Main {
	public static void main(String[] args) {
		My_Obj obj = new My_Obj();
		obj.value = 10;
        
        int a = 10;
        
        System.out.println(obj.value == a) // true 가 나온다.
	}
}

 

 

이런 그림이 될 것 이다.

 

결론

메모리 구조를 한번 더 정리하는 계기가 되었다.

 

 

 

👉🏻 참조

https://codingdog.tistory.com/entry/java-클래스-변수-어떻게-메모리에-올라갈까

'언어 > 자바' 카테고리의 다른 글

List<?> 와 List 의 차이  (0) 2023.05.03
[JAVA] List<?> 와 List<Object>의 차이  (2) 2022.09.21
728x90

<?> 는 와일드카드 타입이다. 즉 모든 타입을 의미한다. 주로 타입 인자를 알지 못할 때 쓰인다.

그러면 그냥 List와 List<?>의 차이는 무엇일까?

List<?> 를 매개변수로 사용하는 메서드는 List의 원소를 읽는 것은 가능하지만, 쓰는 것은 불가능하다.

 

읽기

List, List<?> 둘 다 파라메터로 받아 와서 읽는 것은 가능하다.

 

 

쓰기

코드상에서 바로 에러를 잡을 수 있다.(컴파일 전 타입체크)

List<?>는 write를 하지 못한다는 것을 알 수 있다.

728x90

의심할바 없이 제네릭은 자바에서 헷갈리는 것중 하나입니다. 그리고 매일 자바로 프로그래밍을 하지 않는다면 쉽게 잊어버릴 수 있는 개념이기도 합니다. 예를 들어 List<?> 와 List<Object>는 비슷해 보입니다. 그러나 약간 다릅니다. List<?>는 기본적으로 어떤 타입(any type)의 List 입니다. List<String>, List<Integer> 등을 할당할 수 있습니다.

기본적으로 List<?>는 밑에와 같이 할당할 때 까지는 모릅니다.

List<?> list = new List<String>();

 

그러면 List<Object>로 돌아와봅시다. 이는 List<String>과 별다른 차이가 없습니다. String 대신에 Object를 받아들일 뿐입니다. 모든 클래스는 Object를 상속을 받고 있습니다. 그러면 본질적으로 Object는 어떤 객체든 담을 수 있다는 얘기입니다.

그런데 자세히 읽어보시면 List<?>는 어떤 List 타입을 할당받을 수 있고, List<Object>는 어떤 Object 타입을 저장할 수 있습니다. 이것이 진짜 List<?>와 List<Object>의 차이입니다.

예시를 한번 봐야겠지요?

 

제가 List<?>는 어떤 타입(any type)의 List를 할당할 수 있다고 했지만 List<Object>에는 그렇게 할 수 없습니다. 이와 같이 String, Integer, Double 등 List<Object>에 넣을 수 있는 것들을 다 넣을 수 있습니다. 그러나 List<?>에는 불가능합니다. 왜냐하면 <?>은 타입이 unknown이기 때문입니다.

import java.util.ArrayList;
import java.util.List;

public class Demo {

public static void main(String[] args) {

        printElements(new ArrayList<String>()); // OK
        printElements(new ArrayList<Integer>()); // OK

        printObjects(new ArrayList<String>());
        // NOT OK compile time error

        printObjects(new ArrayList<Integer>());
        // NOT OK compile time error
    }

public static void printElements(List<?> listOfUnknownType) {
        listOfUnknownType.add("abc"); // compile time error

for (Object o : listOfUnknownType) {
            System.out.println(o); // OK
        }
    }

public static void printObjects(List<Object> listOfObjects) {
        listOfObjects.add("abc"); // OK
        listOfObjects.add(101); // OK

for (Object o : listOfObjects) {
            System.out.println(o); // OK
        }
    }
}

위 코드를 보시면 ArrayList<String>와 ArrayList<Integer>는 printElements() 함수를 통과를 하지만 printObjects() 함수는 통과하지 못하는 것을 보실 수 있습니다. 왜냐하면 String과 Integer은 Object와 다른 타입이기 때문입니다. 부모-자식 상으로는 같지만, 제네릭은 불변 타입이기 때문에 상속관계를 무시하기 때문에 다르다고 인식이 됩니다. 첫번째 함수인 printElements()를 통과하는 것이 가능했던 이유는 어떤 타입을 받을 수 있는 List<?>이었기 때문입니다.

 

결국에는 printObjects(List<Object> listOfObject) 는 에러를 던지게 되는거죠 왜냐하면 Object 타입만 받을 수 있기 때문입니다.

만약에 단순 읽는 작업을 하면 List<?>와 List<Object>의 기능은 같습니다. 그런데 objects 를 추가할때는 List<?>는 에러가 납니다. 왜냐하면 unknown 이기 때문입니다.

 

List<?>의 경우 컴파일러는 List의 타입이 유효한지 모릅니다. 그래서 타입안정성을 보장할 수 없기 때문에 에러를 던집니다. 그런데 List<Object>의 경우에는 컴파일러가 List의 타입이 Object인것을 정확히 알기 때문에 String, Integer, Double 등을 할당할 수 있습니다.

 

그래서 List<?>와 List<Object>의 차이는 코더가 무엇을 하느냐에 달려있습니다. 만약 객체를 읽을때 (List에서 객체를 출력할때)  List<?>, List<Object> 둘다 사용해도 좋습니다. 그런데 요소를 추가할때는 List<Object>를 사용해야합니다. 만약 제네릭 함수를 작성중일 때 어떠한 타입의 list를 함수의 매개 변수로 받고싶을때는 List<?>를 사용합니다.

 

 

https://okky.kr/articles/354841

+ Recent posts