[작성 중] Java 참조 타입(Reference Type)

박계현·2024년 12월 10일

참조 타입

메모리 사용 영역

자바의 데이터 타입은 크기 기본 타입primitive type과 참조 타입reference type으로 분류된다. (중략) 참조 타입이란 객체object의 번지를 참조하는 타입으로 배열, 열거, 클래스, 인터페이스 타입이 있다. <이것이 자바다>

객체는 힙 메모리 영역에, 객체의 참조 타입 변수는 스택 메모리 영역에 저장된다. String name = "계계"; 라고 코드를 썼다면, "계계"는 힙 메모리에, 해당 주소를 가리키는 name은 스택에 저장된다. JVM은 하나의 프로세스로서 운영체제로부터 할당받은 메모리 영역Runtime Data Area을 가상 머신으로서 여러 부분으로 나누어 사용하는데, <이것이 자바다> 책에 그림이 너무 잘 되어 있어서 공유해본다.

  • 메소드 영역
    메소드Method 영역은 읽어 온 바이트코드의 내용이 저장된다. 클래스별 상수(final), 정적(static) 필드, 메소드 코드, 생성자 코드 등이 저장된다.
    자바는 객체지향을 자칭하면서, 시작부터 static이라는 꼼수를 쓴다는 이야기를 어느 외국 유튜브 영상에서 본 기억이 난다.
    아무튼 컴퓨터가 일반 프로세스를 실행할 때 코드 영역에 해당한다.

  • 힙 영역
    힙Heap 영역은 객체가 생성되는 영역이다. 일반적으로 메모리에서 힙이라 하면 프로세스에서 동적으로 할당 및 해제되는 메모리 영역이다. 이는 사용자, 즉, 프로그래머와 그가 쓴 코드에 의해서 관리된다. 자바에서는 여기에 객체object를 저장한다. 또한, 사용자(프로그래머)가 할당과 해제에 관여하지 않는다. 이는 JVM의 가비지 컬렉터(Garbage Collector)가 담당한다. 객체는 참조되지 않으면 가비지 컬렉터가 해당 메모리를 해제한다.

  • 스택 영역
    스택Stack 영역은 메서드를 호출할 때마다 생성되는 프레임Frame이 저장되는 영역이라고 설명되어 있다. 메서드를 브라켓{}에 담고 해당 영역 내에서 한 번 선언한 변수는 같은 이름으로 다시 선언할 수 없음을 알 것이다. 이러한 변수들은 메서드의 프레임에 저장되며 함수가 반환(return)할 때 꺼내진다. 이 동작에 대해 더 자세히 알고 싶다면 프로그래밍언어론을 공부해보길 바란다. 단순하게 설명해보자면, 우리야 빨래 개다가 화장실 갔다와도 아까 개던 거 마저할 수 있지만, 컴퓨터가 보는 세상은 어둡다. 우리는 컴퓨터에게 '무언가'를 하기 위해 '어디'로 가라고 정확하게 이야기 해줘야 한다. 만약 볼일이 끝났다면 개던 빨래가 뭐였는지 컴퓨터가 어떻게 알 수 있을까? 하던 일을 하나씩 넣어둘 수 있는 길쭉한 통을 하나 준다. 컴퓨터는 해야할 일을 위에 하나씩 쌓아 올리며, 끝난 일은 꺼내서 버린다. 꺼내서 버리고 나면 전에 담아뒀던 것이 나타나므로 해당 일을 다시 할 수 있다. 그게 스택이라는 자료구조이며, 프로세스의 함수 호출 순서를 다루기 위해 스택이 선택되었다.

두 String 변수를 == 혹은 !=으로 비교하면?

==!= 연산자로 보통 두 변수의 값이 같은지, 아닌지 비교한다. 그래서 자바를 처음하는 사람들은 다음과 같은 실수를 가끔 한다.

private boolean isEqual(String a, String b) {
	return a == b;
}

안타깝게도 위 메서드는 아마 이걸 작성한 사람의 의도와는 다르게 동작한다. 위 a == b가 비교하는 것은 각 a와 b가 나타내는 문자열이 같은지가 아니다. String은 객체이기 때문에 객체를 String의 참조 변수인 ab는 각각 자신이 가리키는 String 객체의 힙 영역 주소를 가리키고 있다. ab 두 변수의 '값'을 비교는 == 혹은 != 연산자는 이 둘의 말 그대로 각자 저장된 주소값을 비교한다. 즉, ab가 같은 객체를 참조하고 있는지를 비교하게 되는 것이다. 두 String 객체의 값을 비교하기 위해서는 String 클래스에서(사실 Object 클래스에서부터 재정의되어) equals()가 제공된다.

private boolean isEqual(String a, String b) {
	return a.equals(b);
}

String의 equals()는 어떻게 이런 기능을 제공하는지 살짝 궁금하지 않은가?

	public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        return (anObject instanceof String aString)
                && (!COMPACT_STRINGS || this.coder == aString.coder)
                && StringLatin1.equals(value, aString.value);
    }

	// StringLatin1.java
	@IntrinsicCandidate
    public static boolean equals(byte[] value, byte[] other) {
        if (value.length == other.length) {
            for (int i = 0; i < value.length; i++) {
                if (value[i] != other[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

자바 17버전 기준 이렇게 생겼다. 나도 처음 열어봤는데 생각보다 다양한 변수가 고려되어 있어서 놀랐다.
일단 간단하게 말하자면 비교 대상이 되는 매개 변수가 같은 객체라면 곧바로 true를, 비교 대상이 String 객체이며, 둘 다 같은 방식으로 인코딩(UTF-16 등) 되어 있을 때, 길이가 같고, 문자열의 각 원소가 같으면 true를, 이 중 하나라도 아니면 false를 반환하게 되어있다. 그냥 흥미로워서 살펴본 것이니 넘어가자.

null과 NullPointerException

참조 타입 변수는 아직 번지를 저장하고 있지 않다는 뜻으로 null 값을 가질 수 있다. null도 초기값으로 사용할 수 있기 때문에 null로 초기화된 참조 변수는 스택 영역에 생성된다.

String refVar1 = "자바";
String refVar2 = null;

refVar1은 힙 영역의 "자바"의 주소를 가리키지만 refVar2는 아직 가리키는 주소가 없다. 대신 null을 값으로 가진다. null이 값이므로 해당 참조 타입 변수가 null 값을 가지는지 ==!= 연산을 통해 확인할 수 있다.

refVar1 != null
refVar2 == null

null 값을 가진 참조 타입 변수를 사용하려고 하면 NullPointerException이 발생한다. null은 말했다시피 값이므로, 이미 객체를 가리키고 있는 참조 변수에 다시 null 값을 대입할 수 있다. 그렇다면 기존에 해당 참조 변수가 참조하던 객체는 어떻게 될까?
어떤 참조 변수도 참조하지 않고 있는 객체를 쓰레기(Garbage)라고 부른다. 이러한 객체는 다시 접근할 수 있는 방법이 없다. JVM은 가비지 컬렉터(Garbage Collector)를 통해 이들을 자동으로 메모리에서 제거한다.

profile
안녕하세요! 차근차근 성장하는 소프트웨어 엔지니어 박계현입니다😊

0개의 댓글