jvm 내부에서 자바 바이트코드를 전달받아 동작하는 클래스 로더에 대해 알아보자!
클래스 로더의 개념
자바는 동적 로드, 즉 컴파일 타임이 아니라 런타임(바이트코드를 실행할 때) 에 클래스를 링크하고 로드하는 특징을 가진다. 자바의 클래스 로딩은 클래스 참조 시점에 JVM 에 코드가 링크되고, 실제 런타임 시점에 로딩되는 동적 로딩을 거친다.
자바는 클래스 로딩은 클래스 참조 시점에 JVM 에 코드가 링크되고, 실제 런타임 시점에 로딩되는 동적 로딩을 거진다.
런타임에 동적으로 클래스를 로딩한다는 것은 JVM 이 미리 모든 클래스에 대한 정보를 메소드 영역에 로딩하지 않았다는 것을 의미합니다.
JVM 내에서 자바 바이트코드를 전달받는 클래스 로더가 자바가 동적으로 로드될 수 있도록 해주는 소프트웨어입니다. 정리하자면, 클래스 로더는 런타임 중에 JVM의 메소드 영역에 동적으로 Java 클래스를 로드하는 역할을 합니다.
클래스 로더
클래스 로더 시스템
- .class 에서 바이트 코드를 읽고 JVM의 메모리 영역인 Runtiem Data Areas(stack, pc, 네이티브.. 등)에 적절히 배치한다.
- 바이트 코드는 말그대로 jvm 이 실행하기 위한 instruction 이다.
- jvm 이 바이트 코드를 실행하면 자료구조가 생성된다.
- 로딩 : 바이트 코드를 클래스로 읽어오는 과정
- 링크 : 레퍼런스를 연결하는 과정
- 초기화 : static 값들 초기화 및 변수에 할당
- 로더, 링크, 초기화 순으로 진행된다.
- 로딩
- 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터(JVM 이 관리하는 자료 구조 형태로 변환되어 메모리에 올라감.)를 만들고 "메소드" 영역에 저장.
- 즉 클래스 정보(바이트 코드 .class)를 메소드 영역에 저장
- 이때 메소드 영역(메타스페이스)에 저장하는 데이터
- FQCN (Full Qualified Class Name) : 패키지 경로, 클래스이름
- 클래스 파일과 클래스 | 인터페이스 | enum 의 관련 여부
- 메소드와 변수 등의 정보
- 메서드 영역이란 : 메서드 영역은 프로그램을 실행하는 데 필요한 공통데이터를 관리
- 이 영역은 프로그램의 모든 영역에서 공유함.
- 로딩이 끝나면 해당 클래스 타입의 Class 객체 (클래스 t 타입의 객체)를 생성하여 "힙" 영역에 저장
Solution solution = new Solution();
Class<? extends Solution> aClass = solution.getClass();
- 클래스 로더가 .class 파일을 읽고 그 내용에 따라 적절한 바이너리 데이터(JVM 이 관리하는 자료 구조 형태로 변환되어 메모리에 올라감.)를 만들고 "메소드" 영역에 저장.
- 링크
- Verify, Prepare, Resolve(optional) 세 단계로 나눠져 있다.
- 검증: .class 파일 형식이 유효한지 체크한다.
- Preparation: 클래스 변수(static 변수) 와 기본값에 필요한 메모리
- Resolve: 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체한다.
- 초기화
- Static 변수의 값을 할당한다. (static 블럭이 있다면 이때 실행된다.)
클래스 로더의 종류
- 클래스 로더는 계층 구조로 이뤄져 있으면 기본적으로 세가지 클래스 로더가 제공된다.
- 부트 스트랩 클래스 로더 - JAVA_HOME\lib 에 있는 java.lang.*, java.util.* 등의 기본 API 클래스들을 제공한다.
- 최상의 우선 순위를 가진 클래스 로더
- 플랫폼 클래스로더 - JAVA_HOME\lib\ext 폴도 떠는 java.ext.dirs 시스템 변수에 해당하는 위치에 있는 클래스를 읽는다. (자바 9 는 module path을 사용해야함)
- 부트 스트랩 클래스 로더 - JAVA_HOME\lib 에 있는 java.lang.*, java.util.* 등의 기본 API 클래스들을 제공한다.
Solution solution = new Solution();
Class<? extends Solution> aClass = solution.getClass();
java --module-path /path/to/libs -m mymodule/com.example.Main
- 애플리케이션 클래스로더 - 애플리케이션 클래스패스(애플리케이션 실행할 때 주는 -classpath 옵션 또는 java.class path 환경 변수의 값에 해당하는 위치)에서 클래스를 읽는다.
ClassLoader classLoader = Solution.class.getClassLoader();
System.out.println(classLoader);
System.out.println(classLoader.getParent());
System.out.println(classLoader.getParent().getParent()); // 최상위 부트스트랩 클래스 로더는 네이티브코드임. 참조해서 출력 불가
}
- 실행결과
classLodaers 파일을 확인하면 BOOT_LOADER, PLATFORM_LOADER, APP_LOADER 가 있는것을 확인할 수 있다.
String append = VM.getSavedProperty("jdk.boot.class.path.append");
부트스트랩 로더는 해당 클래스 패쓰에 있는 클래스들만 읽어준다.
PLATFORM_LOADER = new PlatformClassLoader(BOOT_LOADER);
플랫폼 로더는 Boot 스트랩을 부모로만 설정했다.
String cp = System.getProperty("java.class.path");
if (cp == null || cp.isEmpty()) {
String initialModuleName = System.getProperty("jdk.module.main");
cp = (initialModuleName == null) ? "" : null;
}
URLClassPath ucp = new URLClassPath(cp, false);
if (archivedClassLoaders != null) {
APP_LOADER = (AppClassLoader) archivedClassLoaders.appLoader();
setArchivedServicesCatalog(APP_LOADER);
APP_LOADER.setClassPath(ucp);
} else {
APP_LOADER = new AppClassLoader(PLATFORM_LOADER, ucp);
ArchivedClassLoaders.archive();
}
APP_LODER 는 java 클래스 패스 또는 jdk 모듈 메인에서 값을 가져와 클래스 Path를 읽어준다.
직접 작성한 코드 99% 는 애플리케이션 클래스로더가 읽게 된다.
클래스로더의 동작 방식
JVM의 클래스 로더는 새로운 클래스를 로드해야할 때, 다음과 같이 동작하게 됩니다.
- JVM의 Method Area에 클래스가 로드되어 있는지 확인한다. 만일 로드되어 있는 경우 해당 클래스를 사용한다.
- Method Area에 클래스가 로드되어 있지 않을 경우, 시스템 클래스 로더에 클래스 로드를 요청한다.
- 시스템 클래스 로더는 확장 클래스 로더에 요청을 위임한다.
- 확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임합니다.
- 부트스트랩 클래스로더는 부트스트랩 Classpath(JDK/JRE/LIB) 에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 확장 클래스로더에게 요청을 넘긴다.
- 확장 클래스 로더는 확장 Classpath(JDK/JRE/LIB/EXT) 에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 시스템 클래스 로더에게 요청을 넘긴다.
- 애플리케이션 클래스로더는 시스템 Classpath에 해당 클래스가 있는지 확인한다. 클래스가 존재하지 않는 경우 ClassNotFoundException을 발생시킨다.
런타임 데이터 영역 (Runtime Data Area)
런타임 데이터 영역은 쉽게 말하면 JVM 의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역이다.
이때 Method Area 와 HeapArea 는 모든 쓰레드가 공유하는 영역이고 나머지 Stack Area, PC Register, Native Method Stack 은 각 쓰레드마다 생성되는 개별 영역이다.
따라서 위의 그림을 좀 더 자세히 표현하자면 다음과 같이 도식이 된다.
클래스의 메타데이터는 한벌만 존재,
인스턴스는 각각의 클래스의 상태가 다 다르기 때문에 인스턴스마다 존재
그거랑 별개로 클래스 t타입의 객체는 하나만 있으면 된다. 런타임에 클래스 정보를 얻기 위한 객체이기 때문이다
클래스 로드 시점 파헤치기
클래스 로딩은 class loader 가 .class 파일을 찾고 JVM 에 메모리에 올려놓는 것을 의미한다.
클래스가 로드 되었는지 확인하는 방법은, 콘솔에 java 명령어에 -verbose:class 옵션을 사용하면 클래스 로딩을 디버그 할 수 있다.
> java -classpath 클래스파일위치 -verbose:class 클래스명
아무것도 호출하지 않음
- 메인 메소드를 실행하였으니, 메인 메소드가 위치하고 있는 Main 클래스만 로드된다.
- 아무리 static 멤버들이 있더라도 직접 가져와 사용하지 않는 경우 Outer 클래스는 로드되지 않는다.
class Outer {
// static 변수
static String value = "> Outer 클래스의 static 필드 입니다.";
// static final 상수
static final String VALUE = "> Outer 클래스의 static final 필드 입니다.";
Outer() { System.out.println("> Outer 생성자 초기화"); }
// static 메서드
static void getInstance() {
System.out.println("> Outer 클래스의 static 메서드 호출");
}
// inner 클래스
class Inner {
Inner() { System.out.println("> Inner 생성자 초기화"); }
}
// static inner 클래스
static class Holder {
static String value = "> Holder 클래스의 static 필드 입니다.";
static final String VALUE = "> Holder 클래스의 static final 필드 입니다.";
Holder() { System.out.println("> Holder 생성자 초기화"); }
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
}
}
인스턴스 생성
클래스를 인스턴스화 하면 예상대로 당연히 클래스가 로드된다.
그러나 내부 클래스는 직접 인스턴스를 생성하지 않으니 로드되지않는다.
static 변수 호출
클래스 내부의 static 멤버를 호출하면, 인스턴스화 하지 않아도 클래스가 로드된다.
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
System.out.println(Outer.value); // 정적 변수 호출
}
}
static final 상수 호출
- 단, static final 상수를 호출할 경우 static 변수와 다르게 Outer 클래스가 로드되지 않는다.
- 왜냐하면 상수는 JVM 의 Methode Area 에 Constant Pool 에 따로 저장되어 관리되기 때문이다.
static 메소드 호출
static 변수 호출과 같이 Outer 클래스가 로드된다.
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Outer.getInstance(); // static 메서드 호출
}
}
내부 클래스 호출
내부 클래스를 생성하기 위해선 외부 클래스를 먼저 생성하고 인스턴스화 해야 하기 때문에 Outer 클래스와 Inner 클래스 둘다 로드가 된다.
이러한 특징 떄문에 내부 클래스를 static 으로 선언하지 않고 인스턴스 멤버 클래스로서 사용하면 메모리 누수가 발생하게 된다.
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
new Outer().new Inner(); // 내부 클래스 인스턴스화
}
}
static 내부 클래스 호출
static inner 클래스는 외부 클래스를 생성하지 않고 바로 직접 인스턴스화가 가능하다.
그래서 일반 내부 클래스와는 달리 외부 클래스를 로드하지 않는 다는 차이점이 있다.
static 이 붙었다고 해서 static inner 클래스를 static 멤버나 static 메서드처럼 취급해서 생각하면 안된다.
inner 클래스와 static inner 클래스의 차이는 외부 클래스를 생성해야 내부 클래스를 인스턴스화 할 수 있는지 차이일뿐, 클래스를 초기화해서 사용하는 것은 같다.
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
new Outer.Holder(); // static 내부 클래스 인스턴스화
}
}
static 내부 클래스의 static 변수 호출
- 마찬가지로 클래스를 인스턴스화 하지 않아도 static 멤버를 호출하면 Holder 클래스가 로드된다.
- 그리고 외부 Outer 클래스는 로드되지 않는다.
이어서 static 내부 클래스의 static final 상수를 호출하면, 위에서 봤듯이 Outer 클래스와 그의 내부 클래스는 호출되지 않는다.
(상수는 따로 관리)
클래스 초기화 시점 파헤치기
클래스 초기화는 static 블록과 static 멤버 변수의 값을 할당하는 것을 의미한다. 꼭 new 생성자로 클래스를 인스턴스화 해야 클래스가 초기화되는 것이 아니다.
클래스 로더에서 초기화 과정을 3단계로 나눠 설명하였지만, 사실 클래스 초기화(Initialization) 는 클래스 로드(Loading) 시점과 거의 동시에 일어나기 때문에 같다고 보면된다.
- 클래스의 인스턴스 생성
- 클래스의 정적 메소드 호출
- 클래스의 정적 변수 할당
- 클래스의 정적 변수 사용(final x)
예시를 들자면, 다음 코드를 보면 Outer 클래스의 static 메소드를 호출하면, new 생성자로 인스턴스화 하지 않아도 클래스가 메모리에 로드되어 초기화되는 것을 볼 수 있다.
static 블록 : 클래스가 로딩되고, 클래스 변수가 준비된 후 자동으로 실행되는 블록
class Outer {
// static 블록
static {
System.out.println("> Initializing class Outer");
}
// 생성자
Outer() {
System.out.println("> Outer 생성자 호출");
}
// 정적 메소드
static void getInstance() {
System.out.println("> Outer 클래스의 static 메서드 호출");
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
Outer.getInstance(); // 정적 메소드 호출
}
}
클래스 초기화 진행 순서
클래스 초기화 시점을 알았으니, 이번에는 클래스가 초기화 되면서 클래스 내부에서 어떤 멤버들이 어떤 순서대로 초기화가 순차적으로 진행되는지 알아보자.
코드상으로 정적 변수 선언문이 static 블록보다 위에 위치해있어서 먼저 초기화가 되었지만 , 정확히 말하면 static 필드 변수와 static 블록의 초기화 순서는 코드 선언 순서에 따라 다르다.
class Outer {
public static Object obj = new Print(); // 1. 정적 변수
// static 블록
static {
System.out.println("2. 정적 블록");
}
// 생성자
Outer() {
System.out.println("3. 생성자");
}
}
// 정적 변수가 초기화 되었음을 출력해주는 용도
class Print{
Print(){
System.out.println("1. 정적변수");
}
}
public class Main {
public static void main(String[] args) throws ClassNotFoundException {
new Outer();
}
}
클래스 로드와 초기화 분리하기
위에서 클래스 로드와 초기화가 워낙 순식간에 일어나 동시에 실행한다고 하였지만, 그래도 클래스 로더에서 봤다시피 엄연히 클래스가 메모리에 적재하는 과정을 나눠져있다.
class Outer {
public String field = "Outer 클래스의 필드입니다!";
// static 블록
static {
System.out.println("> Initializing class Outer");
}
// 생성자
Outer() {
System.out.println("> Outer 생성자 호출");
}
// 정적 메소드
static void getInstance() {
System.out.println("> Outer 클래스의 static 메서드 호출");
}
}
// 정적 변수가 초기화 되었음을 출력해주는 용도
class Print{
Print(){
System.out.println("1. 정적변수");
}
}
public class Main {
public static void main(String[] args) throws Exception {
System.out.println("1. 클래스 load만 : ");
Class<Outer> outerClass = Outer.class; // Outer 클래스의 Class 객체 리터럴로 가져오기
System.out.println("\n---------------------------------------------------------------------\n");
System.out.println("2. 클래스 initialization : ");
Outer outer = outerClass.getDeclaredConstructor().newInstance(); // Reflaction API 로 클래스 생성하기
System.out.println(outer.field);
}
}
이를 코드에서 확인하려면 , Object 클래스의 getClass() 메서드를 통해 Class 클래스 객체를 가져오면 된다.
위의 코드에서 Outer 클래스를 콘솔에서 로드했음에도 static 블럭이나 생성자가 실행되지않아 초기화 과정 메시지가 출력이 안된것 을 확인할 수 있다.
즉 Outer.class 클래스 객체만 가져올 경우 클래스가 loading 되며, 클래스 객체를 이용해 인스턴스화 하면 그제서야 클래스가 initailization 이 되는걸, 점선 구분선을 통해 클래스 로딩 과정이 분리되었음을 볼 수 있다.
클래스 초기화는 오직 한번만 수행
클래스 초기화 작업은 오직 한번만 이행된다.
만일 멀티 쓰레드 환경에서 여러개의 쓰레드가 동시에 클래스를 인스턴스화 하여도 클래스 초기화는 오직 한번만 수행된다.
정확이 말하면 클래스 로딩이 최초로 될때, 그때 한번만 초기화를 수행하고 그 이후에는 초기화를 스킵한다고 보면 된다.
이 의미는 멀티쓰레드 환경에서 클래스 초기화 동작 자체는 스레드세이프함을 의미한다.
class Outer {
static {
System.out.println("static 블록 호출");
}
public Outer() {
System.out.println("생성자 호출");
}
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) throws Exception {
// 스레드풀 생성
ExecutorService executorService = Executors.newCachedThreadPool();
// 2. 반복문을 통해 10개의 스레드가 동시에 인스턴스 생성
for (int i = 0; i < 10; i++) {
executorService.submit(() -> {
new Outer();
});
}
// 3. 종료
executorService.shutdown();
}
}
참고사항:
https://velog.io/@ddangle/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%8D%94%EB%9E%80
'JAVA' 카테고리의 다른 글
Java 예외 처리, 제대로 알고 쓰자 (1) | 2025.03.26 |
---|---|
바이트코드 조작 (1) | 2025.03.24 |
JVM, JDK, JRE 의 차이, JVM의 동작방식 (0) | 2025.03.23 |
네트워크 - 프로그램1 (1) | 2025.01.29 |
자바 - IO 기본 (buffer) (1) | 2025.01.28 |