메모리 구조
목차
- 메모리 구조
- 메모리
- 코드 영역
- 데이터 영역
- 힙 영역
- 스택 영역
- 오버플로우
- JVM 런타임 데이터 영역
- pc 레지스터
- 네이티브 메서드 스택
- 스택 영역
- 메서드 영역
- 힙 영역 (java 7이전 vs java8이후)
- 정리
메모리 구조
위 그림과 같이, 프로그램이 실행되기 위해서 운영체제(OS)가 프로그램의 정보를 메모리에 로드해야 한다.
그래야 메모리에 올라온 명령어와 데이터들을 CPU가 처리할 수 있다.
할당받은 대표적인 메모리 공간은 코드영역, 데이터 영역, 힙 영역, 스택 영역 으로 구성되어 있다.
🧐 그렇다면, 왜 이렇게 메모리 공간은 체계적으로 나뉘어져 있을까?
많은 이유와 이점이 있는데, 추려보았다.
- CPU가 메모리에 빠르게 접근하고, 프로그램이 효율적으로 실행되도록 한다.
- 한 프로그램이 다른 프로그램의 메모리를 침범하면 충돌/보안 문제가 발생할 수 있다. 이를 방지하기 위해 각 프로세스는 가상 메모리를 사용해 독립적인 주소 공간을 할당받는다.
- 실행 중인 프로그램이 자기 코드를 함부로 수정하는 것을 방지할 수 있다.
- 지역 변수(Stack)와 동적 할당된 객체(Heap)를 분리하면 메모리 지역성(Locality)을 유지하여 성능을 향상시킬 수 있다.
코드 영역
코드 영역은 실행할 프로그램의 코드가 저장되는 영역이다.
프로그램이 시작하고 종료될 때까지 메모리에 계속 남아있다.
데이터 영역
프로그램의 전역 변수와 정적 변수가 저장되는 영역이다.
프로그램이 시작하고 종료될 때까지 메모리에 계속 남아있다.
힙 영역
힙 영역은 프로그래머가 직접 공간을 할당, 해제하는 메모리 공간이다.
힙 영역에 메모리를 동적 할당 후 사용하고 난 후에는 반드시 메모리 해제를 해줘야 메모리 누수(memory leak)가 발생하지 않는다.
Java에서는 가비지 컬렉션을 통해 프로그래머가 관여하지 않아도 자동으로 메모리를 할당/해제 해준다.
메모리의 낮은 주소에서 높은 주소 방향으로 할당된다.
스택 영역
스택 영역은 지역 변수와 매개변수가 저장된다.
컴파일 타임에 크기가 결정되고 함수 호출과 함께 할당, 호출 완료시 소멸된다.
커널 영역을 보호하기 위해 메모리의 높은 주소에서 낮은 주소 방향으로 할당된다.
오버 플로우
한정된 메모리 공간이 부족하여 메모리 안에 있는 데이터가 넘쳐 흐르는 현상이다.
힙은 위쪽 주소부터 할당되고, 스택은 아래쪽 주소부터 할당되기 때문에 각 영역이 상대 공간을 침범하는 일이 발생할 수 있다.
스택 오버플로우
스택이 힙 영역을 침범해 나타나는 오버플로우
원인에는
- 무한 재귀 호출
- 함수가 호출될 때 매번 새로운 스택 프레임(그 함수만의 스택 영역을 구분하기 위해 생성되는 공간)이 스택 영역에 쌓이는데, 종료 조건 없이 계속 호출되면 스택 메모리를 초과하게 된다. => 재귀 함수 대신 반복문을 사용하여 스택 메모리 사용을 줄일 수 있다. 반복문은 힙 메모리나 CPU레지스터를 사용하기 때문!
- 지나치게 깊은 함수 호출 체인
- 너무 큰 로컬 변수 할당
- 함수 내부에서 너무 큰 크기의 배열이나 객체를 로컬 변수로 선언할 때 발생한다. 스택 메모리 크기를 JVM으로 설정해 스택 오버플로우를 예방할 수 있지만, 이는 근본적인 해결책이 아니다.
힙 오버플로우
JVM에서는 가비지 컬렉터가 힙에 할당되는 메모리에 대해 주기적으로 돌면서 확인하고 지우기 때문에 힙에 할당된 메모리가 자동으로 해제된다.
하지만 가비지 컬렉션에는 단점이 존재한다.
일단 가비지 컬렉터는 실시간으로 사용되지 않는 메모리를 지워주는 것이 아니다.
위에서 설명했듯이 주기적으로 돌면서 확인하고 지우는 것이기 때문에 사용되지 않는 메모리가 지워지지 않고 남아있을 수 있다.
따라서 가비지 컬렉터는 메모리 누수를 완벽하게 막지 못한다!
JVM의 메모리 영역
java의 변수 종류
메모리 영역에 올라가는 것 중 이해하기 가장 쉬운것인 변수이다.
간단하게 정리하고 넘어가자.
public class Hello{
public static int age = 20; //클래스 변수 (전역 변수)
int height = 60; //인스턴스 변수 (전역 변수)
public static void main(String[] args){//매개변수 (파라미터)
int size = 50; //지역변수
}
}
변수 종류 | 선언 위치 | 설명 | 생성 시기 | 소멸 시기 | 저장 메모리 |
클래스 변수 (static variable) |
클래스 영역 | static 키워드 붙고, 여러 객체에서 공통적으로 사용할 때 사용 |
클래스가 메모리에 올라갈때 | 프로그램 종료시 | static |
인스턴스 변수 (instance variable) |
클래스 영역에서 static이 아닌 변수 | 인스턴스가 생성될때 | 인스턴스 소멸시 | heap | |
지역 변수 (local variable) |
메서드 영역 | 메서드 내부에서 선언. | 블록 내에서 변수의 선언문이 실행될 때 | 블록을 벗어날 때 | stack |
JVM이란
간단하게 JVM에 대해 알고가자.
JVM(Java Virtual Method)의 약자이면 자바 가상 머신이다. 바이트코드를 해석하고 실행하는 역할을 한다.
.java 파일을 컴파일러로 .class파일로 변환한다.
.class 파일을 JVM의 ClassLoader에게 보낸다.
ClassLoader에서 JVM 런타임 영역으로 할당하여 메모리 영역에 올린다.
JVM의 런타임 데이터 영역은 자바 어플리케이션을 실행할 때 사용되는 데이터들이 저장되는 메모리 공간이다.
런타임 데이터 영역은 크게
- 메서드 영역
- 힙 영역
- 스택 영역
- PC 레지스터
- 네이티브 메서드 스택
으로 나뉘고,
Thread가 공유하는 영역과 공유하지 않는 영역은 다음과 같다.
- Thread가 공유하는 영역 (java)
- 힙 영역
- 메서드 영역
- Thread가 공유하지 않는 영역 (java)
- stack 영역
- PC 레지스터 영역
- 네이티브 메서드 스택
이제 하나씩 알아보자.
Thread가 공유하지 않는 영역
PC 레지스터
PC 레지스터 영역 은 각 스레드마다 현재 수행 중인 JVM 명령의 주소가 저장되는 영역이다.
네이티브 메서드 스택
네이티브 메서드들은 PC레지스터에 저장되지 않고, 네이티브 메서드 스택에 저장되어
자바 외부의 네이티브 코드를 호출할 때마다 생성되었다가 호출이 완료되면 사라진다.
** 네이티브 : Java가 아닌 다른 언어(c/c++)로 실행된 메서드를 의미한다.
스택 영역
메서드 호출 시 지역변수, 매개변수, 리턴값 이 저장되는 영역이다.
저장되는 데이터들은 하나의 메서드에 필요한 메모리 덩어리를 묶어서 스택 frame이라는 자료구조로 저장된다.
Thread가 공유하는 영역
메서드 영역
JVM의 메서드 영역은 일반적인 메모리 구조에서 코드 영역과 유사하다.
JVM이 시작될 때 생성되고, 클래스 로더에 의해 로드된 클래스 정보를 저장한다.
메서드 영역엔 JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, 생성자, static 변수를 저장한다.
메서드 영역에서 더이상의 데이터 저장 공간이 없으면 outofmemoryerror를 발생시킨다.
런타임 상수 풀 (Runtime Constant Pool)
런타임 상수 풀은 클래스 로더가 메서드 영역에 클래스를 로딩할 때,
클래스 별로 런타임 상수 풀을 가지고, 런타임 상수 풀에는 클래스 및 인터페이스의 상수 뿐만 아니라 메서드와 필드에 대한 모든 레퍼런스에 대한 정보를 가지고 있다.
힙 영역
객체와 배열이 저장되는 공간이다.
힙 영역은 스택영여과 다르게 보관되 데이터가 호출이 끝나더라도 삭제되지 않고 유지된다.
어떤 참조 변수도 힙 영역에 있는 인스턴스를 참조하지 않게되면, 가비지 컬렉터에 의해 메모리에서 청소된다.
정리 및 추가
final 멤버 변수에는 일반적으로 static을 붙인다.
private static final String NAME = "매실";
이 값은 변하지 않으므로, 인스턴스가 만들어질 때마다 새로 메모리를 잡고 초기화하지 않고, JVM메서드 영역에 컴파일시 한번에 메모리를 잡아 저장해 더 효율적으로 메모리를 쓸 수 있다.
Final 멤버 변수에 static을 사용하지 않는 경우
스프링 프레임워크의 의존성 주입이 해당된다.
spring bean은 기본적으로 어플리케이션 전체에서 해당 클래스의 인스턴스가 단 하나만 생성되게 관리된다.
따라서 불변성을 보장해주는 final키워드와 함께 생성자 주입을 사용하면, 의존성 객체가 한 번 주입된 후, 변경이 될 수 없게 된다.
🧐 그럼 왜 static을 사용하지 않을까 ?
다양한 구현체를 주입할 수 있고 런타임에 의존성을 동적으로 주입하기 위해서다.
public interface CustomerPreferenceDao {
List<String> getPreferredGenres(User user);
}
// 데이터베이스 구현체
public class DatabaseCustomerPreferenceDao implements CustomerPreferenceDao {
public List<String> getPreferredGenres(User user) {
// 데이터베이스에서 선호도 조회
}
}
// 메모리 구현체
public class InMemoryCustomerPreferenceDao implements CustomerPreferenceDao {
public List<String> getPreferredGenres(User user) {
// 메모리에 저장된 데이터에서 선호도 조회
}
}
// 테스트용 목 객체
public class MockCustomerPreferenceDao implements CustomerPreferenceDao {
public List<String> getPreferredGenres(User user) {
// 고정된 테스트 데이터 반환
}
}
참고
메모리의 구조 (코드, 데이터, 힙, 스택 영역)
목차 메모리 코드(code) 영역 데이터(data) 영역 힙(heap) 영역 스택(stack) 영역 오버 플로우 메모리 위 그림과 같이, 프로그램이 실행되기 위해서는 운영체제(OS)가 프로그램의 정보를 메모리에 로드
all-young.tistory.com
💾 [CS] 스택 오버플로우(Stack Overflow)란 무엇일까요?
💾 [CS] 스택 오버플로우(Stack Overflow)란 무엇일까요? 스택 오버플로우(Stack Overflow)는 프로그램이 사용 가능한 스택 메모리(Stack Memory) 영역을 초과하여 더 이상 데이터를 쌓을 수 없게 되는 현상을
www.devkobe24.com
[Java] 런타임 데이터 영역(Runtime Data Area)에 대해
자바 가상 머신(JVM)의 런타임 데이터 영역에 대해 알아보자
velog.io
[DI] 의존성 주입(Dependency Injection) 의 개념과 방법 및 장단점
의존성 주입의 역할 어떤 상황에서 필요할까? 우선 의존성에 대해 개념적으로 이해하기 위해 다음의 예시들을 생각해보자. [예시1] 집에서 베이킹을 하면 주방이 어질러지고 난리나니 케이크 배
velog.io
[왜 자바에서 final 멤버 변수는 관례적으로 static을 붙일까?]
왜 자바에서 final 멤버 변수는 관례적으로 static을 붙일까?
자바 final, static 키워드와 코딩 best practice 되짚어보기
djkeh.github.io
'기록 > 스프링' 카테고리의 다른 글
spring security , jwt 를 이용한 인증/인가 구현 (0) | 2025.03.10 |
---|---|
스프링이 클라이언트의 요청을 처리하는 전반적인 로직 (servlet , dispatcher servlet, interceptor, filter, aop) (0) | 2025.03.10 |
[Intellij/트러블슈팅] 프로젝트 폴더 안보임.. (0) | 2025.01.25 |