클래스는 언제 로딩되고 초기화되는가? (feat. 싱글톤)

skyepodium·2022년 1월 23일
28

1. 클래스 로딩

1) 정의

클래스 로더가 .class 파일을 찾고 JVM에 메모리에 올려놓는것을 의미합니다.

2) 왜 올려 놓는가

JVM은 실행될때 모든 클래스를 메모리에 올려놓지 않습니다. 그때 마다 필요한 클래스를 메모리에 올려 효율적으로 관리하기 위함입니다.

2. 그럼 언제 클래스를 로딩하는가

1) 가이드

  • 클래스의 인스턴스가 생성될때
  • 클래스의 정적 변수가 사용될때 (단, 정적 변수는 final로 선언된 상수 x)
  • 클래스의 정적 메소드가 호출될때

2) -verbose:class

-verbose:class 옵션을 사용하면 클래스 로딩을 디버그 할 수 있습니다.

3) 확인 방법

  • 테스트 케이스 하나씩 호출
  • javac 컴파일
  • -verbose:class 옵션으로 JVM 실행
  • 클래스가 로드되는지 확인

Tets 0. 아무것도 호출하지 않음

2개의 클래스 작성 Main, Single

// Main.java
class Main {
    public static void main(String[] args) {
    }
}

class Single {
    // 1. 생성자
    public Single() {}

    // 2. 정적 변수 - final X
    public static int a;

    // 3. 정적 변수 - final O
    public static final int b = 0;

    // 4. 정적 메서드
    public static void getInstance() {
    }

    // 5. 정적 내부 클래스
    public static class Holder {
        public static Single INSTANCE;
    }
}

컴파일 하고, 실행합니다.

javac Main.java
java -verbose:class Main


Main 클래스는 로딩되었지만 Single 클래스는 로딩되지 않았습니다.

Test 1. 인스턴스 생성

class Main {
    public static void main(String[] args) {
        // 생성자로 클래스의 인스턴스 생성
        new Single();
    }
}

class Single {
    // 1. 생성자
    public Single() {}

    // 2. 정적 변수 - final X
    public static int a;

    // 3. 정적 변수 - final O
    public static final int b = 0;

    // 4. 정적 메서드
    public static void getInstance() {
    }

    // 5. 정적 내부 클래스
    public static class Holder {
        public static Single INSTANCE;
    }
}
javac Main.java
java -verbose:class Main

Single 클래스가 로드되었습니다.

Test 2. 정적 변수 호출 - final (X)

class Main {
    public static void main(String[] args) {
        // final 지시어가 없는 정적 변수 호출
        System.out.println(Single.a);
    }
}

class Single {
    // 1. 생성자
    public Single() {}

    // 2. 정적 변수 - final X
    public static int a;

    // 3. 정적 변수 - final O
    public static final int b = 0;

    // 4. 정적 메서드
    public static void getInstance() {
    }

    // 5. 정적 내부 클래스
    public static class Holder {
        public static Single INSTANCE;
    }
}
javac Main.java
java -verbose:class Main

Single 클래스가 로드되었습니다.

Test 3. 정적 변수 호출 - final (O)

class Main {
    public static void main(String[] args) {
        // final 지시어가 붙은 상수 정적 변수 호출
        System.out.println(Single.b);
    }
}

class Single {
    // 1. 생성자
    public Single() {}

    // 2. 정적 변수 - final X
    public static int a;

    // 3. 정적 변수 - final O
    public static final int b = 0;

    // 4. 정적 메서드
    public static void getInstance() {
    }

    // 5. 정적 내부 클래스
    public static class Holder {
        public static Single INSTANCE;
    }
}
javac Main.java
java -verbose:class Main

Single 클래스가 로드되지 않았습니다.

Test 4. 정적 메소드 호출

class Main {
    public static void main(String[] args) {
        // 정적 메소드 호출
        Single.getInstance();
    }
}

class Single {
    // 1. 생성자
    public Single() {}

    // 2. 정적 변수 - final X
    public static int a;

    // 3. 정적 변수 - final O
    public static final int b = 0;

    // 4. 정적 메서드
    public static void getInstance() {
    }

    // 5. 정적 내부 클래스
    public static class Holder {
        public static Single INSTANCE;
    }
}
javac Main.java
java -verbose:class Main

