Java - 메모리관리 ( 스택& 힙)
출처 ->
https://yaboong.github.io/java/2018/05/26/java-memory-management/
Java 의 Stack 과 Heap
Java 메모리 영역중 stack 과 heap 영역이 내가 짠 코드에서는 어떻게 작동하는지,
실제 어떤 데이터들이 garbage 로 분류되는지에 대해서는 몰랐습니다.
Stack 과 heap 영역의 사용에 초점을 맞춰서 정리해보겠습니다.
Stack
- Heap 영역에 생성된 Object 타입(Reference Type)의 데이터의 참조값이 할당된다.
- 원시타입의 데이터가 값과 함께 할당된다.
- 지역변수들은 scope 에 따른 visibility 를 가진다.
- 각 Thread 는 자신만의 stack 을 가진다.
Stack 에는 heap 영역에 생성된 Object 타입의 데이터들에 대한 참조를 위한 값들이 할당됩니다.
또한, 원시타입(primitive types) - byte, short, int, long, double, float, boolean, char 타입의 데이터들이 할당됩니다.
Stack 메모리는 Thread 하나당 하나씩 할당됩니다.
즉, 스레드 하나가 새롭게 생성되는 순간 해당 스레드를 위한 stack 도 함께 생성되며,
각 스레드에서 다른 스레드의 stack 영역에는 접근할 수 없습니다.
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 변수를 함수에 넘겨주고 결과값을 또다시 argument 에 할당하였습니다.
int argument =4 ;
에 의해 스택에 argument 라는 변수명으로 공간이 할당되고, argument 변수의 타입은 기본타입이므로
이 공간에는 실제 4 라는 값이 할당됩니다. 현재 스택의 상태는 아래와 같습니다.
argument = someOperation(argument);
위 의 코드로 인하여 , someOperation() 함수가 호출됩니다.
호출될때 인자로 argument 변수를 넘겨주며 scope 가 someOperation() 함수로 이동합니다.
scope 가 바뀌면서 기존의 argument 라는 값은 scope 에서 벗어나므로 사용할 수 없습니다.
이때 인자로 넘겨받은 값은 파라미터인 param 에 복사되어 전달되는데, param 또한 원시타입이므로 stack 에
할당된 공간에 값이 할당된다. 현재스택의 상태는 아래와 같게 됩니다 .
***scope
Stack 영역에 있는 변수들은 visibility 를 가집니다.변수 scope 에 대한 개념으로,
전역변수가 아닌 지역변수가 foo() 라는 함수내에서 Stack 에 할당 된 경우, 해당 지역변수는 다른 함수에서 접근할 수 없다.
예를들어, foo() 라는 함수에서 bar() 함수를 호출하고 bar() 함수의 종료되는 중괄호 } 가 실행된 경우 (함수가 종료된 경우), bar() 함수 내부에서 선언한 모든 지역변수들은 stack 에서 pop 되어 사라집니다.
그다음
int tmp = param * 3;
int result = tmp / 2;
에 의해 같은 방식으로 스택에 값이 할당되며 현재 스택의 상태는 아래와 같게 됩니다.
다음으로, 닫는괄호 } 가 실행되어 someOperation() 함수호출이 종료되면 호출함수 scope 에서 사용되었던 모든 지역변수들은
stack 에서 pop 됩니다.
함수가 종료되어 지역변수들이 모두 pop 되고, 함수를 호출했던 시점으로 돌아가면 스택의 상태는 아래와 같이 변하게됩니다.
rgument 변수는 4 로 초기화 되었지만, 함수의 실행결과인 6 이 기존 argument 변수에 재할당됩니다.
물론 함수호출에서 사용되었던 지역변수들이 모두 pop 되기 전에 재할당 작업이 일어날 것입니다.
그리고 main() 함수도 종료되는 순간 stack 에 있는 모든 데이터들은 pop 되면서 프로그램이 종료되게됩니다.
Heap
이제 heap 영역에 대해서 알아보게되면,
- Heap 영역에는 주로 긴 생명주기를 가지는 데이터들이 저장됩니다 (대부분의 오브젝트는 크기가 크고, 서로 다른 코드블럭에서 공유되는 경우가 많습니다) ,주로 인스터스
- 애플리케이션의 모든 메모리 중 stack 에 있는 데이터를 제외한 부분이라고 보면 됩니다
- 모든 Object 타입(Integer, String, ArrayList, ...)은 heap 영역에 생성됩니다(Referece 타입의 객체)
- 몇개의 스레드가 존재하든 상관없이 단 하나의 heap 영역만 존재됩니다
- Heap 영역에 있는 오브젝트들을 가리키는 레퍼런스 변수가 stack 에 올라가게 됩니다
간단한 코드로 Heap의 동작을 살펴보겠습니다.
public class Main {
public static void main(String[] args) {
int port = 4000;
String host = "localhost";
}
}
int port = 4000; 에 의해서 기존처럼 stack 에 4000 이라는 값이 port 라는 변수명으로 할당되어
스택의 상태는 아래와 같이 됩니다.
String 은 Object 를 상속받아 구현되었으므로 (Object 타입은 최상위 부모클래스입니다, Polymorphism 에 의해 Object 타입으로 레퍼런스 가능합니다) String 은 heap 영역에 할당되고 stack 에 host 라는 이름으로 생성된 변수는 heap 에 있는 “localhost” 라는 스트링을 레퍼런스 하게 되고 . 그림으로 표현하면 아래와 같게됩니다.
Stack & Heap 심화 예
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);
}
}
프로그램의 시작과 함께 처음 실행되는 구문은 아래의 구문입니다.
List<String> listArgument = new ArrayList<>();
여기서 new 키워드는 특별한 역할을 합니다.
생성하려는 오브젝트를 저장할 수 있는 충분한 공간이 heap 에 있는지 먼저 찾은 다음, 빈 List 를 참조하는 listArgument 라는 로컬변수를 스택에 할당합니다. 결과는 아래와 같습니다.
그 다음으로
listArgument.add("yaboong");
구문이 실행되게 되는데 위 구문은 listArgument.add(new String("yaboong"));d와 같은 역할을 하게 됩니다 .
즉 new 를 이용하여 Heap에 충분한 영역이 있는지 확인한 후 , "yaboong"이라는 문자를 할당하게 됩니다 .
이때 새롭게 생성된 문자열인 "yaboong"은 stack 에 할당하지 않습니다 .
List 내부의 인덱스에 의해 하나씩 add()된 레퍼런스 값을 같게 됩니다 .
다음 ,
listArgument.add("github");
가 실행되면 List에서 참조하는 문자열이 하나 더 생기게 됩니다 . 아래와 같습니다 .
그 다음
print(listArgument);
위 구문으로 인해 함수 호출이 일어납니다.
이떄 listArgument라는 참조변수를 메소드로 넘겨주게됩니다 .
함수 호출시 원시타입의 경우와 같이 넘겨주는 인자가 가지고 있는 값이 그대로 파라미터에 복사됩니다.
println(List<String> listParam) 메소드에서는 listParam 이라는 참조변수를 인자로 받게 되어있습니다.
따라서 print()함수호출에 따른 메모리 변화는 아래와 같습니다.
listParam 이라는 참조변수가 새롭게 stack 에 할당되어 기존 List 를 참조하게 되는데,
기존에 인자인 listArgument 가지고 있던 값(List 에 대한 레퍼런스)을 그대로 listParam 이 가지게 되는 것입니다.
그리고 print() 함수 내부에서 listArgument 는 scope 밖에 있게 되므로 접근할 수 없는 영역이 됩니다.
다음으로, print() 함수 내부에서는 List 에 있는 데이터에 접근하여 값을 value 라는 변수에 저장합니다.
이 때 print() 함수의 scope 에서 stack 에 value 가 추가되고,
이 value 는 listParam 을 통해 List 의 0번째 요소에 접근하여 그 참조값을 가지게 됩니다.
그리고나서 또 데이터를 추가하고, 출력함으로 print() 함수의 역할은 마무리 되게 됩니다.
String value = listParam.get(0);
listParam.add("io");
System.out.println(value);
위의 코드가 실행되고 함수가 종료되기 직전의 stack과 heap의 상태는 아래와 같습니다.
이제 함수가 닫는 중괄호 } 에 도달하여 종료되면 print() 함수의 지역변수는 모두 stack 에서 pop 되어 사라집니다
이때, List 는 Object 타입이므로 지역변수가 모두 stack 에서 pop 되더라도 heap 영역에 그대로 존재합니다.
즉, 함수호출시 레퍼런스 값을 복사하여 가지고 있던 listParam 과 함수내부의 지역변수인 value 만 스택에서 사라지고 나머지는 모두 그대로인 상태로 함수호출이 종료됩니다.
print(listArgument);
호출이 종료된 시점에서 스택과 힙 영역의아래와 같습니다 .
Object 타입의 데이터, 즉 heap 영역에 있는 데이터는 함수 내부에서 파라미터로 copied value 를 받아서 변경하더라도 함수호출이 종료된 시점에 변경내역이 반영되는 것을 볼 수 있습니다.
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 를 상속받아 구현되었으니… Object 타입이고…
당연히 20 이 나오겠지 ㅡ,.ㅡa
지금까지 정리한 내용을 기초해서 실행순서대로 살펴보면,
- Integer 는 Object 타입이므로, 첫 구문인 Integer a = 10; 에서 10 은 heap 영역에 할당되고, 10 을 가리키는 레퍼런스변수 a 가 스택에 할당된다.
- 함수에 인자를 넘겨줄때에 파라미터는 copied value 를 넘겨받는다.
- 그러므로, changeInteger(a); 에 의해, param 이라는 레퍼런스 변수가 스택에 할당되고, 이 param 은 main() 함수에서 a 를 가리키던 곳을 똑같이 가리키고 있다.
- main() 함수에서 레퍼런스하던 a 와 같은 곳을 param 이 가리키고 있으므로 param 에 10 을 더하면, changeInteger() 함수가 종료되고 a 의 값을 출력했을 때 바뀐 값이 출력될 것이다.
뭐냐… 20 이 아니다… 값이 안바뀐다… 헛배웠다…?
값이 바뀌지 않는 이유는 아래 코드를 생각해보면 된다. String 은 불변객체(immutable) 로써 어떤 연산을 수행할때마다 기존 오브젝트를 변경하는 것이 아니라 새로운 오브젝트를 생성하는 것이라고 알고 있을 것이다.
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"; } }
changeString() 내부동작만 살펴보면,
- main() 메소드의 s 변수가 레퍼런스하는 “hello” 오브젝트를 param 에 복사하면서 changeString() 메소드가 시작된다.
- param += " world"; 를 실행하는 것은 heap 에 “hello world” 라는 스트링 오브젝트가 새롭게 할당되는 작업이다.
- 기존에 “hello” 오브젝트를 레퍼런스하고 있던 param 으로 새롭게 생성된 스트링 오브젝트인 “hello world” 를 레퍼런스 하도록 만드는 것이다.
- changeString() 함수가 종료되면, 새롭게 생성된 “hello world” 오브젝트를 레퍼런스 하는 param 이라는 변수는 스택에서 pop 되므로 어느것도 레퍼런스 하지 않는 상태가 된다.
- (아래에서 간략히 살펴보겠지만) 이런 경우 “hello world” 오브젝트는 garbage 로 분류된다.
그러므로, changeString() 메소드를 수행하고 돌아가도 기존에 “hello” 를 레퍼런스하고 있던 s 변수의 값은 그대로이다. Immutable Object 는 불변객체로써, 값이 변하지 않는다. 변경하는 연산이 수행되면 변경하는 것 처럼 보이더라도 실제 메모리에는 새로운 객체가 할당되는 것이다.
자바에서 Wrapper class 에 해당하는 Integer, Character, Byte, Boolean, Long, Double, Float, Short 클래스는 모두 Immutable 이다. 그래서 heap 에 있는 같은 오브젝트를 레퍼런스 하고 있는 경우라도, 새로운 연산이 적용되는 순간 새로운 오브젝트가 heap 에 새롭게 할당된다.
처음에는 왜 이렇게 되는거지? 의문을 가지다가 Integer 클래스의 구현을 보니 final 이라는 키워드가 붙어있었다. 이 final 때문인가? 싶었는데, 아니다. 클래스에 붙어있는 final 은 값을 바꾸지 못하도록 하는 역할이 아닌, 상속을 제한하는 목적으로 붙이는 제어자이다.
Integer 클래스를 까보면 내부에서 사용하는 실제 값인 value 라는 변수가 있는데, 이 변수는 private final int value; 로 선언 되어있다. 즉, 생성자에 의해 생성되는 순간에만 초기화되고 변경불가능한 값이 된다. 이것 때문에 Wrapper class 들도 String 처럼 Immutable 한 오브젝트가 되는 것이다.
Garbage Collection 살짝 겉핥아보기
이제 간단한 코드를 살펴보면서 garbage collection 이 뭔지 살짝만 알아보자.
public class Main { public static void main(String[] args) { String url = "https://"; url += "yaboong.github.io"; System.out.println(url); } }
위 코드에서
String url = "https://";
구문이 실행된 뒤 스택과 힙은 아래와 같다.
stack and heap
다음 구문인
url += "yaboong.github.io";
문자열 더하기 연산이 수행되는 과정에서, (String 은 불변객체이므로) 기존에 있던 "https://" 스트링에 "yaboong.github.io" 를 덧붙이는 것이 아니라, 문자열에 대한 더하기 연산이 수행된 결과가 새롭게 heap 영역에 할당된다. 그 결과를 그림으로 표현하면 아래와 같다.
stack and heap
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 다. Reachable 오브젝트가 레퍼런스하고 있는 오브젝트 또한 marking 한다. 첫번째 단계인 marking 작업을 위해 모든 스레드는 중단되는데 이를 stop the world 라고 부르기도 한다. (System.gc() 를 생각없이 호출하면 안되는 이유이기도 하다)
그리고 나서 mark 되어있지 않은 모든 오브젝트들을 힙에서 제거하는 과정이 Sweep 이다.
Garbage Collection 이라고 하면 garbage 들을 수집할 것 같지만 실제로는 garbage 를 수집하여 제거하는 것이 아니라, garbage 가 아닌 것을 따로 mark 하고 그 외의 것은 모두 지우는 것이다. 만약 힙에 garbage 만 가득하다면 제거 과정은 즉각적으로 이루어진다.
Garbage Collection 이 일어난 후의 메모리 상태는 아래와 같을 것이다.
stack and heap
Garbage Collection 정책과 방식에는 여러가지가 있지만 (아직 공부를 덜해서) 이 포스팅에서는 다루지 않겠다.
비슷한 예제를 하나 더 살펴보자.
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); listArgument = new ArrayList<>(); } private static void print(List<String> listParam) { listParam.add("io"); System.out.println(listParam); } }
위 코드에서는 listArgument 라는 변수에 두번의 할당작업이 일어난다. 위와 같이 실행한 경우 stack 과 heap 영역은 아래와 같이 될 것이다.
stack and heap
기존에 사용했던 listArgument 참조변수는 새롭게 생성한 빈 List 를 레퍼런스 한다. 세개의 String 오브젝트는 List 내부의 인덱스에 의해 레퍼런스 되고 있지만 stack 에서는 Unreachable 한 영역에 있다. 기존에 listArgument 가 참조했던 “yaboong”, “github”, “io” 를 가진 ArrayList 를 참조하고 있는 변수는 어느 stack 에서도 찾아볼 수 없다.
앞서 본 경우와 비슷하게 이런 경우에도 기존의 List 오브젝트와, List 오브젝트가 힙 내부에서 레퍼런스하고 있는 String 오브젝트 모두 garbage 로 분류된다.
Garbage Collection 이 일어난 후의 stack 과 heap 영역은 아래와 같을 것이다.
stack and heap
'JAVA' 카테고리의 다른 글
Java - Garbage Collection (0) | 2019.09.02 |
---|---|
Java - JVM (0) | 2019.09.02 |
Primitive vs Reference (0) | 2019.09.01 |
Java Stream 결과 만들기 (0) | 2019.09.01 |
Java Stream 가공하기 (0) | 2019.09.01 |
댓글