자바의 메모리 구조는 프로그램 실행 중에 데이터를 어떻게 저장하고 관리하는지에 대한 중요한 개념이다. 이번 포스트에서는 자바의 메모리 구조와 static 키워드의 사용법을 알아보자!
메서드 영역 (Method Area): 클래스 정보, static 변수, 상수 풀이 저장되는 영역으로, 프로그램 실행 중에 사용하는 공통 데이터를 저장하고 공유한다.
스택 영역 (Stack Area): 메서드 호출 시마다 새로운 스택 프레임이 생성되며, 메서드가 종료되면(함수의 괄호가 닫히면) 해당 스택 프레임이 제거된다. 각 스레드는 독립적인 스택을 가진다.
힙 영역 (Heap Area): 객체와 배열이 생성되는 영역으로, 가비지 컬렉터(GC)에 의해 더 이상 참조되지 않는 객체는 제거된다. 힙은 영 제너레이션, 올드 제너레이션, 메타스페이스로 나뉘어 객체의 생애 주기에 따라 관리된다.
Mark and Sweep: 참조되지 않는 객체를 '마크'하고, 이를 '스윕'하여 힙에서 제거하는 방식으로 메모리를 관리한다.
Generational Collection: 젊은 객체는 영 제너레이션에 할당되고, 오래 살아남은 객체는 올드 제너레이션으로 이동하여 관리된다.
아래 코드를 실행하면 메서드 영역, 스택 영역, 힙 영역이 어떻게 작용하는지 확인할 수 있다.
public class MemoryExample {
public static void main(String[] args) {
// 힙 영역: 스택 프레임 객체가 힙에 생성
StackFrame stackFrame1 = new StackFrame("Method Stack Frame 1");
StackFrame stackFrame2 = new StackFrame("Method Stack Frame 2");
// 스택 영역: createMethod() 호출로 스택 프레임이 추가
createMethod(stackFrame1); // 스택 프레임 1을 처리
createMethod(stackFrame2); // 스택 프레임 2를 처리
}
// 메서드 영역: 프로그램 실행 중 공통적으로 사용되는 메서드가 저장
public static void createMethod(StackFrame stackFrame) {
System.out.println(stackFrame.getFrameName() + " in execution.");
}
}
// 메서드 영역: StackFrame 클래스와 관련된 정보는 메서드 영역에 저장
class StackFrame {
private String frameName; // 힙 영역에 저장되는 인스턴스 변수
// 힙 영역: 생성자를 통해 인스턴스가 힙에 저장
public StackFrame(String frameName) {
this.frameName = frameName;
}
// 메서드 영역: getFrameName 메서드가 메서드 영역에 저장
public String getFrameName() {
return frameName;
}
}

스택 영역: 메서드 호출 시 생성되는 공간이다. main() 메서드와 createMethod() 메서드의 스택 프레임들이 여기에 저장된다.
힙 영역: StackFrame 객체들이 힙에 생성되며, 이 객체들은 frameName 필드를 가지고 있다.
메서드 영역: StackFrame 클래스의 정보와 createMethod() 메서드가 메서드 영역에 저장된다. 인스턴스가 여러 개 생성되어도 메서드는 메서드 영역에 단 한 개만 정의되어 있고, 메서드를 호출시 메서드 영역의 동일한 메서드가 호출된다.
후입선출 (LIFO): 나중에 넣은 것이 먼저 나오는 구조이다. 자바에서 메서드 호출 시 스택 구조를 사용하여, 메서드가 호출될 때마다 스택에 쌓이고, 종료되면 스택에서 제거된다.
선입선출 (FIFO): 먼저 넣은 것이 먼저 나오는 구조이다. 큐는 선착순 작업에 적합한 자료 구조이다.
스택에는 메서드 호출 시 생성되는 지역 변수가 저장되고, 힙에는 객체가 생성되어 저장된다.
class Item {
private int quantity;
public Item(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
public class MemoryExample {
public static void main(String[] args) {
System.out.println("main start");
initiateProcess();
System.out.println("main end");
}
static void initiateProcess() {
System.out.println("initiateProcess start");
Item item1 = new Item(5);
processItem(item1);
System.out.println("initiateProcess end");
}
static void processItem(Item item2) {
System.out.println("processItem start");
System.out.println("item.quantity = " + item2.getQuantity());
System.out.println("processItem end");
}
}

stack: main() 메서드의 스택 프레임 생성.stack: initiateProcess() 메서드의 스택 프레임 생성.heap: Item 객체 생성, quantity = 5 값 저장.stack: item1 참조 변수가 힙의 Item 객체를 참조.stack: processItem() 메서드의 스택 프레임 생성, item1 참조 변수가 item2로 전달됨.heap: Item 객체의 quantity = 5 값을 읽어옴.stack: processItem() 메서드의 스택 프레임 제거.stack: initiateProcess() 메서드의 스택 프레임 제거.stack: main() 메서드의 스택 프레임 제거.static 변수는 클래스 레벨에서 관리되며, 모든 인스턴스에서 공통으로 사용된다. 객체 생성과 상관없이 클래스명으로 접근할 수 있다.
public class Store {
public static int totalItems = 0; // static 변수
public Store() {
totalItems++; // 객체 생성 시마다 증가
}
}
이 코드에서 totalItems는 모든 Store 객체가 공유하는 값이며, 새로운 객체가 생성될 때마다 증가한다.
static 메서드는 객체를 생성하지 않고 클래스명으로 바로 호출할 수 있다.
주로 유틸리티 성격의 메서드에 사용된다.
public class MathHelper {
public static int square(int number) {
return number * number;
}
}
public class Main {
public static void main(String[] args) {
int result = MathHelper.square(5);
System.out.println("Square of 5: " + result);
}
}
Square of 5: 25
static 메서드는 인스턴스 생성 없이 바로 호출 가능하며, 프로그램 전체에서 공통적으로 사용되는 유틸리티 메서드에 적합하다.