최근 코코아 과정 수업도중에 질문받은 내용중 인상깊었던 질문을 조금더 정리해볼까 한다.
학생분이 질문했을때 나 또한 순간 static 키워드에 생각이 쏠려서 "왜? 맨처음 초기화가 안된거지?"라는 생각에 빠졌는데...
가장 큰 키워드를 생각 안하고있었다... "Inner Class"라는 점.... 이점만 다시 상기하면 다행이 나의 설명이 틀리지는 않았었다!
(진짜 사람이 순간 당황하니까 진짜 내가 알고있던 지식이 왜곡 되는 느낌이였다.... )
우선 문제가된 singleton 코드를 잠시 살펴본후, 이에 대하여 알아보자!
1. LazyHolder 방식의 singleton
public class OuterSingleton {
private OuterSingleton() {
}
public static OuterSingleton getInstance() {
return InnerSingleton.uniqueInstance;
}
private static class InnerSingleton {
private static final OuterSingleton uniqueInstance = new OuterSingleton();
}
}
위 싱글톤 코드는 volatile과 synchronized 키워드 없이도 동시성 문제를 해결할 수 있는 코드이다.
JVM에게 객체의 초기화를 떠님기는 방식으로, 멀티스레드 환경에서도 객체의 단일성을 보장할 수 있다는 장점이 있다.
2. 문제의 질문
"static 변수의 값들은 맨 처음 초기화 되는것 아닌가요?? 어째서 나중에 생성된다는 것 이죠??"
"static으로 선언된 친구들은 jvm이 올라갈 때 전부 초기화가 이루어진다고 알고 있었는데, 어째서 static inner class는 호출될 때 초기화가 되다느 것 이죠?"
생각해볼만한 주제인것 같다.
핵심은!!
로드와 초기화는 분리된 과정이라는 것 이다! (로딩 == 초기화 가 아니다!!)
위 싱글톤 코드를 조금만 변형해보자!
package shine;
public class OuterSingleton {
private static int counter = 0;
static {
System.out.println("Outer의 static block = " + counter);
counter++;
}
private OuterSingleton() {
}
public static OuterSingleton getInstance() {
return InnerSingleton.uniqueInstance;
}
private static class InnerSingleton {
static {
System.out.println("Inner의 static block = " + counter);
counter++;
}
private static final OuterSingleton uniqueInstance = new OuterSingleton();
}
public static void main(String[] args) {
System.out.println("main에서 호출 = " + counter);
counter++;
System.out.println("InnerSingleton 클래스 정보 호출 = " + InnerSingleton.class);
System.out.println("uniqueInstance 호출 = " + InnerSingleton.uniqueInstance);
System.out.println("Last counter = " + counter);
}
}
출력결과는 우선 다음과 같다.
하나하나 JVM을 보면서 생각해보자!
1) 우선 맨처음 위의 싱글톤 코드가 컴파일러에 의해서 컴파일 되면 .class 확장자의 바이트코드가 생성된다.
2) 프로그램을 실행하면 main함수가 실행되어야 하기 때문에 main함수를 가지고 있는 'OuterSingleton' 클래스가 로딩되고, 초기화됩니다.
이때 위 그림에서 Linking 과정의 prepare에서 counter static 변수가 메모리에 생성됩니다.
이후 Initilization 과정에서 counter변수에 0을 할당하게 됩니다. (Outer 클래스를 사용할 준비가 완료됨)
아직 'Inner' 클래스가 로딩되지 않았습니다. 접근한적이 없기 때문이죠!
3) main메서드 호출
main메서드의 첫줄이 호출되면서 1을 출력합니다. 그리고 counter값을 1 올려줍니다.
4) System.out은 'InnerSingleton' class 정보를 출력합니다.
'InnerSingleton' 클래스는 초기화되지는 않았지만, Loader에 로딩은 되었습니다.
5) uniqueInstance 호출 부분!
드디어 InnerSingleton.uniqueInstance에 접근하게 되었습니다. (이전에 이미 Class Loader에 로딩은 되있는 상황)
이때 prepare 과정에서 uniqueInstance 변수를 생성하게 되고, Initilization 과정에서 new 연산자로 객체를 생성하게 됩니다.
이후 InnerSingleton의 Inner Static block으로 이동하여 숫자2를 출력하고 counter값을 1 올려줍니다.
6) 마지막 main으로 돌아와 3출력
3. 로드와 초기화
Main 메소드를 포함하고 있는 OuterSingleton 클래스는 Main 메소드를 실행하기 전에 초기화가 진행이 된다.
그렇기 때문에 static 변수와 static 블록이 초기화가 진행되어, 호출되게 된다.
다음으로는 Inner.class를 호출하는 부분을 생각해보자, 해당 클래스는 메모리에 올라가는 있다.
해당 값(class shine.OuterSingleton$InnerSingleton)이 표시가 되었다.
이는 클래스는 로드가 되어 있는 상태이기 때문에 class에 대한 메타데이터를 출력할 수 있는 것이다.
만약 로딩도 되어있지 않다면 위 메타정보를 출력할수가 없었을 것 이다!
하지만, static 블록이 호출되지 않았기에 해당 Inner 클래스는 아직 초기화 전인것을 알 수 있다!
마지막으로 InnnerSingleton의 uniqueInstance를 호출하기 위해서는 InnnerSingleton이 먼저 초기화가 되어야 한다.
InnnerSingleton의 호출하기 위해서는 Inner 클래스가 초기화가 되어 있어야 한다.
따라서 inner 클래스의 static 변수에 객채 생성이 진행이 되어 할당되고, 다음으로 static 블록에 있는 출력문이 cnt = 2를 출력해준다.
이러면 InnnerSingleton의 초기화 까지 완료된 상태이다!
이후 다시 main으로 돌아와 3을 출력하고 끝난다.
4. Static 변수의 저장 위치는?
Heap 영역은 JVM에 의해 관리된 영역이며, Native 메모리는 OS 레벨에서 관리하는 영역으로 구분된다.
각종 메타 정보를 OS가 관리하는 영역으로 옮겨 Perm 영역의 사이즈 제한을 없앤 것이라 할 수 있다.
'BackEnd > Java' 카테고리의 다른 글
[Java] 동일성(identity)과 동등성(equality) (0) | 2022.09.06 |
---|---|
[Java] Annotation Processor (0) | 2022.06.08 |
[Java] Dynamic Proxy (0) | 2022.06.07 |
[Java] Reflection (0) | 2022.05.22 |
[Java] 바이트코드 조작하기 (0) | 2022.05.20 |
댓글