1. 자바로 개발된 프로그램을 실행하면 JVM은 운영체제로부터 메모리를 할당함
2. 자바 컴파일러(javac)가 자바 소스코드(.java)를 자바 바이트코드(.class)로 컴파일함
3. Class Loader를 통해 JVM Runtime Data Area로 로딩함
4. Runtime Data Area에 로딩 된 .class들은 실행 엔진(Execution Engine)을 통해 바이트 코드를 해석함
5. 해석된 바이트 코드는 Runtime Data Area의 각 영역에 배치되어 수행하며 이 과정에서 실행 엔진에 의해 GC의 작동과 스레드 동기화가 이루어짐.
class = 설계도
object = 설계도로 만든 실제 물건
static = 실제 물건마다 따로 있는 게 아니라, 설계도에 붙어 있는 공용 값/기능
static은 객체가 아니라 클래스에 속하는 공유 상태static 객체를 만들지 않아도 사용할 수 있다는 장점이 있음클래스가 로딩된 뒤에 오래 살아 있고, 여러 객체와 여러 스레드가 함께 공유하는 값이 될 수 있음.
⇒ 메모리 문제, 동시성 문제, 테스트 문제, 초기화 문제를 만들기가 쉬움.
preparing단계에서 static 변수를 위한 메모리를 JVM이 확보하게 됨변수들은 기본값으로 초기화됨
이후 initializing 단계에서 static int count = 10; 과 같은 명시적 초기화 코드와 static { } 블록이 실행됨
class Example {
static int number = 10;
static {
System.out.println("static block 실행");
}
}
JVM 관점에서는 ⬇️ 이렇게 볼 수 있음
preparation 단계:
number = 0
initialization 단계:
number = 10
static block 실행
static필드 는 객체가 생성될 때마다 만들어지는 값이 아니라, 클래스가 JVM에 로딩되고 초기화되는 과정에서 준비되는 클래스 단위의 값static 은 공통으로 오래 유지되어도 괜찮은 값에만 조심해서 쓰는 것이 좋음예시 만약 static 필드가 큰 객체나 컬렉션을 계속 참조하게 된다면 어떻게 될까?import java.util.ArrayList;
import java.util.List;
class BadCache {
private static final List<byte[]> cache = new ArrayList<>();
public static void add() {
cache.add(new byte[10 * 1024 * 1024]); // 10MB
}
}BadCache.add()를 한 번 호출하면 10MB 배열이 cache에 들어감:
cache
└── 10MB 배열
두 번 호출하면:
cache
├── 10MB 배열
└── 10MB 배열
세 번 호출하면:
cache
├── 10MB 배열
├── 10MB 배열
└── 10MB 배열BadCache.add()가 호출될 때마다 10MB 배열이 cache에 쌓임 → cache가 static이기 때문에 계속 참조가 유지되고 GC가 해당 객체들을 쉽게 회수하지 못함GC 는 안쓰는 객체를 회수하는 것이 아니라 더 이상 참조되지 않는 객체를 회수함
위 배열들은 계속 cache 가 붙잡고 있기 때문에 GC 입장에서는 cache가 해당 배열을 계속해서 참조하고 있다고 판단하여 GC가 회수하지 못함 → 메모리 누수처럼 보이는 현상
⇒ static 으로 데이터를 저장하면 잠깐 쓰고 버리는 값이 아니라 애플리케이션 전체에서 오래 들고 있는 값이 될 수 있음
(동시성 문제)
static 필드는 클래스에 속한 공용 값 ⇒ 여러 스레드가 같은 static 값을 동시에 읽고 쓸 수 있음class Counter {
static int count = 0;
} 이 count 는 클래스에 속한 공용 값임 → 여러 스레드가 동시에 접근할 수 있음Thread A ─┐
Thread B ─┼──> Counter.count
Thread C ─┘ → 이 구조로 인해서 동시성 문제 / Race condition 이 발생할 수 있음 ⇒ 여러 스레드가 같은 static 값을 동시에 수정하면 값이 깨질 수 있음예시class LoginContext {
static Long currentUserId;
} 서버는 보통 여러 사용자의 요청을 여러 스레드로 동시에 처리하기 때문에 아래와 같이 흐름이 이어질 수 있음.사용자 A의 id가 1, 사용자 B의 id가 2라고 가정했을 때,
1. 스레드 A가 사용자 A 요청 처리 시작
LoginContext.currentUserId = 1;
2. 아직 사용자 A 요청이 끝나기 전에
스레드 B가 사용자 B 요청 처리 시작
LoginContext.currentUserId = 2;
3. 다시 스레드 A가 currentUserId를 읽음
그런데 값이 1이 아니라 2가 나올 수 있음static 필드는 클래스 단위로 하나만 존재하기 때문에 모든 요청과 스레드가 같은 값을 공유할 수 있음static에 저장하면, 한 사용자의 요청 처리 중 다른 사용자의 값으로 덮어써지는 문제가 발생할 수 있음static 필드나 블록이 있다면 코드에 작성된 순서대로 초기화됨 -> static 변수끼리 서로 의존하고 있으면 아직 초기화되지 않은 값을 참조하거나, 예상과 다른 값이 들어갈 수 있음예시class InitOrder {
static int a = getB();
static int b = 10;
static int getB() {
return b;
}
}public class Main {
public static void main(String[] args) {
System.out.println(InitOrder.a); // 0
System.out.println(InitOrder.b); // 10
}
} a 가 10 이 아니라 0 인 이유?preparation 단계:
a = 0
b = 0
initialization 단계:
a = getB(); // 이 시점의 b는 아직 0
b = 10;⇒ static은 클래스 단위로 먼저 준비되는 값이기 때문에 초기화 순서를 명확히 이해하지 못하면 버그를 만들기 쉬움. 특히 복잡한 객체 생성, 설정값 로딩, 다른 static 값 참조를 static 초기화 과정에 넣는 것은 조심해야 함
static final 은 변수에 다른 값을 다시 대입할 수 없다는 뜻으로, 그 변수가 참조하는 객체 내부까지 변경할 수 없다는 뜻은 아님static final List<Object> data = new ArrayList<>();
static final Map<String, Object> cache = new HashMap<>();
static final User currentUser = new User(); 참조 타입의 경우 객체 내부 변경까지 막지는 못함⇒ static final 컬렉션이나 객체는 JVM에서 클래스 단위로 오래 유지될 수 있고, 여러 스레드가 함께 접근할 수 있으므로 변경 가능한 객체를 static final 로 둘 때는 메모리 증가, 데이터 오염, 동시성 문제 등을 함께 고려해야 함