Single 클래스가 로딩되었습니다.

Test 5. 정적 내부 클래스의 변수 호출

class Main {
    public static void main(String[] args) {
        System.out.println(Single.Holder.INSTANCE);
    }
}

class Single {
    // 1. 생성자
    public Single() {}

    // 2. 정적 변수 - final X
    public static int a;

    // 3. 정적 변수 - final O
    public static final int b = 0;

    // 4. 정적 메서드
    public static void getInstance() {
    }

    // 5. 정적 내부 클래스
    public static class Holder {
        public static Single INSTANCE;
    }
}
javac Main.java
java -verbose:class Main

어머나 Single 클래스 대신 Single 내부의 Holder 클래스만 로드되었습니다.

클래스 로딩 정리

1) 클래스 로딩 시점

  • 클래스의 인스턴스 생성
  • 클래스의 정적 변수 사용 (단, 정적 변수는 final로 선언된 상수 x)
  • 클래스의 정적 메소드 호출

2) 내부 클래스 로드 여부

외부 클래스가 로딩 - 내부의 클래스 로딩 X
내부 클래스만 로딩 - 외부의 클래스는 로딩 X

1. 초기화

1) 정의

클래스 초기화는 static 블록과 static 멤버 변수의 값을 할당하는 것을 의미합니다.

내부의 클래스는 초기화 대상 x

2) 언제 초기화 되는가

오라클 JLS - 초기화 시점

  • 클래스의 인스턴스 생성
  • 클래스의 정적 메소드 호출
  • 클래스의 정적 변수 할당
  • 클래스의 정적 변수 사용 (final x)

사실 위의 클래스 로드 시점과 같습니다. 클래스가 로드되면 초기화도 바로 진행됩니다.

3) 초기화 진행순서

  1. 정적 블록
  2. 정적 변수
  3. 생성자
class Main {
    public static void main(String[] args) {
        new Single();
    }
}

class Single {
    static {
        System.out.println("1. 정적 블록");
    }

    public static Temp temp = new Temp();

    public Single() {
        System.out.println("3. 생성자");
    }
}

class Temp {
    public Temp () {
        System.out.println("2. 정적 변수");
    }
}

4) 오직 한번만 클래스가 로딩됨을 보장

JLS(Java Language Specification)에 따르면 JVM에 클래스가 로딩되고 초기화될때는 순차적으로 동작함을 보장합니다.

멀티 스레드 환경에서 여러개의 스레드가 클래스를 동시에 로딩하려고 오직 한개의 클래스만 로딩됩니다.

다음 코드는 10개의 스레드가 동시에 클래스의 인스턴스를 생성합니다.

결과를 보면 10개의 스레드가 동시에 클래스 로딩을 시도해도

클래스 로딩은 한번만 수행되고, 그때 한번 초기화를 수행합니다. 이후 인스턴스를 10개 생성합니다.

이 의미는 멀티 스레드 환경에서 스레드 세이프함을 의미합니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Main {
    public static void main(String args[]) {
        // 1. 스레드 풀 생성
        ExecutorService service = Executors.newCachedThreadPool();

        // 2. 반복문을 통해 - 10개의 스레드가 동시에 인스턴스 생성
        for (int i = 0; i < 10; i++) {
            service.submit(() -> {
                new Single();
            });
        }
        // 3. 종료
        service.shutdown();
    }
}
class Single {
    static {
        System.out.println("static 블록 호출");
    }

    public Single() {
        System.out.println("생성자 호출");
    }
}
javac Main.java
java -verbose:class Main

싱글톤

갑자기 왜 싱글톤이냐면, 클래스 로딩 및 초기화 과정이 스레드 세이프함을 이용하여 싱글톤 인스턴스를 만들 수 있기 때문입니다.

1) 정의

싱글톤은 인스턴스를 오직 1개만 가지는 클래스입니다.

2) 멀티 스레드 환경

멀티 스레드 환경에서는 여러 스레드가 동시에 인스턴스 생성을 시도할 수 있고, 싱글톤 인스턴스가 여러개 생성될 수 있습니다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Main {
    public static void main(String args[]) {
        // 1. 스레드 풀 생성
        ExecutorService service = Executors.newCachedThreadPool();

        // 2. 반복문을 통해 - 10개의 스레드가 동시에 인스턴스 생성
        for (int i = 0; i < 10; i++) {
            service.submit(() -> {
                Single.getInstance();
            });
        }
        // 3. 종료
        service.shutdown();
    }
}
class Single {
    private Single() {
        System.out.println("싱글톤 인스턴스 생성");
    }

