본문 바로가기
학습/Java

[java] Stack & Heap과 Reference의 개념

by KKambi 2020. 6. 28.

YABOONG 님의 자바 메모리 - 스택 & 힙 포스팅과 DZone의 Java Memory Mangement 포스팅을

학습한 내용을 정리한 글입니다.

 

 

 

자바 메모리 관리


자바는 백그라운드에서 garbage collector로 사용되지 않는 객체를 청소하며 메모리 관리를 자동으로 해준다.

그렇지만 언제나 완벽할 수는 없어서, 사용하지 않는 객체가 청소되지 않을 수도 있다.

 

따라서 최적화를 위해서, 메모리 누수를 해결하기 위해서 자바 메모리가 어떻게 사용되는지 알아야만 한다.

 

Memory Structure

자바의 대략적인 메모리 구조는 그림과 같다.

크게 두 부분의 파트 : stack & heap으로 나누어져 있으며, 실제로 heap의 크기가 stack보다 훨씬 커지게 된다.

 

1. 값 자체를 가지며 stack memory을 차지하는 primitive type 변수

2. heap에 생성된 객체의 참조를 가지며 stack memory을 차지하는 reference type 변수

3. heap에 생성된 객체

 

 

 

Stack


힙 객체의 참조를 갖고 있는 참조타입변수 & 값 자체를 갖고 있는 원시타입변수를 저장하는 공간이다.

스택 내 변수들은 scope에 따른 visibility를 가진다. 현재 활성화된 스코프에 일치하는 변수만 사용될 수 있는 것이다.

 

예컨대 지역 변수가 특정 메소드 내에서 스택에 할당된 경우 해당 지역 변수는 다른 메소드에서 접근할 수 없다.

메소드 실행이 끝나면, 해당 메소드에서 선언된 모든 지역 변수들은 stack에서 pop되어 사라진다.

그리고 scope가 바뀐다.

 

참고로 stack에는 지역 변수뿐만 아니라, 함수 호출 시 전달되는 인자(call by value)와 되돌아갈 주소값도 쌓이게 된다.

 

위의 그림에서 stack Memory가 여러 겹인 걸 볼 수 있다.

이는 stack memory가 쓰레드마다 할당되기 때문이다.

쓰레드가 생성되고 시작될 때마다 고유한 스택 메모리를 가지며, 다른 쓰레드 스택에 접근할 수 없다.

쓰레드끼리 프로세스의 Code / Data / Heap을 공유하지만, Stack은 독립적으로 할당된다는 특징이 바로 이것이다.

 

cf) 프로그램이 OS로부터 할당받는 메모리 공간의 분류

Code: 실행하는 프로그램의 코드

Data: 전역 변수 및 정적 변수

Heap: 사용자의 동적 할당 (객체 생성) (런타임 시 크기 변화)

Stack: 지역 변수 및 인자 (컴파일 타임 시 크기 결정)

 

 

 

Heap


객체들은 힙 공간을 차지한다. 그러면서 스택에 있는 변수들에 의해 참조된다.

 

StringBuilder 객체를 살펴보자.

StringBuilder builder = new StringBuilder();

new 키워드는 힙에 충분한 공간이 있을 때, 해당 타입의 객체를 생성하고 주소를 반환하는 키워드이다.

반환된 주소는 스택에 존재하는, StringBuilder 참조 타입의 builder 변수에 담기게 된다.

 

하나의 실행중인 JVM 프로세스에는 단 하나의 힙 메모리만 존재한다.

몇 개의 쓰레드들이 실행 중이던 그들은 힙 메모리를 공유한다.

그리고 실제의 힙 메모리는 가비지 컬렉션 단계를 위해 그림보다 더욱 세분화되어있는데, 이는 다음 포스팅에서 살펴보려 한다.

 

스택과 힙 사이즈는 미리 결정되진 않지만, JVM 설정을 통해 명시적으로 설정할 수 있다.

 

 

 

Reference Types (참조 타입)


Memory Structure 그림을 잘 살펴보면, 스택의 참조 변수가 힙의 객체를 참조하고 있는 타입이 다른 것을 알 수 있다.

왜냐하면 힙 메모리의 객체를 향한 참조에는 4가지 타입이 있기 때문이다. (Strong / Weak / Soft / Phantom)

그리고 각 타입에 따라 가비지 컬렉팅되는 기준이 다르다.

 

 

 

1. Strong Reference

가장 많이 사용되는 참조 타입.

