자바와 JVM의 메모리 관리
자바는 관리되는 언어(
managed language
)로, 자바 가상 머신(JVM) 위에서 실행되며 메모리 관리를 자동으로 수행한다. JVM은 메모리 할당과 가비지 컬렉션(GC)을 담당하여 개발자가 메모리 관레 드는 수고를 덜 수 있게 한다. 이는 메모리 누수를 줄이고, 프로그램의 효율성과 안정성을 향상시키는 데 도움을 준다.
자바의 메모리 관리가 중요한 이유
메모리 관리는 자바 프로그램의 성능과 안정성에 직접적인 영향을 미친다. 효율적인 메모리 관리는 실행 속도를 최적화하고, 자원을 절약하며, 시스템의 안정성을 보장한다. 반면, 부적절한 메모리 사용은 속도저하, 프로그램의 실패, 그리고 비효율적인 자원 사용으로 이어질 수 있다.
JVM 메모리 구조
JVM 메모리는 크게
Method Area
,Heap Area
,Stack Area
,Native Method Stack
으로 구분된다. 각 영역은 프로그램 실행에 있어 서로 다른 목적을 가지고, 다른 유형의 데이터를 저장한다.
우리가 JVM 메모리 구조를 알아야 하는 이유?
- 가지비 컬렉션(GC) 최적화
- GC는 불필요한 객체를 자동으로 정리하지만(Java, Python), 객체의 생명주기와 메모리 할당 패턴을 이해하면 불필요한 객체 생성을 피하고, 메모리 사용률을 최적화하여 GC 오버헤드를 줄일 수 있다. 이는 애플리케이션의 성능과 반응성을 직접적으로 향상시킨다.
- 스레드 관리 및 동기화
- JVM 내에서 스레드는 스택 메모리를 통해 각각의 실행 컨텍스트를 가진다. 스레드 사이의 올바른 동기화와 데이터 공유를 관리하기 위해서는 스택과 힙 메모리의 작동 방식을 이해해야 한다. 이는 멀티스레드 애플리케이션의 동시성 문제를 방지하고, 데이터 일관성을 유지하는데 중요하다.
- 성능 문제 진단 및 튜닝
- 메모리 사용에 대한 통찰력은 성능 관련 문제를 진단하고 해결하는 데 필수적이다. JVM의 메모리 구조를 이해하면
OutOfMemoryError
와 같은 메모리 문제를 해결하고, 애플리케이션의 성능을 튜닝하기 위한 기법을 마련할 수 있다.
Java의 메모리 관리 및 변수 유효 범위
- 클래스 변수(static 변수) :
클래스가 메모리에 로드될 때 생성
되며, 클래스의 모든 인스턴스에 의해 공유되며 이 변수들은메서드 영역에 저장
된다.- 인스턴스 변수 :
객체가 생성될 때 힙에 할당
되며, 각 객체 인스턴스에 대해 고유함- 지역 변수 :
메서드 내에서 선언되며, 메서드 호출 시 스택에 저장
된다. 메서드가 종료되면 이 변수들은 소멸한다.- 매개 변수 : 메서드 호출과 함께 전달되며, 메서드의 스택 프레임 내에 위치한다.
public class MemoryExample {
// 이 변수는 메서드 영역에 저장되는 정적 변수입니다.
static int staticVar = 10;
// 이 변수는 실행시 힙 영역에 저장되는 인스턴스 변수입니다.
int instanceVar;
// 생성자
public MemoryExample(int var) {
// 생성자의 매개변수는 스택에 저장됩니다.
this.instanceVar = var;
}
// 메서드 호출시 각 매개변수는 스택 영역에 저장됩니다.
public void show(int a) {
// 로컬 변수는 메서드 호출 시 스택 영역에 저장됩니다.
int localVar = a;
System.out.println("Static Variable: " + staticVar);
System.out.println("Instance Variable: " + instanceVar);
System.out.println("Local Variable: " + localVar);
}
public static void main(String[] args) {
// 객체 생성 구문 'new'는 힙 영역에 저장되는 인스턴스를 생성합니다.
MemoryExample example = new MemoryExample(5);
// 메서드 호출은 스택 영역에 프레임을 생성합니다.
example.show(20);
}
}
변수 유형 | 선언 위치 | 설명 |
---|---|---|
클래스 변수 | 클래스 영역 | static 키워드를 사용하여 클래스 영역에 선언된 변수입니다. 모든 인스턴스가 공유합니다. |
인스턴스 변수 | 클래스 영역 | static 이 아닌 변수로, 객체 생성시 힙에 할당됩니다. 각 인스턴스마다 별도로 존재합니다. |
지역 변수 | 메서드 내 | 메서드 내에서 선언되고 메서드가 실행될 때 스택에 존재합니다. 메서드 실행이 끝나면 사라집니다. |
매개 변수 | 메서드 선언부의 괄호 내 | 메서드 호출 시 전달되는 값으로, 메서드 스택 프레임 내에 존재합니다. 메서드 호출 시 사용됩니다. |
Method(Static) 영역
Method 영역 또는 Static 영역은 JVM이 프로그램의 런타임 데이터를 저장하는 곳 중 하나로, 모든 스레드에 의해 공유되는 메모리 영역이다. 이 공간에는 클래스와 인터페이스의
메타데이터
, 실행되는클래스의 바이트코드
,상수(static final)
,정적(static) 변수
가 저장된다. 여기에는 생성자 코드와 모든 메서드의 코드도 포함되며, 이는 클래스 또는 객체가 아닌 클래스 자체와 연결된다. Method 영역은 가비지 컬렉션의 대상이긴 하지만, 힙 영역과 비교하여 가비지 컬렉션이 수행되는 빈도는 상대적으로 낮다.
클래스 변수(정적 변수)는 클래스가 JVM에 로드될 때 생성되어 Method 영역에 저장된다. 이러한 변수들은 해당 클래스의 모든 인스턴스에 의해 공유되며, 클래스가 언로드될 때까지 유지된다. 또한, 모든 클래스의 메서드 코드도 이 영역에 저장되므로, 모든 인스턴스와 스레드가 같은 메서드 코드를 공유할 수 있다.
static
키워드는 특정 필드나, 메서드, 블록을 클래스의 인스턴스가 아니라 클래스 자체와 연결시키는 데 사용된다.static
변수는 힙 영역의 객체와는 독립적으로 Method 영역에 저장되기 때문에 클래스의 모든 인스턴스가 같은 메모리를 참조하여 사용할 수 있다. 이는 메모리 사용량을 줄이는 데 도움이 될 수 있지만, 동시에 데이터가 공유되므로 스레드 안정성에 주의를 기울여야 한다. 또한, static 메서드는 인스턴스를 생성하지 않고도 호출할 수 있으며, 클래스 로더에 의해 클래스가 로드될 때 메서드 영역에 저장되어 프로그램 전체에서 접근 가능하다.
public class Main {
public static void main(String[] args) {
int a = 100;
a = wow(a);
}
public static int wow(int num) {
int b = num * 4;
return b;
}
}
Stack 영역
스택 영역은 각 스레드에 의해 사용되는 메모리 부분으로, 메서드 호출과 지역 변수 할당에 사용된다. 이 영역은
스레드마다 별도로 존재
하며, 메서드 호출 시 생성되는 스택 프레임을 저장하는 데 사용된다.
스택 영역은 메서드가 호출될 때마다 스택 프레임을 쌓는 방식으로 동작한다. 각 스택 프레임은 호출된 메서드의 매개변수, 지역변수, 반환 값 및 연산 중간 결과 등을 포함한다. 메서드가 종료되면 해당 스택 프레임은 스택에서 제거되고, 메모리가 반환된다. 이 메모리 할당 및 반환 프로세스는 매우 빠르며, 스택 포인터의 이동만으로 관리된다.
스택 프레임은 특정 메서드의 호출과 실행에 필요한 정보를 포함하는 데이터 블록이다. 메서드가 호출되면 하나의 스택 프레임이 스택 영역에 할당되고, 이는 해당 메서드의 매개변수, 지역변수, 리턴 주소, 그리고 연산에 필요한 다른 정보들을 포함한다.
스택 프레임 내에서 지역변수와 매개변수는 메서드가 실행될 때 할당되고, 메서드가 종료될 때 스택에서 제거된다. 이들은 메서드가 활성 상태일 때만 존재하며, 메서드가 종료되면 이들의 스코프(유효범위)가 끝나고, 해당 스택 프레임은 스택에서 제거되어 메모리가 해제된다.
스택은
Last In, First Out(LIFO)
의 특성을 가지고 있다.가장 마지막에 들어간 요소가 가장 먼저 나온다
. 이 특성은 메서드 호출과 반환에 따라 스택 프레임이 추가되고 제거되는 방식에 적용된다. 스택의 LIFO 특성은 메서드 호출 순서와 실행 흐름을 관리하는 데 중요하며, 프로그램의 스코프를 정의하는 데에도 사용된다. 각 메서드의 스코프 내에서 선언된 지역 변수는 해당 스코프 안에서만 존재하며, 스코프를 벗어나면 더 이상 접근할 수 없다.
💡 따라서, wow 스택 프레임은 스택에서 제거(pop)되며,
main
메서드의 스택 프레임으로 돌아간다. main 메서드의a
변수에wow
메서드로부터 반환받은 값400
이 할당된다.
Heap 영역
자바 메모리 관리의 핵심 부분으로, 동적으로 할당되는 모든 객체와 배열의 저장소이다. 즉, 프로그램이 실행되는 동안 필요에 따라
new
키워드를 통해 객체가 생성되면, 이 객체들은 힙 영역에 저장된다. 객체가 더 이상 필요하지 않게 되면( 어떠한 참조 변수도 그 객체를 가르키지 않게 되면), 가비지 컬렉션(GC)에 의해 자동으로 메모리에서 해제된다.
힙 영역의 중요한 특징 중 하나는 그것이 프로그램 전체의 실행 동안 지속되고 모든 스레드에 의해 공유된다는 것이다. 이는 객체의 생명주기가 스택 메모리에서와 같이 메서드 호출의 시작과 종료에 의존하지 않음을 의미한다. 대신, 객체는 프로그램의 실행 흐름과 애플리케이션 로직에 따라 생성되고 유지된다.
가비지 컬렉션(Garbage Collection, GC)은 힙 메모리 영역에서 사용되지 않는 객체를 자동으로 검색하고, 해제하는 JVM의 프로세스이다. 이는 메모리 누수를 방지하고, 프로그램이 메모리를 더 효율적으로 사용할 수 있도록 도와준다.
package programmers;
public class Main {
public static void main(String[] args) {
Counter c = new Counter();
}
}
class Counter {
private int state = 0;
public void increment() {
state++;
}
public int get() {
return state;
}
}
package programmers;
public class Main {
public static void main(String[] args) {
Counter c = new Counter();
two(c);
}
public static void two(Counter c) {
c.increment();
c.increment();
}
}
class Counter {
private int state = 0;
public void increment() {
state++;
}
public int get() {
return state;
}
}
💡 두 가지의 예제에서 강조되는 부분은 호출되는 메서드가 파라미터로 객체값을 전달받아 객체의 상태를 변경하게되면, 메서드 종료(스택 제거) 이후에도 힙 영역에 있는 객체의 상태는 쭉 유지된다는 점이다.
main의 마지막 코드가 실행되면 main 스택 프레임은 스택 영역에서 제거된다.
- 스택 영역은 메서드의 끝을 알리는 닫는 중괄호
}
를 만나면 자동으로 메모리에서 제거된다.- 그러나 Heap 영역에는 여전히 객체 데이터가 메모리에 상주하게 된다.
가비지 컬렉션(GC)가 Heap 영역을 청소한다.
- 가비지 컬렉션은 힙 영역에 참조되지 않고 남아 있는 객체들을 식별해 힙 영역을 청소한다.
👉🏻 이는 개발자가 직접 수행하지 않고, JVM이 자동으로 수행한다. GC가 언제 발생할지는 예측할 수 없으며, JVM에 의해 결정된다.- 추가로 코드 실행이 모두 끝나면 Method(Static) 영역도 비워지게 된다.
Stack 메모리와 Heap 메모리의 차이점
구분 | Stack 메모리 | Heap 메모리 |
---|---|---|
작동 방식 | LIFO 원칙, 메서드 실행 시 생성 및 종료 시 해제 | 동적 할당, 가비지 컬렉션에 의한 메모리 관리 |
저장 데이터 | 지역 변수, 매개변수 | 객체, 참조형 변수 |
메모리 관리 | 컴파일 시 결정, 자동 할당 및 해제 | 런타임 시 결정, 수동 할당 및 자동 해제 |
접근 속도 | 빠름 | 상대적으로 느림 |
스레드 공유 | 각 스레드별 독립적 | 모든 스레드가 공유 |
메모리 한계 | 제한적, StackOverflowError 가능성 | 확장 가능, OutOfMemoryError 가능성 |