Java에서 객체가 어떻게 형성되고 관리되는 지를 이해하려면, .java 파일로 작성되었던 소스코드가 어떻게 JVM(Java Virtual Machine) 위로 로딩(Loading)되는 지 아는 것이 대단히 중요하다! 이를 알아야 클래스 로딩 때 발생하는 이슈('java.lang.ClassNotFoundException'과 같은 Java 개발 중 흔히 보이는 에러)를 해결할 수 있고, 코드 상에서 동적으로 클래스를 로딩하는 구문을 이해할 수 있으며, 드물지만 직접 클래스 로더(User-defined loader)를 만드는 것이 가능하기 때문이다.
.java 파일 / .class 파일
자바 개발을 해봤다면, .java 파일과 .class 파일이 생성되는 것을 봤을 것이다. .java 파일은 자바 파일이라고 하며, java 언어로 소스 코드를 작성할 때, 해당 소스 코드 내용을 적는 파일을 말한다. 그리고 이 소스 코드 파일 즉, .java 파일을 컴파일하게 되면 .class 파일이 생성 되는데, 이를 클래스 파일 또는 자바 바이트 코드라고 부른다.
자바 바이트 코드는 JVM 만 설치되어 있다면, 어떤 운영체제이든 상관없이 Java 언어로 작성된 코드를 실행할 수 있다.
흔히 Eclipse와 같은 IDE 혹은 커맨드 라인을 활용하여, 'javac' 라는 명령어를 통해 컴파일 했을 때 나오는 파일인 것이다. JVM 에서는 클래스 로더(Class Loader)가 이러한 자바 바이트 코드들을 로딩하게 되는데, 이번 포스팅에서는 그 클래스 로딩의 관련된 내용에 대해 살펴보도록 하자!
클래스 로더(Class Loader)란?
Java는 *동적 로딩(Dynamic Loading)을 하는 특징을 가지고 있는데, 이 동적 로딩을 담당하는 부분이 JVM에서의 클래스 로더이다. 즉, 클래스 로더가 Java로 작성된 프로그램의 런타임(Runtime) 중에, JVM의 메소드 영역에 동적으로 Java 클래스를 Load 하는 역할을 하는 것이다. 클래스 로더는 크게 3가지 단계를 거쳐 메모리에 Java 클래스를 적재(Load)하게 된다.
- 로딩(Loading)
: .class 파일을 읽어서 바이너리 코드(Binary Code)로 만들고, 이를 메모리의 메서드 영역(Method Area)에 저장하는 과정.
- Fully-Qualified Class Name : 클래스 로더, 클래스 패키지 경로, 패키지 이름, 클래스 이름을 모두 포함한 값
- Class, Interface, Enum을 구분하여 저장
- 메서드와 변수
- 로딩이 끝나면 해당 클래스 타입의 객체를 생성하여 메모리의 힙 영역(Heap Area)에 저장함.
- 서로 상하 관계에 있는 클래스 로더들이 정해진 순서에 따라 클래스를 로딩하는데, 클래스 로딩을 요청할 때 상위 클래스 방향으로 위임하는 메커니즘이 있는데, 이를 Delegation Model(위임 모델) 이라고 한다. 이에 관한 자세한 내용은 아래에서 설명하도록 한다. - 링크(Linking)
: 코드 내부의 레퍼런스를 연결하는 과정.
- Verify(검증) - Prepare(준비) - Resolve(분석)(Optional) 의 과정을 거침.
- Verify : .class 파일 형식이 유효한지 검사.
- Prepare : 클래스가 필요로 하는 메모리를 할당하고, 클래스의 필드, 메서드, 인터페이스를 나타내는 데이터 구조를 준비.
- Resolve : 클래스의 상수 Pool 내 모든 **Symbolic Reference를 실제 Memory Reference(Direct Reference) 로 교체.
Optional인 이유는, 교체(Binding)될 수도 있고, 이후 사용이 일어날 때에 동적으로 교체될 수도 있기 때문. - 초기화(Initialization)
: ***Static 변수를 초기화하고 값을 할당하는 과정
*동적 로딩(Dynamic Loading) : 프로그램을 실행할 때(런타임 중), 필요할 때마다 동적으로 메모리를 할당하고 적재하며, 필요 없는 메모리는 자동으로 할당 해제하여, 메모리를 효율적으로 관리하는 로딩 방식.
<=> 정적 로딩(Static Loading) : 프로그램을 실행할 때(실행하는 순간), 모든 실행 파일을 메모리에 적재하는 방식.
**Symbolic Reference : 참조하는 클래스의 특정 메모리 주소 자체를 참조 관계로 구성한 것이 아니라, 참조하는 대상의 이름만을 지칭하는 방식.
***Static 변수 : Java 에서 Static 키워드를 사용한다는 것은, 메모리에 한 번 할당되면 프로그램이 종료될 때에 해제되는 것을 의미한다. Static 변수는 클래스 변수이며, Static 메모리 영역에 존재하므로, 객체가 생성되기 이전에 이미 할당이 되어 있어, 객체 생성 없이 바로 접근이 가능한 변수를 말한다.
클래스 로더의 3종류
클래스 로더는 JRE(Java Runtime Environment)의 일부로, JVM이 런타임에 클래스를 요청할 때, 클래스 로더는 클래스를 찾고 정규화된 이름을 사용해서 로드하게 된다. 이때 JVM은 Application ClassLoader(가장 하위의 자식 클래스 로더, Java8 버전까지는 Application ClassLoader라 부르고, Java 9 버전 이후에는 System ClassLoader 라고 부른다.)에게 최초의 요청을 보내는데, 클래스가 아직 로드되지 않았다면, 상위(부모) 클래스 로더에 책임을 위임하게 된다. 이러한 메커니즘을 'Delegation Model' 이라고 부르는 것이다.
아래에서는 Java 클래스 로더의 종류 3가지에 대해 알아보자!
- Bootstrap ClassLoader
: JVM 시작 시 가장 최초로 실행되는 클래스 로더로, Java 클래스를 로드하는 것이 아닌, Java 클래스를 로드할 수 있도록 하는 자바 자체의 클래스 로더와 최소한의 자바 클래스(java.lang.Object / Class / ClassLoader)만을 로드하는 역할을 수행.
- Java 8 : $JAVA_HOME/jre/lib/rt.jar 에 담긴 jdk 클래스 파일을 로딩.
- Java 9 이후 : rt.jar 등이 없어짐에 따라, 로딩할 수 있는 클래스의 범위가 전반적으로 축소되어, 정확하게 ClassLoader 내 최상위 클래스들만 로드하게 됨. - Extension ClassLoader (=Platform ClassLoader)
: 확장 자바 클래스들을 로드. java.ext.dirs 환경 변수에 설정된 디렉토리의 클래스 파일을 로드하고, 이 값이 설정되어 있지 않은 경우, $JAVA_HOME/jar/lib/ext 에 있는 클래스 파일을 로드하게 됨.
- Java 8 : URLClassLoader를 상속하며, $JAVA_HOME/jre/lib/ext 내 모든 클래스를 로드
- Java 9 이후 : jre/lib/ext, java.ext.dirs를 지원하지 않으며, Java SE의 모든 클래스 및 Java SE에는 없지만 JCP에 의해 표준화된 모듈 내의 클래스를 로드함으로써, Java 8에 비해 로드할 수 있는 범위가 확장됨.
URLClassLoader가 아닌 BuiltinClassLoader를 상속하여, Inner Static 클래스로 구현되어 있음. - Application ClassLoader (=System ClassLoader)
: 자바 프로그램 실행 시 지정한 Classpath 에 있는 클래스 파일 혹은 jar 에 속한 클래스들을 로드함. 개발자가 어플리케이션 구동을 위해 직접 작성한 대부분의 클래스가 이 클래스로더에 의해 로딩됨.
- Java 8 : URLClassLoader를 상속
- Java 9 이후 : URLClassLoader가 아닌 BuiltinClassLoader를 상속하여, ClassLoaders 클래스 내부 static 클래스로 구현.
위 클래스 로더의 종류를 종합하여, 클래스 로더의 동작 방식을 설명 하자면,
JVM의 메서드 영역에 클래스가 로드되어 있는지 확인 (로드 되어 있는 경우, 해당 클래스 사용)
-> 메서드 영역에 클래스가 로드되어 있지 않은 경우, 시스템 클래스 로더(최하위 클래스로더)에 클래스 로드를 요청
-> 시스템 클래스 로더는 확장 클래스 로더에 요청을 위임
-> 확장 클래스 로더는 부트스트랩 클래스 로더에 요청을 위임
-> 부트스트랩 클래스로더는 부트스트랩 ClassPath(JDK / JRE / LIB)에 해당 클래스가 있는지 확인후, 존재하지 않을 경우 확장 클래스 로더에 요청을 넘김
-> 확장 클래스 로더는 확장 ClassPath(JDK / JRE / LIB / EXT)에 해당 클래스가 있는지 확인후, 클래스가 존재하지 않을 경우 시스템 클래스 로더에게 요청을 넘김
-> 시스템 클래스 로더는 시스템 ClassPath에 해당 클래스가 있는지 확인 후, 클래스가 존재하지 않는 경우 ClassNotFoundException 에러를 발생시킴.
클래스 로더의 3가지 작동 원칙
Delegation Principle (위임 원칙)
- 클래스 로더는 클래스 또는 리소스를 찾기 위해 요청을 받았을 때, 상위 클래스 로더에게 책임을 위임하는 모델을 따른다.
Visibility Principle (가시범위 원칙)
- 하위 클래스로더는 상위 클래스로더가 로딩한 클래스를 볼 수 있지만, 상위 클래스로더는 하위 클래스 로더가 로딩한 클래스는 볼 수 없다.
- 이러한 원칙 덕분에 java.lang.Object, String.class 등 상위 클래스 로더에서 로드한 클래스도 하위 클래스 로더인 시스템 클래스 로더 등에서 사용할 수 있다.
Uniqueness Principle (유일성 원칙)
- 하위 클래스 로더가 상위 클래스 로더에서 로드한 클래스를 다시 로드하지 않아야 한다.
- 위임 원칙에 의해 상위 클래스로만 책임을 위임하기 때문에, 고유한 클래스를 보장할 수 있게 하는 원칙이다.
참고
https://steady-coding.tistory.com/593
https://engkimbs.tistory.com/606
'Java' 카테고리의 다른 글
[Java] JVM 실행 옵션 (3) - Advanced Runtime Options (0) | 2022.11.28 |
---|---|
[Java] Spring 프레임워크에서의 MVC 패턴과 레이어드 아키텍처 (0) | 2022.11.03 |
[Java] Spring Boot에서 H2 데이터베이스 설치 및 테스트 작동 확인하기 (0) | 2022.10.24 |
[Java] JVM 실행 옵션 (2) - Non-Standard Options (1) | 2022.10.04 |
[Java] JVM 실행 옵션 (1) - Standard Options (1) | 2022.09.30 |
최근댓글