Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- docker
- 코딩테스트
- Java
- Jenkins
- AWS
- Spring Legacy Project
- 프로그래머스
- 책 정리
- EC2
- Git
- SSL
- db
- WebHook
- window
- spring
- ssh
- spring boot
- centos7
- sample
- jdbc
- 토비의스프링
- TLS
- Linux
- vagrant
- github
- Hibernate
- DISTINCT
- Client
- TypeScript
- mariadb
Archives
- Today
- Total
Woopii Vyeolog
JAVA에서의 메모리 관리 (Heap, Stack, Static ) 본문
JAVA에서의 메모리 영역은 크게 Heap, Stack, Static 영역으로 나뉠 수 있다.
Static 영역
하나의 JAVA 파일(Class)은 크게 필드(field), 생성자(constructor), 메소드(method)로 구성된다.
그 중 필드 부분에서 선언된 변수(전역변수)와 static키워드가 붙은 자료형 혹은 메소드는 Static 영역에 데이터를 저장한다.
Static 영역의 데이터는 프로그램의 시작부터 종료가 될 때까지 메모리에 남아있게 된다.
다르게 말하면 전역변수가 프로그램이 종료될 때까지 어디서든 사용이 가능한 이유이기도 하다.
따라서 전역변수를 무분별하게 많이 사용하다 보면 메모리가 부족할 우려가 있어 필요한 변수만 사용할 필요가 있다.
Stack 영역
New를 통해서 인스턴스 객체(Object)를 하나 생성한다고 하자, 그러면 Heap영역에는 생성된 객체가 메모리 상에 올라간다.
그리고, Stack영역에는 해당 객체에 대한 주소값(Reference)이 저장된다.
또한 Stack영역에는 원시타입(primitive types)의 데이터형 - byte, short, int, long, double, float, boolean, char타입의 데이터가 저장된다.
그리고 전역 변수가 아닌 지역변수들도 Stack메모리에 올라가게 된다.
또한 Stack메모리는 Thread당 하나씩 할당이 된다. 즉, Thread가 새롭게 생성되는 순간 해당 Thread에 해당하는 Stack영역도 생기게 되며, 각 Thread에서 다른 Thread의 Stack영역에는 접근 할 수 없다.
Stack영역에 어떻게 동작하는 지 예제를 하나 들어본다
public class Main {
public static void main(String[] args) {
int param = 4;
param = someOperation(param);
}
private static int someOperation(int param2){
int calc = param2 + 10;
int result = calc + 10;
return result;
}
}
먼저 프로세스가 처음 작동 하면 아래와 같은 동작을 한다.
int param= 4;
Stack영역에 param이라는 변수명이 할당되고, param변수의 타입인 int는 원시 타입이므로 이 공간에는
실제 4라는 값이 할당이 된다. 현제 스택 상태는 아래와 같다.
그 다음 동작은
param = someOperation(param);
이다. 해당 동작으로 인해 someOperater() 함수가 호출이 된다. 그리고 param값이 매개변수로서 변수를
해당 함수에 넘겨주게 되며, scope가 someOperater()로 이동한다.
scope가 바뀌면서 기존의 param값은 scope에서 벗어나므로 사용 할 수 없다.
이 때 인자로 넘겨받은 갑은 파라미터인 param2에 복사되어 전달되는데, param2또한 원시 타입이므로 Stack에 할당된 공간에 값이 할당된다. 현재 Stack상태는 아래와 같다.
다음으로,
int calc = param2 + 10;
int result = calc + 10;
에 의해 같은 방식으로 스택에 값이 할당되며 현재 스택의 상태는 아래와 같다.
다음으로, 닫는괄호 } 가 실행되어 someOperation() 함수호출이 종료되면 호출함수 scope 에서 사용되었던 모든 지역변수들은 stack 에서 pop 된다.
함수가 종료되어 지역변수들이 모두 pop 되고, 함수를 호출했던 시점으로 돌아가면 스택의 상태는 아래와 같이 변한다.
Heap 영역
Heap 영역에는 Object타입(Integer, String, ArrayList.....)의 데이터가 생성된다.
그리고, 몇개의 Thread가 있던 상관없이 단 하나의 Heap영역만 존재한다.
Heap영역에 생기는 Object들의 레퍼런스 변수는 Stack영역에 생긴다.
간단한 예를 통해서 Heap 영역이 어떻게 돌아가는지 보면,
public class Main {
public static void main(String[] args) {
int test_Int = 100;
String test_String = "test";
}
}
해당 클래스가 실행되면 먼저,
int test_Int = 100;
에 의해서 변수 test_Int에 100이라는 값이 Stack메모리 영역에 할당되어 메모리 상태는 아래와 같다.
String타입은 Object를 상속받아 구현되었으므로 (Object 타입은 최상위 부모클래스다, Polymorphism 에 의해 Object 타입으로 레퍼런스 가능하다) String 은 heap 영역에 할당되고 stack 에 test_String이라는 이름으로 생성된 변수는 heap 에 있는 “test” 라는 값을 레퍼런스 하게 된다. 그림으로 표현하면 아래와 같다.
기본적인 Stack과 Heap영역의 작동은 위와같고 조금 더 복잡한 예제를 보기로 한다.
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("test1");
list.add("test2");
print(list);
}
private static void print(List<String> listparam) {
String value = listparam.get(0);
listparam.add("test3");
System.out.println(value);
}
}
프로그램이 실행되면, 다음의 첫 구분이 실행된다.
List<String> list = new ArrayList<>();
여기서 new 키워드는 특별한 역할을 한다.
생성하려는 오브젝트를 저장할 수 있는 충분한 공간이 heap 에 있는지 먼저 찾은 다음,
빈 List 를 참조하는 list라는 로컬변수를 스택에 할당한다. 결과는 아래와 같다.
그 다음으로
list.add("test1");
list.add("test2");
구문이 실행된다. 위 구문들은 list.add(new String("test1"));과 같은 의미이다.
즉, new 키워드에 의해서 Heap영역에 충분한 공간이 있는지 확인한 후 "test1"이라는 문자열을 할당하게 된다.
이 때, 새롭게 생긴 문자열인 "test1"을 위한 변수는 Stack에 할당되지 않는다.
List내부의 인덱스에 의해서 하나씩 add() 된 데이터에 대한 레퍼런스 값을 갖는다.
그림으로 보면 아래와 같다.
다음으로 실행되는 구문은
print(list);
이다. print라는 함수를 호출하고 list라는 변수는 참조변수로서 넘겨주게 된다.
함수 호출 시 원시타입의 경우와 같이 인자가 가지고 있는 값이 그대로 파라미터에 복사하게 된다.
매개변수로 넘겨준 list변수는 listparam이라는 참조변수로 인자를 받는다.
따라서 print() 함수 호출 시 메모리의 변화는 아래와 같다.
listParam 이라는 참조변수가 새롭게 stack 에 할당되어 기존 List 를 참조하게 되는데, 기존에 인자인 list가 가지고 있던 값(List 에 대한 레퍼런스)을 그대로 listParam 이 가지게 되는 것이다.
그리고 print() 함수 내부에서 list는 scope 밖에 있게 되므로 접근할 수 없는 영역이 된다.(참조할 수 없는 영역이 되어 회색으로 표시)
다음으로, print() 함수 내부에서는 List에 있는 데이터에 접근하여 값을 value라는 변수에 저장한다.
이 때 print() 함수의 scope 에서 stack 에 value 가 추가되고, 이 value 는 listParam 을 통해 List 의 0번째 요소에 접근하여 그 참조값을 가지게 된다.
그리고나서 또 데이터를 추가하고, 출력함으로 print() 함수의 역할은 마무리 된다.
private static void print(List<String> listparam) {
String value = listparam.get(0);
listparam.add("test3");
System.out.println(value);
}
해당 코드를 표현하면 아래와 같다.
이제 함수가 닫는 중괄호 } 에 도달하여 종료되면 print() 함수의 지역변수(value, listparam)는 모두 stack 에서 pop 되어 사라진다.
이때, List 는 Object 타입이므로 지역변수가 모두 stack 에서 pop 되더라도 heap 영역에 그대로 존재한다.
즉, 함수호출시 레퍼런스 값을 복사하여 가지고 있던 listparam 과 함수내부의 지역변수인 value 만 스택에서 사라지고 나머지는 모두 그대로인 상태로 함수호출이 종료된다.
Object 타입의 데이터, 즉 heap 영역에 있는 데이터는 함수 내부에서 파라미터로 복자된 값을 받아서 변경하더라도 함수호출이 종료된 시점에 변경내역이 반영되는 것을 볼 수 있다.
참고/출처
https://yaboong.github.io/java/2018/05/26/java-memory-management/
'java' 카테고리의 다른 글
[Java] Socket 통신, Tcp Server 샘플 (0) | 2022.04.02 |
---|---|
[디자인 패턴] Build Pattern, 빌더 패턴 (1) | 2021.02.24 |
Vector vs Arraylist (0) | 2020.02.28 |
String, StringBuffer, StringBuilder (0) | 2020.02.19 |
garbage collecter (0) | 2020.02.19 |
Comments