Java Memory Management, Primitive Type And Reference Type, String Pool
출발점
자바 코드를 짜다가 스스로 설명이 되지 않는 부분이 있어서 찾아보고 정리했습니다
public class StringCompare {
public static void main(String[] args) {
String string1 = "Hello";
String string2 = "Hello";
System.out.println("string1.hashCode() = " + string1.hashCode()); // 69609650
System.out.println("string2.hashCode() = " + string2.hashCode()); // 69609650
System.out.println("string1 == string2 : " + (string1 == string2)); // true
System.out.println("string1.equals(string2) : " + (string1.equals(string2))); // true
}
}
String은 reference type이기 때문에 immutable이고 매번 다른 객체가 heap 구조에 생기는 걸로 알고 있었는데 같은 객체를 참조하고 있어 궁금했습니다
그래서 자바 메모리 구조와 Primitive and Reference Type까지 보게 되었고 String Pool 내용도 정리하게 되었습니다
DZone에 Constantin Marian 님께서 작성하신 글과
yaboong님의 글에서 많은 도움을 받았습니다
https://dzone.com/articles/java-memory-management
https://yaboong.github.io/java/2018/05/26/java-memory-management/
관련 내용들을 찾아보고 개인 정리를 위해 남기는 내용이라 비슷한 내용이 포함되어 있습니다
위에 글들 좋은 글이니 읽어보는 것을 추천드립니다
(Constantin Marian 님의 글에는 메모리에 대한 설명에 Garbage Collector 내용까지 자세하게 설명되어 있습니다)
(둘 다 읽어보신다면 야붕님의 글을 읽어보신 뒤에 Constantin Marian님의 글을 읽으면 좋다고 생각합니다 저는 반대로 찾아서 반대로 읽긴했습니다)
Memory Structure
일반적으로 메모리는 스택과 힙 크게 두 영역으로 나뉩니다. 힙 영역은 스택 영역과 비교했을 때 큽니다 (이미지가 정확히 비례하지는 않습니다)
Stack
스택
- 원시타입(primitive type)이 저장됩니다
- Thread 별로 각자의 stack이 있습니다
- 함수별로 다른 지역변수를 가지고 있으면 다른 visibility (also called scope)를 가져갑니다
- Heap 영역의 Object 참조값을 가집니다
[scope]
For example, assuming that we do not have any global scope variables (fields), and only local variables, if the compiler executes a method’s body, it can access only objects from the stack that are within the method’s body. It cannot access other local variables, as those are out of scope. Once the method completes and returns, the top of the stack pops out, and the active scope changes.
전역 변수가 없고 지역변수만 있고 컴파일러가 메소드를 실행했을 때, 해당 메소드는 자시느이 바디에 있는 스택의 객체만 접근할 수 있습니다. 다른 메소드의 지역 변수는 scope을 벗어난 것이기 때문에 접근할 수 없습니다. 메소드가 끝나고 반환(return)되면 스택에서 pop되고 활성 스콥(active scope)이 변경됩니다.
위의 그림에서 스택 영역에 네모 박스 그려진 부분이 scope 입니다.
Heap
힙
- 모든 Reference 타입 (String, Integer, Double, ArrayList,, )가 생성됩니다
- Reference 타입은 스택에 저장된 변수들에 참조 됩니다
- JVM 프로세스 별로 단 한개의 힙 메모리가 존재합니다
StringBuilder builder = new StringBuilder();
The `new` keyword is responsible for ensuring that there is enough free space on heap, creating an object of the StringBuilder type in memory and referring to it via the “builder” reference, which goes on the stack.
new 키워드는 힙 영역에 충분히 여유 공간이 있는 지 확인하고 StringBuilder 타입의 객체를 힙 영역에 만든 뒤에 스택 영역에 있는 'builder' 변수(reference)에 참조시킵니다.
스택과 힙 영역의 최대값은 정해져 있지 않고 기기에 따라 다릅니다. JVM 설정으로 스택과 힙 사이즈를 변경할 수 있습니다.
Reference Types
1. Strong Reference
가장 대표적이고 일반적으로 생각하는 참조 타입입니다
위에 StringBuilder 예시 코드처럼 힙 영역에 있는 객체를 스택 영역에 있는 변수가 참조하는 경우입니다
힙 영역에 있는 객체가 참조가 끊어지면 GC (garbage collector)가 수집합니다
2. Weak Reference
WeakReference<StringBuilder> reference = new WeakReference<>(new StringBuilder());
In simple terms, a weak reference to an object from the heap is most likely to not survive after the next garbage collection process.
힙 영역에 있는 약한 참조 객체는 다음 GC 과정에서 대부분 살아남지 못할 것입니다
왜 다음 GC에서 대부분 죽는 지 궁금해서 소스코드를 봤는데 이해가 안됩니다. 좀 더 리서치가 필요합니다. 어떤 use case가 있는 지 알아놨다가 다음에 필요할 때 그 때 더 찾아보고 적용하면 좋을 거라 생각합니다.
본문에 적혀있는 use case 입니다
A nice use case for weak references are caching scenarios. Imagine that you retrieve some data, and you want it to be stored in memory as well — the same data could be requested again. On the other hand, you are not sure when, or if, this data will be requested again. So you can keep a weak reference to it, and in case the garbage collector runs, it could be that it destroys your object on the heap. Therefore, after a while, if you want to retrieve the object you refer to, you might suddenly get back a `null` value.
A nice implementation for caching scenarios is the collection `WeakHashMap<K,V>`.
캐싱하는 경우가 좋은 시나리오라고 합니다. 캐싱을 했지만 같은 요청이 언제 들어올지 그리고 같은 데이터에 대한 요청이 들어오기나 할지 잘 모르겠을 때 Weak Reference를 걸어놓고 다음 GC가 실행될 시간 정도만 캐싱을 하고 있는 경우입니다. 캐싱의 경우 자바에 이미 구현되어 있는 WeakHashMap을 쓰면 됩니다.
3. Soft Reference
These types of references are used for more memory-sensitive scenarios, since those are going to be garbage collected only when your application is running low on memory. Therefore, as long as there is no critical need to free up some space, the garbage collector will not touch softly reachable objects. Java guarantees that all soft referenced objects are cleaned up before it throws an `OutOfMemoryError`.
Soft Reference는 힙 영역의 메모리가 충분하면 GC가 soft referenced로 참조되고 있는 objects를 수거하지 않습니다. 자바는 `OutOfMemoryError`가 뜨기 전에 soft referenced objects를 지우는 것을 보장합니다.
4. Phantom Reference
Used to schedule post-mortem cleanup actions, since we know for sure that objects are no longer alive. Used only with a reference queue, since the `.get()` method of such references will always return `null`. These types of references are considered preferable to finalizers.
힙 영역에 참조 대상이 더이상 존재하지 않는 것을 알아서 사후에 정리하는 과정에 사용되는 참조입니다. 조금 다른 맥락으로 java Optional을 생각해서 읽었습니다. Optional에서 .isPresent()를 호출후에 .get()을 호출하는 것처럼 참조하지 않고 있는 것을 확인하고 정리하기 위한 참조입니다.
String 은 어떻게 참조되는 지 (String Pool)
자바에서 String은 조금 다르게 처리됩니다. String은 immutable이라 선언할 때마다 새로운 객체가 힙 영역에 생성될 것처럼 생각이 되지만 그렇지 않습니다. 자바에서는 메모리에 `string pool`을 저장하고 있습니다. 그래서 자바는 같은 문장이라면 재사용 합니다.
string pool은 힙 영역 내에 존재하는 메모리 입니다.
public class StringCompare {
public static void main(String[] args) {
String string1 = "Hello";
String string2 = "Hello";
// 같은 hash code를 가지고 있습니다
System.out.println("string1.hashCode() = " + string1.hashCode()); // 69609650
System.out.println("string2.hashCode() = " + string2.hashCode()); // 69609650
// 같은 객체입니다
System.out.println("string1 == string2 : " + (string1 == string2)); // true
// 같은 내용입니다
System.out.println("string1.equals(string2) : " + (string1.equals(string2))); // true
}
}
"Hello" 가 `string pool` 영역에 저장이 되고 string1과 string2가 같은 "Hello"를 참조하고 있습니다.
== 비교에서도 당연히 true가 나옵니다.
내용도 같으니 .equals()에서도 true가 나옵니다.
public class StringCompare {
public static void main(String[] args) {
String string1 = "Hello";
String string3 = new String("Hello");
// 같은 hash code가 나옵니다
System.out.println("string1.hashCode() = " + string1.hashCode()); // 69609650
System.out.println("string3.hashCode() = " + string3.hashCode()); // 69609650
// 다른 객체입니다
System.out.println("string1 == string3 : " + (string1 == string3)); // false
// 같은 내용입니다
System.out.println("string1.equals(string3) : " + (string1.equals(string3))); // true
}
}
new String()으로 새롭게 생성한 객체는 힙 영역에 새롭게 생성되고 다른 객체를 참조하고 있는 것을 알 수 있습니다. 만약에 자주 사용하는 문자열이라고 하면 .internal() 메소드로 JVM에게 string pool에 추가하라고 하면 됩니다.
public class StringCompare {
public static void main(String[] args) {
String string1 = "Hello";
String string3 = new String("Hello").intern();
// 같은 hash code가 나옵니다
System.out.println("string1.hashCode() = " + string1.hashCode()); // 69609650
System.out.println("string3.hashCode() = " + string3.hashCode()); // 69609650
// 같은 객체 입니다
System.out.println("string1 == string3 : " + (string1 == string3)); // true
// 같은 내용입니다
System.out.println("string1.equals(string3) : " + (string1.equals(string3))); // true
}
}
같은 객체가 나오는 것을 확인할 수 있습니다.
공부하면서 느낀 것
Reference를 더 잘 이해하기 위해서는 GC(Garbage Collector)를 잘 이해하는 것이 중요하다고 생각했습니다.
Naver D2에 GC을 설명한 글(https://d2.naver.com/helloworld/1329)과 Constantin Marian님의 글(https://dzone.com/articles/java-memory-management) 참고하면서 많은 내용을 리서치 해봐야겠다고 생각했습니다.
## reference
1. https://dzone.com/articles/java-memory-management
2. https://yaboong.github.io/java/2018/05/26/java-memory-management/