Java의 가비지 컬렉션(GC) 메커니즘은 자동으로 메모리를 관리하긴 하지만, 그 과정에서 발생할 수 있는 성능 저하를 최소화하고 고급 기능을 최적화 하기 위해서는 메모리 구조에 대한 지식이 필요하다.
1. 메모리 누수나 다른 메모리 관련 문제들은 애플리케이션의 안정성과 성능에 큰 영향을 미칠 수 있다.
2. 메모리 구조에 대한 이해는 이러한 문제들을 효과적으로 진단하고 해결하는 데 도움을 준다.
3. 예를 들어, 특정 객체가 왜 가비지 컬렉션의 대상이 되지 않는지, 혹은 왜 특정 시점에 메모리 사용량이 급증하는지 분석할 수 있다.
4. 고급 Java 기능들, 예를 들어 멀티스레딩, 동시성 컨트롤 등을 사용할 때, 각 기능이 메모리에 어떤 영향을 미치는지 이해하는 것이 중요하다.
JVM 메모리 구조
5가지 영역이 있으며 Method, Heap 영역은 하나만 존재하고,
Stack, PC, Native Method 영역은 Thread마다 존재한다.
이 중 가장 핵심적인 역할을 하는 Method, Heap, Stack 영역에 대해서 먼저 살펴보자.
메서드 영역
1. 클래스 로더에 의해 로드된 각 클래스와 인터페이스에 대한 메타데이터가 저장된다.
2. 이 영역은 모든 스레드가 공유한다.
저장되는 데이터 종류
● 클래스 정보
1. 클래스 이름, 필드 이름, 데이터 타입, 접근 제어자 정보, 메서드 이름, 리턴 타입, 매개변수 등이 저장된다.
2. 또한 메서드의 바이트 코드, 즉 JVM이 실행할 수 있는 기계어 수준의 코드도 저장된다.
● static 변수
클래스 레벨의 정적 변수가 저장되며 이 변수들은 프로그램 실행 동안 초기화되고 모든 인스턴스에서 공유된다.
● 런타임 상수 풀
1. 프로그램 실행 시 필요한 공통 리터럴 상수를 저장한다.
2. 공통 리터럴 상수는 프로그램 코드에서 사용되는 리터럴 값들(예: 숫자 42, 문자열 "Hello")을 의미한다.
3. Java 컴파일러는 같은 값을 갖는 리터럴 상수들을 메서드 영역의 런타임 상수 풀에 통합하여 저장한다.
4. 예를 들어, 여러 클래스에서 "Hello"라는 문자열 리터럴을 사용하는 경우, 이 문자열은 런타임 상수 풀에서 하나의 항목으로만 존재하게 된다.
5. 이러한 최적화를 통해 메모리 사용을 줄이고, 동일한 리터럴 값에 대한 참조가 중복으로 생성되는 것을 방지한다.
스택 영역
1. 각 스레드마다 하나씩 존재하는 메모리 영역으로, 스레드의 실행에 필요한 메소드의 호출 정보를 저장한다.
2. 각 메소드 호출은 스택 프레임이라는 블록을 생성하며, 이 블록에는 지역 변수, 중간 연산 결과, 메소드 호출 정보 등이 저장된다.
3. 각 메소드가 종료될 때마다 해당 스택 프레임은 스택에서 제거된다.
힙 영역
1. 런타임 데이터 영역으로, 모든 클래스 인스턴스와 배열이 할당되는 곳이다.
2. 애플리케이션의 메모리 사용량과 성능에 직접적인 영향을 미치며 힙의 관리 방식은 메모리 누수 및 효율적인 메모리 사용을 결정하는 데 핵심적인 역할을 한다.
3. 가비지 컬렉션의 주요 대상이며 더이상 참조되지 않는 객체는 가비지 컬렉션에 의해 제거된다.
힙 메모리에는 객체의 실질적인 데이터가 저장되며 메서드 코드는 메서드 영역에 존재한다.
1. Data 클래스로 인스턴스 100개를 생성하면 힙 메모리에 100개의 인스턴스가 생긴다.
2. 각각의 인스턴스는 내부에 변수와 메서드를 가진다.
3. 같은 클래스로 부터 생성된 객체라도 인스턴스 내부의 변수 값은 서로 다를 수 있지만, 메서드는 공통된 코드를 공유한다.
4. 따라서 객체가 생성될 때, 인스턴스 변수에는 메모리가 할당되지만, 메서드에 대한 새로운 메모리 할당은 없다.
5. 메서드는 메서드 영역에서 공통으로 관리되고 실행된다.
6. 정리하면 인스턴스의 메서드를 호출하면 실제로는 메서드 영역에 있는 코드를 불러서 수행한다.
스택 영역과 함수 호출
예제
public class Main {
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");
}
}
실행 결과
과정
1. 처음 자바 프로그램을 실행하면 main()이 실행된다.
2. 이 때 main()을 위한 스택 프레임이 하나 생성되고 내부에 args 매개변수를 가진다.
3. main()은 method1()을 호출한다.
4. method1() 스택프레임이 main() 스택 프레임 위에 생성된다.
5. method1()에는 m1, cal 지역변수를 가지므로 스택프레임에 포함된다.
6. method1()은 method2()를 호출하고 method1() 위에 스택 프레임이 생성된다.
7. method2()는 m2 지역 변수를 가진다.
8. method2()가 종료되면 method2() 스택프레임이 제거되고 지역변수 m2도 제거된다.
9. 프로그램은 method1()으로 돌아가며 method2()를 호출한 지점으로 돌아간다.
10. method1()이 종료되고 스택 프레임이 제거되며 지역변수 m1, cal도 제거된다
11. 이 후 main()으로 돌아가고 main()이 종료되면 스택 프레임이 완전히 비워지고 자바는 프로그램을 정리하고 종료한다.
PC 레지스터
1. 현재 스레드가 실행중인 JVM 명령의 주소를 가리키는 레지스터이다.
2. 각 스레드는 자신만의 PC 레지스터를 가진다
3. 스레드가 Native Method를 실행할 때는 PC 레지스터의 값은 정의되지 않는다.
4. Native Method는 JVM이 관리하는 영역 밖에서 실행된다.
네이티브 메소드 스택
1. 네이티브 메소드 스택은 Java가 아닌 다른 언어로 작성된 메소드(예: C, C++)의 호출과 실행을 관리하는 스택이다.
2. JVM 내에서 Java 메소드와는 별개로 운영되며, Java가 네이티브 코드와 상호 작용할 때 사용된다.
3. 이 스택은 각 스레드별로 존재하며, 네이티브 메소드의 호출 정보, 지역 변수, 중간 결과 등을 저장한다.
4. Java는 주로 안전하고 이식성이 높은 코드 실행을 목표로 하지만, 시스템 레벨의 작업이나 플랫폼 특화 기능을 수행하기 위해 네이티브 코드를 필요로 할 때가 있다.
'java > basic' 카테고리의 다른 글
생성자 (0) | 2024.04.13 |
---|---|
자바 클래스, 객체, 인스턴스 (0) | 2024.03.31 |