들어가며
이번 포스트에서는 Java가 메모리를 어떤식으로 활용하는지에 대한 이야기를 정리해보려고 합니다.
자세하게 생각하면 끝도 없는 영역이기 때문에 아 이런식으로 동작하는 구나 정도의 수준의 정리 내용입니다.
언어들은 메모리를 어떻게 쓸가?
Java와 같이 컴파일러를 통해 기계어를 만들어 사용하는 언어들은 아래 그림처럼
코드를 실행하는 영역과 데이터를 저장하는 영역으로 나누어 메모리를 할당해 사용합니다.
자바의 경우에 데이터를 저장하는 방법은 크게 3가지로 나누어 이야기할 수 있습니다.
Java가 최초 저장할때는 해당영역을 사용합니다.
더 정확히 구분하면
Method Area 와 Static Area로 나눌 수 있습니다.
Method Area vs Static Area?
메서드 영역(Method Area):
- 모든 스레드가 공유하는 영역으로, 클래스 파일의 바이트 코드, 정적 변수(static variable), 상수(Constant), 메서드 코드 등이 저장됩니다.
- 클래스 로더(Class Loader)에 의해 클래스 파일이 로드되면 해당 클래스의 정보가 메서드 영역에 저장됩니다.
스태틱 영역(static Area):
- 객체가 생성되기 전에 이미 메모리에 할당되는 영역으로, 모든 인스턴스가 공유하는 데이터를 저장합니다. 클래스 변수(static variable)가 static 영역에 저장됩니다
결국 이전 포스트에서 이야기했던 .class를 저장하는 영역으로 보는지 혹은 객체 이전에 생성된 모든 인스턴스가 공유하는 저장 공간을 의미 하는지의 차이로 생각합니다.
표로 정리하면 다음과 같습니다.
구분 Static 영역 Method 영역
저장 데이터 | 클래스 정보, 상수 풀, 클래스 변수, 메서드 영역 참조 | 메서드 코드, 로컬 변수 |
접근 방식 | 클래스 이름으로 직접 접근 | 객체를 통해 접근 |
메모리 할당/해제 | 프로그램 시작/종료 | 클래스 로딩/언로드 |
주의 사항 | 메모리 누수 가능성 | 클래스 참조 존재 시 메모리 누수 가능성 |
또한, 여기서 클래스변수 와 정적변수가 같은 영단어를 쓰는 걸 볼 수 있는데
공부해본 바로는 다음과 같습니다.
- 클래스 변수(Class Variable): 클래스 변수는 객체 간에 공유되는 변수를 의미하며, 객체를 생성하지 않고도 클래스 이름을 통해 접근할 수 있습니다. 클래스 변수는 일반적으로 static 키워드로 선언됩니다.
- 정적 변수(Static Variable): 정적 변수는 객체에 속하지 않는 변수를 의미하며, 클래스 내부에서만 사용되는 변수를 일컫습니다. 따라서 객체를 생성하지 않고도 클래스 내부에서 직접 접근할 수 있습니다. 클래스 변수와 유사하지만, 클래스 변수가 반드시 static 키워드로 선언되어야 하는 반면, 정적 변수는 그렇지 않을 수 있습니다.
정리하면 결국 같은 의미입니다. 특정 클래스의 변수다 라는 의미를 강조하기 위해 클래스 변수라는 개념을 사용하는 것인데 같은 의미로 두고 봐도 무방하다고 생각합니다.
Stack Area && Heap Area
- 힙 영역(Heap Area):
- 동적으로 생성된 객체와 배열이 저장되는 영역입니다.
- 힙 영역은 가비지 컬렉션(Garbage Collection)에 의해 관리되며, 더 이상 사용되지 않는 객체는 메모리에서 해제됩니다.
- 스택 영역(Stack Area):
- 각 스레드마다 별도로 할당되는 영역으로, 메서드 호출 시 생성되는 지역 변수(local variable), 매개변수(parameter), 메서드 호출 스택(method call stack) 등이 저장됩니다.
- 메서드 호출 시 해당 메서드의 스택 프레임이 스택 영역에 생성되며, 메서드 실행이 완료되면 스택 프레임이 제거됩니다.
정리하면 힙은 객체를 저장하는 공간 , 스택은 로컬 변수와 매개 변수 , 메서드를 호출하는 스택이 저장됩니다.
정리해보면 이런식으로 볼 수 있습니다.
변수나 메서드 조건문 등이 하나의 Stack의 들어와 작동하게 됩니다.
하지만 항상 존재하는 것은 아니고 사용 후 에는 해당 메모리를 비워줍니다.
이렇게 해서 메모리의 효율성을 보장할 수 있게 됩니다.
그리고 계산된 결과 중 이후 사용이 있는 데이터들에 대해서는 캐시에 저장하여 더욱 높은 효율성을 볼 수 있게 합니다.
이외에도 Pc레지스터와 네이티브 메서드 스택이 존재합니다.
Pc레지스터 && 네이티브 메서드
- PC 레지스터(Program Counter Register):
- 스레드마다 별도로 할당되는 영역으로, 현재 실행 중인 명령어의 주소를 가리키는 포인터입니다.
- JVM이 다음에 실행할 명령어의 주소를 가리키며, 스레드가 실행 중인 동안에만 유효합니다.
- 네이티브 메서드 스택(Native Method Stack):
- 자바 언어가 아닌 다른 언어로 작성된 네이티브 코드(native code)를 실행하기 위한 스택 영역입니다.
- JNI(Java Native Interface)를 통해 자바 언어와 네이티브 언어 간의 상호 작용을 지원합니다.
이런 방법을 통해 자바가 메모리를 관리하고 있다는 것을 알 수 있습니다.
Heap Area 에 대한 고찰
힙 공간(Heap Space)은 동적으로 생성된 객체와 배열이 저장되는 메모리 영역입니다.
자바에서는 new 키워드를 사용하여 객체를 생성할 때 힙 영역에 메모리를 할당하고,
이 객체는 가비지 컬렉션(Garbage Collection)에 의해 관리됩니다.
힙 영역은 JVM(Java Virtual Machine)이 시작될 때 생성되며, 프로그램이 종료될 때까지 유지됩니다
만약 C로 동적 프로그래밍을 했다면? 포인터연산을 통해 지정하며 코딩해야 합니다.
예시코드를 보며 설명해보자면
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
public class Main {
public static void main(String[] args) {
// 힙 영역에 Person 객체를 생성하여 메모리를 할당합니다.
Person person1 = new Person("Alice", 25);
Person person2 = new Person("Bob", 30);
// 생성된 객체의 메서드를 호출하여 정보를 출력합니다.
System.out.println("Person 1: " + person1.getName() + ", " + person1.getAge() + " years old");
System.out.println("Person 2: " + person2.getName() + ", " + person2.getAge() + " years old");
}
}
이런 식으로 힙 영역에 메모리를 할당해 새로운 객체 (동적으로 생성한 객체)를 저장하고 사용할 수 있습니다. 그런데 궁금한점이 생겼습니다. 자료구조들은 어떻게 저장할까?
기본적으로 String 이라는 것 , 문자열(String)은 불변(immutable) 객체로 취급되며, 메모리 상에 생성된 문자열은 변경할 수 없습니다.
다른 자료구조들인 정수(int, long, short, byte), 부동 소수점(float, double), 문자(char), 논리(boolean) 등과는 차이점이 분명히 존재합니다.
String에 대하여
위 언급한 String은 문자”열” 입니다. 문자를 나열한 정보를 저장해두는 자료구조입니다.
자바는 이전에 말씀드린 데이터를 객체로 만드는 것을 지향합니다.
따라서 기본 데이터 유형(primitive data types)들 과 String(Object)은 분명한 차이점이 존재하게 됩니다.
따라서 문자열을 생성하면 해당 문자열은 힙 영역에 저장되고, 이후에 변경되지 않습니다.
public class StringExample {
public static void main(String[] args) {
// 문자열 리터럴을 사용하여 String 객체 생성
String str1 = "Hello";
// 새로운 String 객체 생성
String str2 = new String("World");
// 문자열 연결 연산으로 새로운 String 객체 생성
String str3 = str1 + str2;
// 문자열 상수를 직접 가리키는 경우 동일한 문자열 리터럴은 동일한 메모리 주소를 참조
String str4 = "Hello";
// 문자열 내용이 같더라도 새로운 객체가 생성됨
String str5 = new String("Hello");
// String 객체가 힙 영역에 저장되고 있는지 확인하기 위해 hashCode() 메서드 호출
System.out.println("str1 hashCode: " + str1.hashCode());
System.out.println("str2 hashCode: " + str2.hashCode());
System.out.println("str3 hashCode: " + str3.hashCode());
System.out.println("str4 hashCode: " + str4.hashCode());
System.out.println("str5 hashCode: " + str5.hashCode());
}
}
실행시켜보면 모두 다른 주소를 사용하고 있다는 것을 확인할 수 있습니다.
primitive data types 는 어디에 저장될까?
기본 데이터 유형(primitive data types)은 일반적으로 스택(stack) 영역에 저장됩니다.
기본 데이터 유형은 정수(int, long, short, byte), 부동 소수점(float, double), 문자(char), 논리(boolean) 등이 있습니다.
스택 영역은 지역 변수(local variable) 및 메서드 호출 시 생성되는 프레임(frame)을 저장하는 영역으로, 메모리의 상대적으로 빠른 공간을 활용합니다.
public class PrimitiveExample {
public static void main(String[] args) {
// 정수형 변수 선언 및 초기화
int intValue = 10;
// 부동 소수점 변수 선언 및 초기화
double doubleValue = 3.14;
// 문자 변수 선언 및 초기화
char charValue = 'A';
// 논리형 변수 선언 및 초기화
boolean booleanValue = true;
// 스택 영역에 저장된 변수 값 출력
System.out.println("intValue: " + intValue);
System.out.println("doubleValue: " + doubleValue);
System.out.println("charValue: " + charValue);
System.out.println("booleanValue: " + booleanValue);
}
}
위의 코드에서 intValue, doubleValue, charValue, **booleanValue**는 모두 스택 영역에 저장되는 변수입니다.
이들 변수는 메서드 내에서 사용되는 지역 변수로써, 메모리가 할당된 스택 프레임에 저장됩니다.
따라서 이들 변수는 해당 프레임이 소멸될 때 함께 소멸되며, 스택 프레임이 소멸되면 해당 변수가 사용되는 메모리 공간도 함께 해제됩니다.
정리해보면
Object 는 Heap Area 에서 관리한다. 그리고 이걸 관리하는 Garbage Collection이 있다.
Garbage Collection
Garbage Collection(가비지 컬렉션)은 프로그램 실행 중에 더 이상 사용되지 않는 메모리를 자동으로 해제하는 자바의 기능입니다.
이는 프로그래머가 명시적으로 메모리 관리를 수행할 필요가 없게 해줍니다.
수행 방식에 대해 정리해보면
- 객체 생성: 프로그램에서 객체가 생성되면 힙(heap) 영역에 메모리가 할당됩니다. 객체는 생성될 때마다 힙 영역의 어딘가에 저장되고, 해당 객체를 참조하는 변수가 있다면 그 변수에 객체의 참조(주소)가 저장됩니다.
- 객체 사용: 프로그램이 실행되는 동안 객체는 사용되며, 해당 객체에 대한 참조가 계속 유지됩니다.
- 참조 해제: 객체를 참조하는 변수가 더 이상 존재하지 않으면 해당 객체에 대한 참조는 끊어지며, 이 때 객체는 가비지(garbage)로 간주됩니다.
- 가비지 컬렉션 실행: JVM은 주기적으로 또는 필요할 때마다 가비지 컬렉션을 실행하여 힙 영역에서 더 이상 사용되지 않는 객체를 탐지하고 제거합니다.
- 메모리 해제: 가비지 컬렉션에 의해 탐지된 가비지 객체들은 메모리에서 해제되고, 해당 메모리 공간은 다시 사용 가능한 상태가 됩니다.
객체를 생성 , 사용 → 참조 해제 → 가비지 컬렉션이 돌아다니며 객체를 탐지 제거 → 메모리 해제
이렇게 순서대로 이해할 수 있습니다.
장점은 당연히 해야할 일이 적어지고 이전 포스트에서도 이야기 하였듯이
사람이 메모리를 명시적으로 할당하다 발생하는 사고에 대해 미연의 방지하는
메모리 누수를 막을 수 있습니다.
단점은 가비지 컬렉션이 일단 실행되려면 프로그램을 일시적으로 일시정지해야합니다.
왜냐면 아직 사용 중 인데 해제시키면 안되니까요
즉, 프로그램이 동작하는데 소요되는 시간이 증가하게 됩니다.
따라서 성능을 개선하기 위해 여러 기법들이 고려되어야 합니다.
- 메모리 할당 최적화: 객체를 생성할 때 메모리를 적게 사용하도록 최적화하는 것이 중요합니다. 이는 객체의 크기를 최소화하거나, 불필요한 객체 생성을 피하고, 객체 풀링(Object Pooling)과 같은 기술을 사용하여 객체의 재사용을 촉진하는 것을 의미합니다.
- GC 튜닝: JVM에서 Garbage Collection 알고리즘 및 설정을 조정하여 성능을 최적화할 수 있습니다. 이는 Young Generation과 Old Generation의 비율 조정, GC 알고리즘 선택, GC 동작 주기 조절 등을 포함합니다. 예를 들어, GC 알고리즘을 G1GC로 변경하거나, GC 동작 주기를 조정하여 응용 프로그램의 요구에 맞게 설정할 수 있습니다.
- 메모리 누수 감지: 메모리 누수를 방지하고 탐지하는 것이 중요합니다. 메모리 누수는 가비지 컬렉션의 성능을 저하시킬 수 있습니다. 따라서 메모리 프로파일러(memory profiler) 및 메모리 분석 도구를 사용하여 메모리 누수를 식별하고 해결해야 합니다.
- 간접적인 참조 제거: 불필요한 객체에 대한 간접적인 참조를 제거하는 것이 중요합니다. 예를 들어, 캐시를 사용할 때 캐시에 저장된 객체에 대한 간접적인 참조를 정리하는 것이 필요합니다.
- 멀티 스레딩 및 병렬 처리: 가비지 컬렉션 작업을 멀티 스레드로 실행하거나 병렬로 처리하여 성능을 향상시킬 수 있습니다. 예를 들어, Parallel GC(병렬 GC) 또는 G1GC(Garbage First GC)와 같은 병렬 가비지 컬렉션 알고리즘을 사용하여 가비지 컬렉션 작업을 여러 CPU 코어에서 동시에 처리할 수 있습니다.
보기만 해도 어려운 이야기가 많습니다만
정리하면 그렇게 어렵지 않습니다.
그냥 메모리 최적화를 위한 Polling , GC에서 사용하는 알고리즘 , 누수 감지 , 간접참조 제거 , 멀티스레딩
입니다. (알고리즘은 알고리즘 관련 포스트를 올릴 예정이라 거기서 다루어 보겠습니다.)
Object Pooling
여기서 객체 풀링(Object Pooling) 이란 컴퓨터관련 전공을 한사람들은 한번 쯤 어디선가 들어봤을법한 풀(pool)에 저장하고 필요할 때마다 풀에서 가져와서 쓰는 방법입니다.
필자가 기억하기로는 SQL , Network에서 연결관리 할때 사용했던것으로 기억합니다.
어쨋든 정말 많이 사용되는 기법인데
여기서 객체 풀링이란 ,많은 객체를 반복적으로 생성하고 제거하는 대신, 객체를 미리 생성하여 풀(pool)에 저장하고 필요할 때마다 풀에서 객체를 가져와 사용하는 기법을 말합니다.
(자세한 이야기는 다른 포스트에서 하겠습니다.)
정리하며
이번 포스트에서는 자바가 메모리를 어떻게 관리하는지에 대한 이야기를 정리해봤습니다.
항상 느끼는거지만 이런 Low레벨에서 개발하시는 하드웨어나 OS개발자분들이 정말 대단한거 같습니다. 공부할때마다 어렵고 이해가 안되는 부분이 계속 나오는 걸 보면요.
간단하게 자바 메모리 사용의 대한 정보를 정리하면서 또 공부해보고 싶은 이야기들도 생겨서 재미있었습니다. (Gc튜닝 같은 부분들..)
다음 포스트에서는 자바의 스레드의 대한 이야기를 해보려고합니다.
'JAVA' 카테고리의 다른 글
JAVA-OverLoding & OverRiding (0) | 2024.05.29 |
---|---|
JAVA-스레드 (0) | 2024.05.28 |
JAVA - 구동과정 (0) | 2024.05.27 |
JAVA -객체 와 동작 (0) | 2024.05.27 |
JAVA - 소개 (0) | 2024.05.27 |