자바의 메모리 구조
자바의 메모리 구조는 자바의 JVM 메모리 구조를 말하는 것과 같다. 자바 애플리케이션이 실행되는 동안 사용되는 메모리를 구분하고 효율적으로 관리하기 위해 여러 영역으로 나눠져 있다.
크게 보면 JVM 메모리는 메서드 영역, 스택 영역, 힙 영역의 3가지로 나뉜다. 이 외에도 PC 레지스터나 네이티브 메서드 스택 같은 영역이 있지만, 자바 동작을 이해하는 데는 이 3개의 영역만 인지해도 충분하다.
1. 메서드(Method) 영역
메서드 영역은 JVM이 실행 중에 클래스 수준의 정보를 저장하는 중요한 메모리 공간이다. 프로그램 실행 도중 로드된 클래스와 관련된 데이터가 이곳에 저장되며, 모든 스레드가 공유하는 영역이다.
메서드 영역에 저장되는 주요 데이터는 다음과 같다.
- 클래스 메타데이터: 클래스 이름, 클래스 로더, 부모 클래스 정보 등 클래스 구조를 설명하는 정보
- 클래스 변수(static 변수): 클래스가 로드될 때 초기화되며, 모든 객체가 공유하는 변수
- 메서드의 바이트코드: 프로그램 실행을 위한 컴파일된 메서드의 코드
- 런타임 상수 풀(Runtime Constant Pool): 컴파일 시 생성된 상수와 참조 정보를 저장하며, 런타임에 동적으로 사용할 수 있는 상수 정보도 포함
이 영역은 프로그램 시작 시 JVM에 의해 초기화되며, 클래스가 로드될 때 해당 클래스의 정보가 메서드 영역에 저장이 된다.
이 영역은 모든 스레드가 공유하며, 자바 프로그램에서 클래스 정보를 관리하고 공유하는 데 핵심적인 역할을 한다.
메서드 영역은 일반적으로 프로그램 실행 도중 클래스 메타데이터와 관련된 정보를 저장하며, 대부분 프로그램 시작부터 종료까지 유지됩니다. 그러나 JVM은 동적으로 로드된 클래스가 더 이상 참조되지 않을 경우 해당 데이터를 가비지 컬렉션(GC)의 대상으로 처리가 될 수도 있다.
2. 힙(Heap) 영역
힙은 동적 메모리 할당에 사용되며, 프로그램 실행 중 메모리 크기가 결정됩니다. 자바에서는 생성된 객체나 배열 데이터가 힙에 할당된다.
힙은 큰 데이터 구조나 객체를 저장하는 데 적합하지만, 일반적으로 메모리를 수동으로 관리해야 하는 부담이 있다. 그러나 자바에서는 가비지 컬렉션(Garbage Collection)을 통해 더 이상 참조되지 않는 객체를 자동으로 수거함으로써, 메모리 관리에 대한 개발자의 부담을 줄이고, 메모리 누수를 방지한다.
힙은 모든 스레드가 공유하는 영역이기 때문에, 스레드 간 동기화가 적절히 이루어지지 않으면 데이터 불일치와 같은 정합성 문제가 발생할 수 있다. 하지만 동기화를 적절히 처리하면 여러 스레드 간 데이터 공유에 유용하다. 힙은 스택보다 유연하지만, 메모리 할당과 해제 과정이 상대적으로 느리고 관리가 복잡하다는 단점이 있다.
3. 스택(Stack) 영역
스택은 정적 메모리 할당에 사용되며, 컴파일 시간에 크기가 결정됩니다. 함수 호출 시 생성되는 지역 변수와 매개변수는 스택 프레임(Stack Frame) 형태로 저장되며, 객체의 참조 변수 또한 스택에 저장된다. 다만, 참조 변수와 달리 실제 객체 데이터는 힙에 저장된다.
스택 메모리는 LIFO(Last In, First Out) 원칙을 따르는 스택 자료구조처럼 동작한다. 함수 호출 시 메모리가 스택 프레임 단위로 쌓이고, 함수가 종료되면 가장 마지막에 추가된 프레임이 제거된다. 이처럼 자동으로 메모리가 할당 및 해제되기 때문에 접근 속도가 빠르고 효율적이라는 장점이 있다.
또한, 스택은 각 스레드별로 독립적인 영역을 가지므로, 스레드 간 간섭 없이 안전하게 메모리에 접근할 수 있다.
스택 영역 동작 방식
다음 코드를 통해 스택 영역이 어떤 방식으로 동작되는지 확인해 보자.
public class JavaMemoryMain1 {
public static void main(String[] args) {
System.out.println("main start");
method1(10);
System.out.println("main end");
}
static void method1(int m1) {
System.out.println("method1 start");
int cal = m1 * 2;
method2(cal);
System.out.println("method1 end");
}
static void method2(int m2) {
System.out.println("method2 start");
System.out.println("method2 end");
}
}
main start
method1 start
method2 start
method2 end
method1 end
main end
위 코드를 보면 자바 프로그램 첫 실행 메서드인 `main`메서드가 실행되며, 그 안에 `method1()`메서드가 실행되고 `method1()` 안에 있는 `method2()`가 실행되는 형태이다. 이걸 그림으로 보면 다음과 같다.
스택 자료구조 형태로 메서드 실행 순서대로 스택프레임이 만들어 지는 것을 확인할 수 있다. 스택 프레임에는 아까 설명한 내용과 같이 매개 변수가 포함되어 있는 것을 확인할 수 있다. `m1, cal, args...`
`method2`를 시작으로 하나씩 종료되는 모습은 다음과 같고, 스택의 후입선출과 같은 형태로 진행이 된다.
메서드의 실행이 끝나면 스택 프레임도 종료되는 것을 볼 수 있다.
정리하자면 자바는 스택 영역을 사용해서 메서드 호출과 지역 변수(매개변수 포함)를 관리한다. 메서드를 계속 호출하면 스택 프레임이 스택 자료구조와 동일한 형태로 쌓이게 되며, 마지막 들어온 스택 프레임부터 동작을 시작하게 된다. 동작이 마무리되면 스택 프레임이 관리하는 데이터들도 스택 프레임과 함께 종료되어진다. 모든 스택 프레임이 종료된다면(제거를 의미) 프로그램도 종료되어지게 된다.
스택 영역과 힙 영역의 동작 방식
다음 예제 코드를 통해 스택 영역과 힙 영역이 어떤 방식으로 관계를 맺고 있는지 확인해 보자.
package memory;
public class Data {
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
public class JavaMemoryMain2 {
public static void main(String[] args) {
System.out.println("main start");
method1();
System.out.println("main end");
}
static void method1() {
System.out.println("method1 start");
Data data1 = new Data(10);
method2(data1);
System.out.println("method1 end");
}
static void method2(Data data2) {
System.out.println("method2 start");
System.out.println("data.value=" + data2.getValue());
System.out.println("method2 end");
}
}
main start
method1 start
method2 start
data.value=10
method2 end
method1 end
main end
위 코드는 `main()` -> `method1()` -> `method2()` 순서로 호출하는 단순한 코드이다. method1()`에는 `Data` 클래스의 인스턴스를 생성하는 코드가 있다. 그렇게 만들어진 인스턴스는 `method1()` 에서 `method2()` 를 호출할 때 매개변수에 `Data` 인스턴스의 참조값을 전달한다.
위 실행결과를 보면 스택에 쌓인 순서대로 동작을 하였고, `method1`에서 대입된 매개변수가 `method2`에서도 동일한 값 형태로 나오는 것을 확인할 수 있다. 다음 그림을 통해 더 자세히 알아보자.
위 그림을 보면 메서드 실행 순서에 따른 스택프레임이 만들어져 있고, `method1()`에서 만들어서 `Data` 클래스의 인스턴스가 힙 영역에 만들어진 것을 확인할 수 있다. 이전 포스팅에서 정리했듯이 참조형에서 변수가 가지는 값은 실제 메모리(힙영역)에 있는 인스턴스의 위치(참조) 값을 가지게 된다. 그렇기 때문에 스택 프레임에 `data1`변수는 `x001`와 같은 참조값을 저장하고 있게 된다.
다음으로 `method1()`에서 만든 인스턴스를 `method2()`메서드의 매개변수로 활용했는데 이 경우에도 참조값이 복사되어 전달되므로 `method2()`의 스택 프레임에도 참조값을 저장하게 된다. 그래서 `method2()`에서의 호출값이 `method1()`의 `data1`변수와 동일값을 나타내는 것이다.
이번엔 실행 후 종료에 대한 과정을 보자.
우선 `method2()`가 종료되어 해당 스택 프레임이 제거된다. 스택 프레임이 제거되면서 `data2`변수도 함께 제거되며, 인스턴스를 가리키는 참조값을 가진 변수 하나가 사라짐을 의미한다.
이후, `method1()`도 종료가 되면 제거가 된다. 이 때도 `data1`이 함께 제거되므로 `Data`인스턴스를 가리키는 변수가 모두 사라지게 된다.
이 경우 `Data`인스턴스는 참조하는 곳이 없어지므로, GC의 대상이 되어 메모리에서 제거가 된다.
static과 메서드 영역
지금까지 지역 변수는 스택 영역에, 객체(인스턴스)는 힙 영역에 관리되는 것을 확인했다. 이제 나머지 하나가 남았다. 바로 메서
드 영역이다. 메서드 영역이 관리하는 변수도 있다. 이것을 이해하기 위해서는 먼저 `static` 키워드를 알아야 한다. `static` 키워드는 메서드 영역과 밀접한 연관이 있다.
1. static 변수
`static`키워드는 클래스 레벨(Class-level)의 멤버(변수 및 메서드)를 정의하는 데 사용됩니다. static 멤버는 클래스에 속하며, 객체의 인스턴스와 무관하게 사용된다. 다음과 같은 특징을 가진다.
- 한 번 생성되면 프로그램 종료 시까지 메모리에 유지된다.
- 객체를 생성하지 않아도 클래스명으로 직접 접근할 수 있다.
- 모든 인스턴스에서 동일한 값을 참조하며, 값 변경 시 모든 인스턴스에 영향을 준다.
가장 중요한 특징으로 static 변수는 메서드 영역(Method Area)에 저장된다.
예시를 한번 보자.
public class Example {
static int sharedValue = 0; // static 변수
public void incrementValue() {
sharedValue++;
}
}
public class Main {
public static void main(String[] args) {
Example ex1 = new Example();
Example ex2 = new Example();
ex1.incrementValue();
ex2.incrementValue();
System.out.println(Example.sharedValue); // 출력: 2
}
}
위 예시와 같이 `static`변수를 선언해 놓고, 그 공유 자원의 값이 프로그램 끝날 때까지 유지해야 될 때 주로 사용이 된다. 여기서 `static int sharedValue` 변수를 클래스 변수라 부르게 된다.
멤버 변수(필드)의 종류
멤버 변수는 인스턴스 변수와 클래스 변수로 구분되어진다.
- 인스턴수 변수: `static`이 붙지 않은 멤버 변수를 말한다. `int name;`
- `static`이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라
서 인스턴스 변수라 한다. - 인스턴스 변수는 인스턴스를 만들 때마다 새로 만들어진다.
- 힙 영역에 위치해 있다.
- `static`이 붙지 않은 멤버 변수는 인스턴스를 생성해야 사용할 수 있고, 인스턴스에 소속되어 있다. 따라
- 클래스 변수: `static`이 붙은 멤버 변수 `static int count;`
- 클래스 변수, 정적 변수, static 변수 등으로 부른다. 모두 같은 의미이므로 잘 알아두도록 하자.
- `static` 이 붙은 멤버 변수는 인스턴스와 무관하게 클래스에 바로 접근해서 사용할 수 있고, 클래스 자체에
소속되어 있다. 따라서 클래스 변수라 한다. - 클래스 변수는 자바 프로그램을 시작할 때 딱 1개가 만들어진다. 인스턴스와는 다르게 보통 여러 곳에서 공
유하는 목적으로 사용된다.
변수와 생명주기
지역 변수(매개변수 포함)
지역 변수는 스택 영역의 스택 프레임에 저장된다. 메서드가 종료되면 해당 스택 프레임과 함께 제거된다. 따라서 지역 변수의 생명주기는 메서드 실행 중으로 매우 짧다.
인스턴스 변수
인스턴스의 멤버 변수로, 힙 영역에 저장된다. 힙 영역은 GC(가비지 컬렉션)가 발생하기 전까지 유지되므로 지역 변수보다 생명주기가 길다.
클래스 변수
클래스 변수는 메서드 영역의 static 영역에 저장된다. 클래스가 JVM에 로딩되는 순간 생성되며, JVM이 종료될 때까지 생명주기가 유지된다. 가장 긴 생명주기를 가진다.
static 키워드가 정적인 이유는 생성과 제거 시점의 차이에 있다. 인스턴스 변수는 힙 영역에 동적으로 생성되고 제거된다. 반면, 정적 변수(static)는 프로그램 실행 시점에 생성되고, 프로그램 종료 시점에 제거된다. 즉, 정적 변수는 이름 그대로 정적이다.
2. static 메서드
자바에서 static 메서드는 클래스에 속하며, 객체를 생성하지 않고도 호출할 수 있는 메서드이다. 이를 통해 공통적으로 사용되는 기능을 제공하거나, 유틸리티 메서드를 구현하는 데 사용된다.
- 객체 생성 없이 호출 가능: 클래스명으로 직접 호출한다.
- static 변수 및 다른 static 메서드만 접근 가능: 인스턴스 변수나 메서드는 접근할 수 없다.
- 클래스 로드 시 초기화되며, 프로그램 종료 시까지 메모리에 유지된다.
사용법
`static` 키워드를 사용하여 메서드를 선언한다.
public class MathUtils {
// static 메서드 정의
public static int add(int a, int b) {
return a + b;
}
}
다음과 같이 두 가지 방법으로 호출 가능하다.
1. 클래스명으로 호출 (권장방법)
int result = MathUtils.add(5, 3);
System.out.println(result); // 출력: 8
2, 객체를 통해 호출 가능 (권장하지 않음)
MathUtils utils = new MathUtils();
int result = utils.add(5, 3); // 동작은 같지만 비효율적
객체를 통한 호출이 권장되지 않는 이유는 static 메서드만에 특징을 활용 못하고, 개발자가 헷갈리게 할 수 있기 때문이다. 그리고 `static`메서드의 주요 사용 사례는 인스턴스 변수가 없는 유틸리티 클래스 형태이므로 굳이 객체를 생성할 필요가 없기 때문이다.
3. static 메서드의 제약 사항
1. 인스턴스 변수 사용 불가
- `static` 메서드는 클래스 레벨에서 실행되므로, 인스턴스 변수에 직접 접근할 수 없다.
public class Example {
int instanceVar = 10;
public static void staticMethod() {
System.out.println(instanceVar); // 오류 발생
}
}
2. `this` 키워드 사용 불가
- `static` 메서드는 객체와 독립적이므로 `this`키워드를 사용할 수 없다.
3. 오버라이딩 제한
- static 메서드는 객체가 아닌 클래스 레벨에서 동작하므로 오버라이딩이 불가능하다.
- 대신, 정적 메서드 숨김(Static Method Hiding)이 발생할 수 있다.
이러한 제한은 정적 메서드가 클래스 이름으로 직접 호출되기 때문이다. 정적 메서드는 참조값의 개념이 없으며, 특정 인스턴스의 기능을 사용하려면 참조값이 필요하다. 따라서 정적 메서드 내부에서는 인스턴스 변수나 인스턴스 메서드를 사용할 수 없다.
main() 메서드는 정적 메서드
대표적인 정적 메서드의 예는 main() 메서드이다. main() 메서드는 프로그램의 시작점이며, 인스턴스 생성 없이 실행된다. 이는 main() 메서드가 static 메서드이기 때문이다. 정적 메서드는 다른 정적 메서드만 호출할 수 있다. 따라서, main() 메서드가 같은 클래스의 메서드를 호출하려면 해당 메서드도 static으로 선언해야 한다.
참고
'Language > Java' 카테고리의 다른 글
[Java] 접근 제어자와 캡슐화 (0) | 2025.01.10 |
---|---|
[Java] 생성자가 필요한 이유 (0) | 2025.01.09 |
[Java] null과 NullPointerException (0) | 2025.01.07 |
[Java] 변수의 초기화 (0) | 2025.01.07 |
[Java] 기본형 vs 참조형 (0) | 2025.01.04 |