[JAVA] 메모리 구조와 static
런타임 데이터 영역(Runtime Data Area)
- 메서드 영역 : 클래스 정보를 보관, 이 클래스 정보가 붕어빵 틀이다.
- 스택 영역: 실제 프로그램이 실행되는 영역, 메서드를 실행할 때 마다 하나씩 쌓임
- 힙 영역: 객체(인스턴스)가 생성되는 영역, 붕어빵틀로 생성된 붕어빵이 존재하는 공간 (new 명령어, 배열)
- 메서드 영역: 프로그램을 실행하는데 필요한 공통 데이터 관리, 프로그램의 모든 스레드가 공유
-
static 영역: 메서드 영역의 일부로,
static
으로 선언된 변수나 메서드가 저장되는 공간. 인스턴스 없이도 접근 가능예시:
static int count = 0;
→ 클래스가 로딩될 때 한 번만 생성되고 모든 객체가 공유. -
런타임 상수 풀: 문자열 리터럴, 숫자 상수 등 변하지 않는 값 저장
예시:
String s1 = "hello"; String s2 = "hello";
→ 문자열 리터럴"hello"
는 상수 풀에 한 번만 저장되고 재사용됨.
-
- 스택 영역: 메서드 호출 시마다 생성되는 프레임이 저장되는 공간
-
스택 프레임: 스택 영역에 있는 박스가 하나의 스택 프레임이다. 메서드를 호출할 때 마다 하나의 스택 프레임이 쌓이고, 메서드가 종료되면 해당 스택 프레임이 제거된다(LIFO 구조)
1 2 3
void foo() { int x = 10; // x는 스택에 저장됨 }
-
-
힙 영역: 객체(인스턴스)가 저장되는 공간 (new로 생성된 객체, 배열 등) 모든 스레드가 공유하고 GC가 이루어지는 주요 영역
1
Person p = new Person(); // p는 스택, new Person()은 힙에 저장
메서드 코드는 메서드 영역에
메서드는 메서드 영역에 한 번만 로딩되며, 모든 인스턴스는 해당 메서드 코드를 공통으로 참조하여 사용한다.
따라서 객체를 생성할 때마다 메서드에 대해 새로운 메모리가 할당되는 것이 아니라, 메서드 영역에 저장된 공통 메서드 코드를 그대로 사용한다.
변수(필드) → 인스턴스마다 개별 저장 (힙 영역)
메서드 → 클래스당 한 번만 메모리에 로딩 (메서드 영역)
메서드는 공유, 변수는 독립적
스택(Stack)과 큐(Queue) 자료 구조
스택 (Stack)
-
후입선출 (LIFO, Last In First Out)
→ 나중에 넣은 데이터가 먼저 나온다
- 예시:
- 1 → 2 → 3 순서로 넣으면
- 3 → 2 → 1 순서로 나온다
- 활용 예: 메서드 호출/종료, 실행 기록 저장 등
큐 (Queue)
-
선입선출 (FIFO, First In First Out)
→ 먼저 넣은 데이터가 먼저 나온다
- 예시:
- 1 → 2 → 3 순서로 넣으면
- 1 → 2 → 3 순서로 나온다
- 활용 예: 대기열, 작업 순서 처리, 선착순 이벤트 등
스택 영역과 힙 영역
스택 영역과, 힙 영역이 함께 사용되는 경우
1
2
3
4
5
6
7
8
9
10
11
12
package memory;
public class Data {
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package memory;
public class JavaMemoryMain2 {
static void main(String[] args) {
System.out.println("main start");
method1(); //메서드 호출 시 스택 프레임 생성
System.out.println("main end");
}
static void method1() {
System.out.println("method1 start");
Data data1 = new Data(10); //힙에 객체 생성, 스택에는 참조값 저장
method2(data1); // 스택에서 힙 객체를 참조해 메서드 실행
System.out.println("method1 End");
}
static void method2(Data data2) {
System.out.println("method2 start");
System.out.println("data.value = " + data2.getValue());
System.out.println("method2 end");
//method2 종료
}
}
main start
method1 start
method2 start
data.value = 10
method2 end
method1 End
main end
main → method1 → method2
: 메서드 호출 시 스택 프레임이 차례대로 생성new Data(10)
: 힙 영역에 객체 생성,data1
은 스택에 참조값 저장method2(data1)
: 스택에서 힙의 객체를 참조하여 메서드를 호출data2.getValue()
: 힙에 있는 객체의 메서드 실행- 각 메서드 종료 시 해당 스택 프레임은 제거
- 하지만 힙 객체는 여전히 참조 중이거나, 참조가 없으면 GC 대상이 됨
static 변수
static
키워드를 사용하면, 모든 인스턴스가 공유하는 변수를 만들 수 있다.
이런 변수는 정적 변수, 클래스 변수라 부른다.
1
2
3
4
5
6
7
8
9
10
11
package static1;
public class Data3 {
public String name;
public static int count; //static 변수 선언
public Data3(String name) {
this.name = name;
count++; //생성될 때마다 count 증가
}
}
- 맴버변수에
static
을 붙이게 되면 해당 변수는 클래스 로딩 시 메서드 영역에 생성 - 모든 인스턴스가 동일한 메모리를 공유
- 객체가 생성되면 생성자에서 정적 변수
count
의 값을 하나 증가시킨다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package static1;
public class DataCountMain3 {
public static void main(String[] args) {
Data3 data1 = new Data3("A");
System.out.println("A count: " + Data3.count);
Data3 data2 = new Data3("B");
System.out.println("A count: " + Data3.count);
Data3 data3 = new Data3("C");
System.out.println("A count: " + Data3.count);
System.out.println("=================");
//인스턴스를 통한 접근 (권장X)
Data3 data4 = new Data3("D");
System.out.println(data4.count); //인스턴스 변수인지 static 변수인지 모를 수 있기때문에 권장하지 않음
//클래스를 통한 접근 (권장)
System.out.println(Data3.count);
}
}
A count: 1
A count: 2
A count: 3
=================
4
4
Data3.count
와 같이 클래스명에 .(dot)
을 사용한다. 마치 클래스에 직접 접근하는 것 처럼 느껴진다.
static
변수는 클래스가 로딩될 때 메서드 영역에 생성됨클래스명.변수명
으로 접근하는 것이 명확- 인스턴스에서 접근이 가능하지만 혼동될 수 있으므로 주의
- 모든 인스턴스가 하나의 static 변수 공유
맴버 변수(필드)의 종류
인스턴스 변수 (Instance Variable)
static
키워드가 붙지 않은 멤버 변수- 인스턴스를 생성해야 사용할 수 있으며, 인스턴스에 속함
- 인스턴스를 만들 때마다 새로운 변수가 생성됨
1
2
3
public class Data {
public String name; //인스턴스 변수
}
클래스 변수 (Class Variable)
static
키워드가 붙은 멤버 변수- 클래스에 직접 속하며, 인스턴스를 생성하지 않아도 클래스명으로 접근 가능
- 자바 프로그램 시작 시 딱 1개만 메모리에 생성
- 여러 인스턴스에서 공유 용도로 사용
-
다른 이름으로는 정적 변수, static 변수라고도 함
→ 모든 용어는 같은 의미, 상황에 따라 혼용되니 익숙해질 것
1
2
3
public class Data {
public static int count; // 클래스 변수
}
왜 static
을 정적변수라 하는지
메모리에서 한 번만 생성되어, 프로그램 종료 전까지 계속 유지되기 때문이다.
-
힙(Heap) 영역에 생성되는 인스턴스 변수는
→ 인스턴스가 생성될 때 메모리에 올라가고,
→ 필요 없어지면 GC(Garbage Collector)에 의해 제거되는 “동적(dynamic)” 변수다.
-
반면,
static
변수는→ 클래스가 처음 로딩될 때 메서드(Method) 영역에 생성되고,
→ 프로그램이 종료될 때까지 계속 유지된다.
즉, static
변수는 시작과 끝이 고정된 정적인 메모리 구조를 가지므로
정적 변수(static variable)라고 부른다.
static 메서드
클래스에 static
키워드를 붙이면, 인스턴스를 생성하지 않고도 메서드를 호출할 수 있다.
“hello”
라는 문자열 앞 뒤에 *
을 붙여 *hello*
와 같이 꾸며주는 코드를 작성해보자
1. 인스턴스 메서드 사용
1
2
3
4
5
6
7
package static2;
public class DecoUtil1 {
//인스턴스 메서드
public String deco(String str) {
return "*" + str + "*";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package static2;
public class DecoMain1 {
public static void main(String[] args) {
String s = "Hello World!";
// 인스턴스를 먼저 생성해야 호출 가능
DecoUtil1 utils = new DecoUtil1();
String deco = utils.deco(s);
System.out.println("before : " + s);
System.out.println("after : " + deco);
}
}
뭔가 불편함. 굳이 DecoUtil1
의 인스턴스를 생성해야만 사용할 수 있음. 즉 비효율적
2. static 메서드로 변경
1
2
3
4
5
6
7
8
package static2;
public class DecoUtil2 {
// static 메서드
public static String deco(String str) {
return "*" + str + "*";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
package static2;
public class DecoMain2 {
public static void main(String[] args) {
String s = "Hello World!";
// 인스턴스 없이 클래스명으로 바로 호출
String deco = DecoUtil2.deco(s);
System.out.println("before : " + s);
System.out.println("after : " + deco);
}
}
static
메서드는 인스턴스 생성 없이 호출 할 수 있는 메서드다. 주로 공통적인 기능을 처리할 때 사용한다. 반면, 인스턴스 메서드는 객체 상태(필드)를 사용해야 할 때 사용된다
static 메서드의 특징
- 객체 생성 없이 클래스명으로 바로 호출 가능
- 클래스에 소속된
정적 변수(static variable)
,정적 메서드(static method)
만 접근 가능 - 인스턴스 변수, 인스턴스 메서드는 접근 불가
- 어디서든 접근 가능 (접근 제어자만 허용되면)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package static2;
public class DecoData {
private int instanceValue;
private static int staticValue;
// 정적 메서드
public static void staticCall() {
// static 메서드에서는 인스턴스 변수나 인스턴스 메서드에 직접 접근 불가
// instanceValue++; // 인스턴스 변수 접근 = 컴파일 오류
// instanceMethod(); // 인스턴스 메서드 접근 = 컴파일 오류
staticValue++; // 정적 변수 접근
staticMethod(); // 정적 메서드 호출
}
// 인스턴스 메서드
public void instanceCall() {
instanceValue++; // 인스턴스 변수 접근
instanceMethod(); // 인스턴스 메서드 호출
staticValue++; // 정적 변수 접근 가능
staticMethod(); // 정적 메서드 호출 가능
}
private void instanceMethod() {
System.out.println("instanceValue=" + instanceValue);
}
private static void staticMethod() {
System.out.println("staticValue=" + staticValue);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class DecoDataMain {
public static void main(String[] args) {
System.out.println("1. 정적 호출");
DecoData.staticCall(); // 클래스명으로 직접 호출
System.out.println("2. 인스턴스 호출1");
DecoData data1 = new DecoData();
data1.instanceCall(); // 인스턴스를 통한 호출
System.out.println("3. 인스턴스 호출2");
DecoData data2 = new DecoData();
data2.instanceCall();
}
}
1. 정적 호출
staticValue=1
2. 인스턴스 호출1
instanceValue=1
staticValue=2
3. 인스턴스 호출2
instanceValue=1
staticValue=3
- 정적 메서드(
static method
)는 인스턴스의 참조값(this
)이 없기 때문에 인스턴스 기능을 사용할 수 없다. - 정적 메서드는 클래스 이름으로 바로 호출되기 때문에 this 즉 참조값이 없다.
- 인스턴스 변수나 인스턴스 메서드는 객체마다 따로 존재하는 값
- 정적 메서드는 “내가 어떤 인스턴스를 보고 있는 건지”를 모름
1
2
3
4
public static void staticCall() {
this.instanceValue++; // 오류: this 없음
}
// “대상이 없는 상태에서 아무 객체나 조작하려는 것”이라서 컴파일 에러
맴버 메서드의 종류
1
2
3
4
5
// 인스턴스 메서드
public void hello() { ... }
// 클래스(정적) 메서드
public static void hi() { ... }
인스턴스 메서드는 new
로 객체를 만들어야 사용 가능하지만, 정적 메서드는 클래스명만으로 바로 사용할 수 있다.
정적 메서드 접근
1
2
3
DecoData data3 = new DecoData();
data3.staticCall(); // 인스턴스를 통한 접근 (비권장)
DecoData.staticCall(); // 클래스를 통한 접근 (권장)
둘의 차이는 없지만, 정적 메서드의 경우 인스턴스를 통한 접근은 인스턴스 메서드로 접근하는 것 처럼 오해할 수 있기 때문에 추천하지 않음!
따라서 클래스를 통한 접근이 더 명확하므로 정적 메서드에 접근할땐 클래스를 통해 접근하자
static import
1
2
3
4
5
import static static2.DecoData.staticCall;
// 또는
import static static2.DecoData.*;
staticCall(); // 클래스명 없이 호출 가능
static import
는 메서드뿐 아니라 정적 변수에도 사용할 수 있음
정적 메서드 main()
1
public static void main(String[] args) { ... }
static
이 붙은 정적 메서드- 자바 프로그램의 시작점(entry point) 역할
- JVM이 클래스 로딩 직후, 객체 생성 없이 호출하기 위해 static으로 선언됨
왜 인스턴스 없이 실행해야 할까?
- 프로그램이 처음 시작할 때는 아직 어떤 객체도 생성되어 있지 않음
- 그런데 JVM이 프로그램을 시작하려면 실행 가능한 메서드가 필요함
- 따라서 객체를 생성하지 않고도 실행 가능한
static
메서드인main()
*을 사용함
static 메서드는 static만 호출할 수 있다?
static 메서드는 같은 클래스 내부에서 인스턴스 멤버를 직접 호출할 수 없다.
그래서
main()
에서 호출할 메서드도 static으로 선언하는 것이 일반적이다.static 메서드는 클래스 차원에서 실행되므로, 인스턴스가 없어서
this
를 사용할 수 없다.
1
2
3
public class ValueData {
public int value = 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
public class ValueDataMain {
public static void main(String[] args) {
ValueData valueData = new ValueData();
add(valueData); // static 메서드 호출
}
static void add(ValueData valueData) {
valueData.value++;
System.out.println("숫자 증가 value=" + valueData.value);
}
}
main
은 static
이라서 static
메서드만 직접 호출 가능하다
정리
1. 메서드 코드는 메서드 영역에 1번만 올라간다
메서드는 인스턴스를 만들어도 계속 공유된다.
즉, 인스턴스는 변수만 따로 갖고, 메서드는 공유된 코드만 사용한다.
2. 힙에는 객체, 스택에는 참조값
new로 만든 객체는 힙에 저장되고,
그 참조값이 스택에 저장된다.
3. static 메서드는 static만 직접 호출 가능
static 메서드 내부에서는 this 참조값이 없기 때문에
인스턴스 변수와 메서드에 직접 접근할 수 없다.
4. static 변수는 클래스당 하나만 생성됨
static 변수는 메서드 영역에 단 하나만 생성되고,
모든 인스턴스가 공유한다. 인스턴스마다 따로 생기지 않는다.
5. main()
은 왜 static인가?
자바 프로그램 시작 시 객체가 없기 때문에,
JVM
이 클래스명으로 직접 호출할 수 있도록main()
은 반드시static
이어야 한다.
출처: 김영한의 실전 자바 - 기본편