자바 클래스 초기화 탐구
궁금했던 내용
@Slf4j
public class ClassLoaderTest {
@Test
void classInitTest() throws Exception {
Class<?> classDriverA = Class.forName("com.study.java_practice.ClassLoaderTest$DriverA");
ClassLoader classLoader = classDriverA.getClassLoader();
Class<?> classDriverB = classLoader.loadClass("com.study.java_practice.ClassLoaderTest$DriverB");
}
static class DriverA {
static {
log.info("DriverA : 초기화 되었습니다.");
}
}
static class DriverB {
static {
log.info("DriverB : 초기화 되었습니다.");
}
}
}
// 실행 결과 -> DriverA 클래스만 초기화 된다
22:33:47.995 [Test worker] INFO com.study.java_practice.ClassLoaderTest - DriverA : 초기화 되었습니다.
- Class.forName은 클래스를 이름으로 가져오면서 클래스를 초기화한다.
- ClassLoader의 loadClass는 똑같이 클래스를 이름으로 가져오지만 클래스를 초기화하지 않는다.
둘 다 클래스를 이름으로 가져오는 메서드인데, 하나는 초기화가 수행되고 다른 하나는 초기화가 수행되지 않는 이유가 궁금했다.
API 문서 확인
//Class
public static Class<?> forName(String className)
- 주어진 문자열 이름을 가진 클래스 또는 인터페이스와 관련된 Class 객체를 반환함
- className을 가진 클래스가 초기화 됨
// ClassLoader
public Class<?> loadClass(String name)
- 지정된 이름으로 클래스를 검색하여 로드함
- 클래스 참조를 resolve 하기 위해 JVM에 의해 수행됨
일단 API 문서 상에도 forName은 클래스를 초기화한다고 명시되어 있으나, loadClass는 클래스를 초기화한다고 명시해두지 않았다. 또 다른 차이점은, 후자는 JVM에 수행된다고 명시되어 있다.
바이트 코드 확인
API 문서만으로는 저걸 어떻게 구분해서 JVM이 하나는 초기화 시켜주고, 하나는 초기화를 안 시켜주는지가 궁금했다. 그래서 바이트 코드를 확인 해보기로 했다.
확인 해보니 확실한 차이점이 있다.
- Class.forName → INVOKESTATIC
- ClassLoader.loadClass → INVOKEVIRTUAL
해당 차이점 때문에 초기화 여부가 갈리는 것이라고 추측했고, 각 옵코드의 뜻을 알아보기 위해 jvm spec문서를 확인했다.
- https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-6.html#jvms-6.5.invokestatic
- https://docs.oracle.com/javase/specs/jvms/se17/html/jvms-6.html#jvms-6.5.invokevirtual
조금은 어려운 내용이었기에, 전부 다 이해할 순 없었지만 invokestatic 의 특징에서 그 이유를 찾을 수 있었다. 다음은 invokestatic 옵코드에 대한 간단한 설명이다.
- 옵코드로 수행할 메서드는 인스턴스/인터페이스 초기화 메서드가 아니어야 함
- static 메서드여야 하기 때문에 abstract가 될 수 없음
- 메서드가 올바른지 확인되면, 해당 메서드를 선언한 클래스/인터페이스가 아직 초기화 되지 않은 경우 해당 클래스/인터페이스가 초기화 됨
그리고, invokevirtual의 간단한 설명은 다음과 같다. (초기화 관련 내용은 없으며, 그저 메서드를 실행하는 옵코드다)
- 호출되는 메소드에 대해 JVM 스택에 새로운 프레임이 작성됨
- 새 프레임의 지역 변수 값이 연속적으로 만들어짐
- JVM의 PC가 호출할 메서드의 첫 번째 명령의 옵코드를 가리킴
- 메서드의 첫 번째 명령을 실행함
그런데, 클래스의 초기화 여부는 어떻게 구별할까?
일단 클래스 메타 정보(걍 클래스 로더에 의해 JVM에 로드만 된 상태의 파일??)는 메서드 영역(논리적으로 힙영역에 속함)에 로드가 되어있을 것이고, 트리거 포인트에 의해 "초기화가 유발"되는 경우, 초기화된 클래스 정보(?일지 아니면 다른 취급을 할지….)가 어디로 가는지가 궁금해서 몇 가지 가설을 세워봤다.
- 로드되어있던 메서드 영역에 그대로 있음(초기화 구별은 플래그같은걸로 함)
- 초기화된 클래스는 다른 공간에 보내버림(예를 들어 메소드 영역 → 힙 영역)
위 둘 중에 하나로 구분이 되지 않을까? 라는 생각을 하며, JVM spec 문서를 뒤져보았고, 아래의 문서에 아주 자세히 설명이 되어있을 것 같아 읽어보았다. Chapter 5. Loading, Linking, and Initializing
솔직히 내용이 너무 어려워서 전부 이해하진 못했지만, 이렇다 할 힌트를 얻은 것 같다.
- 초기화 메서드 실행이 정상적으로 완료 → 클래스 개체가 완전히 초기화된 것으로 레이블을 지정 → 대기중인 모든 스레드에 알림 → LC(클래스락)해제 후 절차를 정상적으로 완료함
결론적으로, 클래스 초기화가 정상 수행되면 클래스가 초기화 되었음을 레이블로 지정하는 것 같다. 초기화 구별은 별도의 레이블로 수행할 수 있는 듯 하다.
마무리
해당 내용을 확인하는 과정을 거치면서, 내가 JVM의 메모리를 아직도 제대로 이해하고 있지 못한다는 생각이 들었다. 그저 알려진 글들만 읽어옴 + 그대로 납득하기만 했음을 깨달았고, 좀 더 제대로된 공부를 해봐야겠다고 결심하는 계기가 되었다. (문제 해결을 함께 도와주신 ㅎㅈ님 감사합니다)
메모리 구조 관련 참고 자료
Chapter 2. The Structure of the Java Virtual Machine