위에서 예시로 사용한 builder 변수는 StringBuilder 객체에 대해 강한 참조를 갖는다.

힙 메모리의 객체는 직접적으로 Strong Reference에 의해 참조되거나, Strong Reference Chain에 의해 도달 가능한 동안 가비지 컬렉터에 의해 수거되지 않는다. 즉, GC에서 무조건 제외된다.

따라서 메모리 누수를 방지하기 위해선 강하게 참조되는 객체들을 주의깊게 살펴야 한다.

 

 

 

2. Weak Reference

약하게 참조되는 객체는 GC가 발생하기 전까지 참조가 유지되고, GC가 발생하면 회수된다.

즉 짧은 시간 동안(GC가 수행되기 전까지)만 참조가 필요할 때 사용한다.

 

약한 참조는 WeakReference<T> Wrapper Class를 통해 만들 수 있다.

WeakReference<StringBuilder> reference 
				= new WeakReference<>(new StringBuilder());
                
StringBuilder builder = reference.get();

...

builder = null;

 

약한 참조는 캐싱에서 주로 사용된다.

특정 데이터를 반환해야 하고, 계속해서 요청되는 동일한 정보를 메모리에 보관하고 싶은 상황이다.

그러나 언제 다시 요청될 지 정확히 알 수는 없다.

 

위의 코드를 보면 StringBuilder 객체에 대한 약한 참조(reference 변수) + 강한 참조(builder 변수)가 존재한다.

하지만 builder에 null을 할당하는 순간, 해당 객체에 약한 참조만 존재하게 된다.

이 경우 GC가 발생하면 해당 객체는 회수되며, 그 객체를 다시 반환하려 할 때 null value를 만나게 된다.

 

Weak Reference #1
Weak Refence #2 약한 참조만 존재하는 상태. 다음 GC에서 회수된다.

이를 위해 WeakHashMap<K, V> 컬렉션을 활용하면 좋다.

WeakHashMap 클래스의 명세서를 살펴보면 이는 WeakReference 클래스를 확장(extend)하며 Key에 대해 약한 참조를 가지게 된다.

Key에 대한 약한 참조가 끊기게 되면 (key가 GC에 의해 회수되면), Map의 해당 entry는 삭제된다.

 

 

 

3. Soft Reference

메모리에 민감한 상황에서 주로 사용한다.

왜냐하면 실행 중인 어플리케이션의 메모리가 적을 때만 해당 참조를 갖는 객체가 회수되기 때문이다.

즉, 여유 메모리 공간을 늘리고자 하는 필수적인 요구가 없는 한, GC는 softly reachable objects를 건드리지 않는다.

자바는 OutOfMemoryError가 발생하기 전에 soft referenced objects가 회수됨을 보장하는 것이다.

 

Weak Reference와 유사하게 Soft Reference는 다음과 같이 만들어진다.

SoftReference<StringBuilder> reference 
				= new SoftReference<>(new StringBuilder());

 

 

 

4. Phantom Reference

잘 사용하진 않는다.

객체가 더 이상 살아있지 않을 때, 사후 청소를 스케줄링하기 위한 용도로 쓰인다.

팬텀 레퍼런스의 get() 메소드는 항상 null을 반환하기 때문에 Reference Queue의 사용이 필수적이다.

finalize()를 대체하기 때문에 finalizers라고 불리기도 한다.

 

 

 

String이 참조되는 방식


자바의 String은 특별하게 다뤄진다.

Immutable Object(불변 객체)이므로 String에 어떤 작업을 할 때마다 Heap에 새로운 객체를 생성한다.

그리고 이를 위해 String Pool을 운영한다. 즉, 기존의 String 객체를 저장하고 재사용하기도 한다는 것이다.

특히 new 키워드를 사용하지 않는 String Literal에 주로 사용된다.

아래 코드에서 localPrefix 변수와 prefix 변수는 "297"이라는 값을 갖고 있는 동일한 String 객체를 참조하고 있다.

String localPrefix = "297";
String prefix = "297";

if (prefix == localPrefix) {
    System.out.println("Strings are equal" );
} else {
    System.out.println("Strings are different");
}

 

 

하지만 계산된 String 객체(computed String)는 새로운 객체를 만들어낸다.

이 경우 두 변수가 참조하고 있는 객체는 서로 다르다.

String localPrefix = new Integer(297).toString();

 

 

우리는 .intern() 메소드를 사용해서 JVM에게 해당 객체를 String Pool에 추가시키라고 강제할 수 있다.

String localPrefix = new Integer(297).toString().intern();

댓글