    private static Single instance;

    public static Single getInstance() {
        // 여러 스레드가 동시에 if문을 통과하는 경우
        if(instance == null) {
            // 여러개의 인스턴스가 만들어질 수 있음
            instance = new Single();
        }
        return instance;
    }
}

무려 10개의 스레드가 생성되었습니다.

3) 클래스 로딩 및 초기화가 딱 한번만 수행됨을 활용

클래스 로딩 및 초기화는 10개의 스레드가 동시에 시도해도 오직 단 한번만 수행됩니다.

이때 수행되는 초기화 과정에서 클래스의 인스턴스를 딱 한번만 생성되도록 합니다.

아래의 방법은 LazyHolder 라는 방법으로 싱글톤 인스턴스를 생성하는데 가장 권장되는 방법중 하나입니다.(다른 하나는 enum 사용)

장점

  • 스레드 세이프 보장
  • 필요할때 인스턴스 생성(getInstance 메소드가 호출될때)
  • 모든 자바 버전에서 적용가능
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class Main {
    public static void main(String args[]) {
        // 1. 스레드 풀 생성
        ExecutorService service = Executors.newCachedThreadPool();

        // 2. 반복문을 통해 - 10개의 스레드가 동시에 인스턴스 생성
        for (int i = 0; i < 10; i++) {
            service.submit(() -> {
                SingleTon.getInstance();
            });
        }
        // 3. 종료
        service.shutdown();
    }
}

// 싱글톤 클래스
class SingleTon {
    // private 생성자 new 를 통한 인스턴스 생성 방지
    private SingleTon() {
        System.out.println("싱글톤 인스턴스 생성");
    }

    // 정적 메서드를 통해 내부 클래스 로딩
    public static SingleTon getInstance() {
        return LazyHolder.INSTANCE;
    }

    // 내부 클래스가 로딩될때 초기화 수행 - 싱글톤 인스턴스 생성
    private static class LazyHolder {
        private static final SingleTon INSTANCE = new SingleTon();
    }
}

인스턴스가 오직 한개만 생성되었습니다.

총 정리

1) 클래스 로딩 시점

  • 클래스의 인스턴스 생성
  • 클래스의 정적 변수 사용 (단, 정적 변수는 final로 선언된 상수 x)
  • 클래스의 정적 메소드 호출

2) 외부, 내부 클래스 로딩

외부 클래스 로딩 - 내부 클래스 로딩 X
내부 클래스 로딩 - 외부 클래스 로딩 X

3) 초기화 의미

  1. static 블록 수행
  2. static 변수 메모리 할당

4) 클래스 로딩 및 초기화

클래스가 로딩될때 초기화도 수행

클리스 로딩 및 초기화 과정은 스레드 세이프함, 여러 스레드가 동시에 시도해도, 오직 한번만 수행

5) 싱글톤 - LazyHolder

클래스 로딩 및 초기화 과정이 스레드 세이프함을 이용하는 방법

장점

  • 스레드 세이프 (오직 1개의 싱글톤 인스턴스 생성)
  • 필요할때 인스턴스 생성(getInstance 메서드 호출 시점)
  • 모든 자바 버전에서 사용가능
profile
callmeskye

2개의 댓글

comment-user-thumbnail
2022년 1월 24일

내용 중에 싱글톤 기법으로 LazyHolder 를 소개해 주셨는데, 이는 위키피디아 예제 코드에 사용된 클래스의 이름입니다. 실제 기법의 이름은 Initialization-on-demand holder idiom 입니다. 참고해 주세요.

답글 달기
comment-user-thumbnail
2022년 11월 9일

정리 잘해주셔서 많은 도움이 되었습니다.
궁금한게 있어서 여쭤봅니다.
TEST3 에서
// final 지시어가 붙은 상수 정적 변수 호출
System.out.println(Single.b);
<== static final 변수 호출 시 Single클래스가 로딩 되지 않아도 값은 출력이 되는데
그렇다면 클래스가 언제 로딩되어 static final 변수가 메모리에 저장된 건지 궁금합니다.

답글 달기