자바의 메모리 구조에 대해 알고 프로그램이 어떤 식으로 실행되는지 이해 하는 것은 아주 중요하다. 기초적인 부분이지만 정리해두면 좋을 것 같아서 정리해보았다.
1. JAVA의 메모리 구조
자바의 메모리 구조는 크게 메서드 영역, 스택 영역, 힙 영역으로 나눌 수 있다.
1-1. 메서드 영역(Method Area)
메서드 영역은 프로그램을 실행하는데 필요한 공통 데이터를 관리하며 이 영역은 프로그램의 모든 영역에서 공유된다.
- 클래스 정보: 클래스의 실행 코드(바이트 코드), 필드, 메서드와 생성자 코드 등 모든 실행 코드가 존재한다.
- static 영역: static 변수와 메서드들을 보관한다.
static 키워드가 붙지 않은 필드는 인스턴스를 생성해야 사용이 가능하고, 인스턴스에 소속되어 있다. 따라서 인스턴스 변수라고 한다. 반면 static 키워드가 붙은 변수는 정적 변수라고 한다. 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에 소속되어 있는 변수이다. 정적 변수는 프로그램을 시작할 때 딱 1개가 만들어지고, 여러곳에서 공유하는 목적으로 사용된다. 클래스 명을 통해 바로 변수에 접근하거나 인스턴스를 통해서도 접근할 수 있다.
힙 영역에 생성되는 인스턴스 변수는 동적으로 생성되고, 제거되지만 정적 변수는 프로그램 실행 시점에 만들어지고, 프로그램 종료 시점에 제거된다. 그래서 정적이라고 불리는 것이다.
static 키워드가 붙은 메서드는 클래스의 인스턴스를 생성하지 않아도 클래스 명을 통해 바로 호출할 수 있다. 이를 정적 메서드라고 하고 정적 메서드는 클래스 명을 통해 바로 호출하거나, 인스턴스를 통해서도 호출할 수 있다.
* 자바의 정적 변수, 메서드는 인스턴스를 통해 호출이 가능한데 그렇기 때문에 오해의 소지가 있다. 인스턴스에 속한 변수, 메서드라고 오해할 여지가 있는 것이다. 코틀린에서는 자바의 static과 유사하게 Companion Object(동반 객체)라는 것을 제공하는데 이는 오직 클래스 명을 통해서만 접근가능하기 때문에 이런 오해의 여지가 없다.(동작방식도 다른데 자바의 static과 다르게 실제로 객체를 생성한다)
- 런타임 상수 풀: 프로그램을 실행하는데 필요한 공통 리터럴 상수(상수값, 리터럴 등)를 보관한다. 클래스, 메서드, 필드 정보와 관련된 상수들을 보관하며, 주로 정수 상수, 문자열 리터럴, 클래스 이름 등의 값을 포함한다. 예를 들어, 프로그램에 "String"이라는 리터럴 문자가 있으면 이런 문자를 공통으로 묶어서 관리한다. 이외에도 프로그램을 효율적으로 관리하기 위한 상수들을 관리한다.
(참고, 문자열을 다루는 문자열 상수 풀은 자바 7부터 힙 영역으로 이동했다.)
* 문자열 상수 풀: 문자열 중복을 방지하고 메모리를 절약하기 위해 존재한다. 문자열을 생성할 때, 이미 동일한 문자열이 존재하는지 상수풀을 확인한 후 존재하지 않으면 새로 생성하고, 존재하면 기존의 문자열 객체를 재사용한다. 동일한 문자열을 여러 번 사용하는 경우 메모리 낭비를 줄일 수 있다.
public static void main(String[] args) {
String str1 = "String";
String str2 = "String";
String str3 = new String("String");
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str2 == str3); // false
}
이는 직접 위의 코드를 실행해봄으로써 확인할 수 있다. str1과 str2는 동일한 객체이기 때문에 동일성을 체크해보면 동일하다고 나온다. 하지만 String 생성자를 이용해 만든 str3는 str1과 str2와 다른 것을 확인할 수 있다. String 생성자를 이용하거나 StringBuilder 등을 이용하는 경우에는 문자열 상수 풀을 이용하지 않고, 문자열 객체를 새로 생성하게 된다.
1-2. 스택 영역 (Stack Area)
자바 실행 시, 하나의 실행 스택이 생성된다. (더 정확히는 스택 영역은 쓰레드별로 하나의 실행 스택이 생성된다. 따라서 쓰레드 수 만큼 스택 영역이 생성된다.) 각 스택 프레임은 지역 변수, 중간 연산 결과, 메서드 호출 정보 등을 포함한다.
* 스택 프레임: 스택 영역에 쌓이는 네모 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다.
위의 그림과 같은 예시를 간단히 살펴보면 다음과 같다.
먼저 프로그램을 실행하면 main()이 실행되고, main()을 위한 스택 프레임이 하나 생성된다.
main()에서 method1()을 호출하면 method1() 스택 프레임이 생성된다. method1()이 가지는 매개 변수와 지역 변수들이 스택 프레임이 포함되게 될 것이다.
그리고 method1()이 method2()를 호출하면 method2() 스택 프레임이 생성된다. method1()가 마찬가지로 변수들이 스택 프레임에 포함되게 될 것이다.
method2() 실행이 종료되면 method2() 스택 프레임이 제거되고, method1()에서 method2()를 호출한 지점으로 돌아가게 될 것이고, method1() 실행이 종료되면 역시 method1() 스택 프레임도 제거되게 될 것이다.
main()이 종료되면 스택 프레임이 비워지고, 프로그램을 정리하고 종료하게 된다.
1-3. 힙 영역(Heap Area)
객체(인스턴스)와 배열이 생성되는 영역이다. 가비지 컬렉션(GC)가 이루어지는 주요 영역이며, 더 이상 참조되지 않는 객체는 GC에 의해 제거된다.
위의 예시처럼 특정 클래스로 2개의 인스턴스를 생성하면, 힙 메모리에 2개의 인스턴스가 생긴다. 각각의 인스턴스는 내부에 변수를 가진다. 같은 클래스로부터 생성된 객체라도, 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유하기 때문에 객체가 생성될 때 인스턴스 변수에는 메모리가 할당되지만, 메서드는 메서드 영역에서 공통으로 관리되고 실행된다.
두 개의 인스턴스에 대한 참조 값이 각각 data1, data2라는 변수에 보관된 상태이다. method2()의 실행이 종료되어 method2()의 스택 프레임이 제거되면 매개변수 data2도 함께 제거될 것이다. 그러면 더이상 x002라는 참조값을 가진 Data 인스턴스를 참조하는 것이 없게 된다. 참조하는 곳이 없으므로 사용되는 곳도 없고 더 이상 사용되지 않는 객체이다. 이러면 GC가 일어나 해당 인스턴스를 메모리에서 제거한다. 마찬가지로 method1()의 실행이 종료되면 x001의 참조값을 가진 Data 인스턴스 역시 메모리에서 제거될 것이다.
'Java' 카테고리의 다른 글
지역 클래스의 지역 변수 캡쳐 (0) | 2025.01.08 |
---|---|
JAVA 타입 변환 정리 (0) | 2025.01.07 |
JAVA 문자열 관련 메서드 정리 (0) | 2025.01.06 |
Java의 equals()와 hashCode() (0) | 2025.01.03 |
JAVA 배열 (0) | 2024.12.31 |