자바에는 여러가지 메모리 모델이 융합되어 사용되는데 각 메모리 구역에 따라 필요한 역활을 수행하기 때문에 메모리 구역을 나눈 것이다.
대표적으로 메모리 구역은 크게 이렇게 나뉜다. 물론 다른 PC register와 같은 메모리 영역들도 있지만 면접질문에서 주로 다루는 항목 위주로 적어보겠다.
- Heap
- Stack
- MetaSpace(Method Area)
Heap Memory Area
Heap 영역에서는 생성된 클라스 인터페이스, 스트링, 배열과 같은 오브젝트들을 저장한다. 대체로 Runtime때 생성된 오브젝트를 보관하는 역활을 한다.
여기서 중요한점은 String이나 Integer(-128 ~ 127 까지만) 처럼 Inbuilt 자바 클라스들은 Pool을 사용하며, Pool은 Heap에 있는 값들을 참조하기 때문에 생선된 값이 Heap에 이미 존재하면 이 값을 재활용한다. 말그대로 주소값을 공유한다.
스트링 풀(String Pool)을 예로 들어보자, 우리는 인스턴스를 초기에 선언하고 생성할 때, new를 이용하지만 스트링을 사용 할 때 만큼은 그러지를 않았는데 이는 자바 String 라이브러리에서 Pool을 통해 Heap의 String 값들을 찾아보고 존재 하지않으면 new를 통해 새로운 스트링 인스턴스를 만들지만 존재 하면 해당 값의 주소를 복사해서 보내는 방법으로 최적화를 했기 때문이다.
예를 들어서 우리가 스트링 타입을 가지는 수많은 변수를 같은 값으로 지정했을 때 실제로 메모리에 값은 하나만 존재하고 변수들은 이름만 다를 뿐, 똑같은 주소값을 가르킨다(Pointer).
아래 예시 코드를 보자. a, b, c는 스트링 풀을 이용한 스트링 선언문이고 d, e, f는 new를 이용한 새로운 인스턴스 생성 선언문이다.
private void stringHeapPoolTest() {
String a = "hello";
String b = "hello";
String c = "hello!";
System.out.println("""
String initialization with literal assignment(utilize string pool)
---- a = "hello", b = "hello", c = "hello!" ----
""");
System.out.printf("a == b : %b\n", a == b);
System.out.printf("b == c : %b\n", b == c);
System.out.printf("a == c : %b\n", a == c);
System.out.println("a.equals(b) : " + a.equals(b));
System.out.println("b.equals(c) : " + b.equals(c));
System.out.println("a.equals(c) : " + a.equals(c));
String d = new String("hello");
String e = new String("hello");
String f = new String("hello!");
System.out.println("""
String initialization with "new String()"
---- d = "hello", e = "hello", f = "hello!" ----
""");
System.out.printf("d == e : %b\n", d == e);
System.out.printf("e == f : %b\n", e == f);
System.out.printf("d == f : %b\n", d == f);
System.out.println("d.equals(e) : " + d.equals(e));
System.out.println("e.equals(f) : " + e.equals(f));
System.out.println("d.equals(f) : " + d.equals(f));
}
해당 코드를 돌리면 다음과 같이 출력된다.
놀랍게도 일반적으로 사용되는 String a = "hello"는 b 변수와 대조를 하니 주소 값이 같게 나오고.
String d = new String("hello); 는 변수 e와 다른 주소값이 나오는것을 볼 수 있다.
결론적으로 선언 방식에 따라서 다른 방향으로 스트링 변수를 정의할 수 있게 되는것이다.
만약 본인이 스트링 변수의 주소까지 다르게 하고 싶으면 new를 지정하는 것이 올바른 방법인 것이다.
Stack Memory Area
스택 메모리 영역은 스레드가 생성되고 실행 될 때 사용되는 영역이며, 함수 범위의 변수와 값들을 저장한다. 말 그대로 인스턴스가 정의되면 콜 스텍에 쌓은 메서드(순차대로 실행 되어야 할 메서드)는 Stack에 쌓이고 스택에 마지막으로 쌓인 함수를 토대로 연산을 진행한다 (Last in First Out - LIFO). 그러기 때문에 우리가 흔히 많이 들어왔던 Stack Over Flow가 여기서 나오는 것이다.
재귀(Recursion), 혹은 함수 호출 로직을 잘 못 짜서 함수호출을 무한으로 하거나 할당된 메모리 양을 넘어서서 호출할 때 빈번히 일어나는 일인데, 문제점은 함수를 Stack에 쌓았는데 함수 연산을 진행하지 않아 콜 스택에 함수가 없어지지 않고 계속 쌓이기만 해거나 함수 진행속도가 쌓이는 속도가 너무 빠르면 JVM에서 설정된 메모리 이상을 넘어 갔을 때 Stack-Over-Flow와 같은 에러문구와 같이 프로세스를 강제로 종료시킨다.
이부분은 본인이 졸업프로젝트를 진행할 때 부딪힌 문제점이기도 했다.
프로젝트는 아래와 같은 State Machine Diagram을 자동으로 코드로 만들어 줘서 해당 코드들을 IoT 기기한테 그대로 입력하면 아래 다이어 그램 로직과 동일하게 작동하는 IoT 코드 자동화 프로그램이었는데. 문제는 우리가 state를 메서드로 정의하고 화살표를 해당 메서드를 부르는 형식으로 자동화(Code Generator)를 했는데, 그렇게 state들이 거대해지고 loop이 빈번하게 일어나게 되면 infinite state machine이 되어 버려 주입해야 할 함수 수가 늘어나서 스택 오버 플로우가 뜰 확율이 높았기 때문이다.
다행히 우리 프로젝트는 초기 단계라서 교수님이 로직이 복잡해지지 않아도 된다는 허락을 받아 통과가 되었지만 이후 해당 프로젝트를 이어나가는 후배 팀원들이 이 문제점을 어떻게 해결을 했을지가 매우 궁금하다.
추가로, 스택과 힙메모리 영역은 서로 분할된 영역이지만 서로 상호작용을 하는 구간이여서 완전이 따로 볼 수는 없다.
왜냐하면 메서드 내부의 데이터들은 여전히 힙 메모리에 저장되기 때문이다.
MetaSpace Memory Area
MetaSpace는 Heap과 Stack이랑 거리가 먼 메모리 영역인데 이유는 JVM에서 따로 할당받은 메모리가 아닌, JVM 자체 메모리로 구역이 일정하지가 않고 언제든지 추가로 사용하고 줄일 수가 있다. 여기서 Garbage collection 프로세스, 클라스 메타데이터, Pool 정보, 자바 바이트코드가 MetaSpace 영역을 사용한다. 즉, 메타스페이스는 소프트웨어, 런타임의 관리 차원에서 사용되는 데이터들이나 프로세스들이 해당 메모리 영역을 사용한다고 생각하면 편하다.
'개발 언어 > 자바' 카테고리의 다른 글
Java Spring 의 Servet 이해하고 만들어보기 (0) | 2025.03.24 |
---|---|
String, String format, 그리고 String builder (2) | 2024.01.29 |
Java 8와 Java 17의 차이 (0) | 2024.01.22 |
Java 스레드(Thread) 분석/모니터링 도구들 (0) | 2024.01.17 |
자바의 Predicate, Consumer와 Supplier 그리고 응용 (0) | 2024.01.12 |