티스토리 뷰

다른 시리즈 보기

 

 

 

ClassLoader - Dynamic Loading

저번 시간에 자바 컴파일러를 거쳐서 소스 코드(.java)에서 바이트 코드(.class) 파일까지는 만들었다. 이제 JVM 내부의 클래스로더가 런타임 시점에 JVM의 Memory area인 Runtime Data Area에 바이트 코드인 클래스 파일을 올리면 이를 이용하여 JVM의 Execute Engine이 사용할 수 있게 된다. 그러면, 클래스 파일 모두가 메모리에 올려질까? 그렇지않다.

 

자바는 동적으로 클래스를 읽어오므로, 프로그램이 실행 중인 런타임에서야 모든 코드가 JVM과 연결되는데, 이렇게 동적으로 클래스를 로딩해주는 역할을 하는 것이 바로 클래스 로더(class loader)이다. 클래스 로더는 클래스(.class) 파일을 묶어서 JVM이 Host Operating System으로부터 할당받은 메모리 영역인 Runtime Data Area로 적재한다.

 

동적으로 클래스를 읽어온다는 것은, 간단하게 "자바 클래스의 코드가 런타임 시에 필요할 때 메모리에 로드되는 것"을 의미한다. 이를 동적 로딩(Dynamic Loading)이라고 하며, 이를 통해 자바 애플리케이션에서 코드를 실행하는 동안 클래스 파일을 동적으로 로드하고 초기화할 수 있다. 동적 로딩은 아래와 같이 두 가지 방식이 있다.

 

  • Load Time Dynamic Loading : 클래스를 로드하는 과정에서 그와 연관된 다른 클래스를 한 번에 동적으로 로드하는 것
  • Run Time Dynamic Loading : Reflection 같이 실제로 메서드가 실행될 동적으로 로드하는

 

그림1. oracle docs - Class Loader 동작 과정

 

그림2. ClassLoader 동작 과정

 

JVM 클래스로더는 클래스 파일을 load, link, initialize하는 3가지 단계로 메모리에 올리는데 이를 상세히 보도록 하자.

 

 

ClassLoader 작동 원리 - Load

로드하는 단계에서는 JVM이 바이너리로 표현된 특정 이름을 가진 클래스 혹은 인터페이스를 찾고, 이를 생성하는 과정을 진행한다. 클래스 로더에는 계층 구조를 가지는 클래스로더가 있고 작동 원칙이 있다. 클래스로더의 분리를 통해 JVM에서 클래스들이 독립적으로 로드되어 충돌을 방지하고 메모리 관리를 용이하게 할 수 있게 되었다.

 

각 클래스로더는 로드된 클래스들을 네임스페이스(namespace)라는 장소에 보관하고, 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위하여 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉, 다른 클래스로더가 로드한 클래스면 다른 클래스로 간주한다는 의미이다. 클래스를 로드하는 ClassLoader를 알기 위하여 getClassLoader() 메서드를 사용할 수 있다. 모든 클래스는 바이너리 이름에 따라 로드되는데, 이러한 클래스 중 하나라도 발견되지 않는다면 NoClassDefFoundError나 ClassNotFoundException을 반환한다.

 

추가로, 클래스 로더는 Java 8과 Java 9 이후에서 약간의 차이가 있다. 이는 [Java의 실행원리 1편] Compile-time 환경에서 언급한 Java 9부터 모듈 시스템의 도입으로 인한 변화 때문이다.

 

 이제 클래스로더를 상세하게 살펴보자.

 

그림3. Java 8 Class Loader의 계층구조

 

그림4. Java 8 Class Loader 로드 대상

 

Bootstrap Class Loader

 

  • 클래스로더 중 최상위 계층의 클래스로더이며, JVM이 실행될 때 Java 애플리케이션에 필요한 기본적인 class 들을 로드한다.
  • Java 8에서는 모든 Java 플랫폼 API, Java 애플리케이션을 함께 패키징한 rt.jar 파일을 로드한다. (ex. java.lang )
  • 클래스로더 중 유일하게 Native C로 구현되어있다. 따라서, Java 코드에 의해 인스턴스화가 불가능하다는 뜻이라서 String.class.getClassLoader()는 null을 반환한다.

 

Extension Class Loader

 

  • 주로, jre/lib/ext에 있는 .jar 파일들을 로드한다. JDK가 추가적으로 제공하는 라이브러리를 로드하는 것이다.
  • Java로 구현되어있다.

 

Application Class Loader

 

  • classpath에 설정된 클래스를 로드한다. 개발자가 -cp 명령어를 통해 직접 클래스 경로를 명시할 수도 있다.
  • JAR 파일 내부의 Manifest의 Class-Path 속성값으로 지정된 폴더에 있는 클래스도 로드한다.
  • Java로 구현되어있다.
  • 개발자가 애플리케이션에 작성한 대부분의 클래스는 이곳을 통해 로드된다.

 

User-defined Class Loader

 

  • 개발자에 의해 정의된 사용자 정의 클래스로더로, JVM의 기본 클래스로더는 아니다.
  • 모든 사용자 정의 클래스로더는 추상 클래스인 ClassLoader의 하위 인스턴스이다. 이는 오라클 공식문서의 5.3에 "Every user-defined class loader is an instance of a subclass of the abstract class ClassLoader"라고 명시된 내용이다.
  • 클래스로더를 만들어서 사용하는 것은 클래스 로딩을 제어하고, 클래스를 동적으로 로드하고 언로드하며, 클래스를 다른 원격서버에서 로드할 있도록 하기 위함이다. 예를 들어, 특정 조건을 충족하지 않으면 클래스를 로드하지 않도록 수도 있다스택오버플로우의 답변 추가적으로 참고하자.

 

Java.lang.ClassLoader 메서드

 

  • loadClass(String name, boolean resolve) : JVM이 참조하는 클래스를 로드할 때 사용한다. 이때, 클래스 이름을 매개변수로 사용한다.
  • defineClass() : final 메서드라서 재정의할 수 없다. 바이트 배열을 클래스의 인스턴스로 정의하는데 사용하는데, 클래스가 유효하지 않으면 ClassFormatError가 발생한다.
  • findClass(String name) : 지정된 클래스를 찾을 때 사용한다. 클래스를 찾기만하고 로드 하지는 않는다.
  • findLoadedClass(String name) : JVM이 참조하는 클래스가 이전에 로드되었는지 여부를 확인한다.
  • Class.forName(String name, boolean initialize, ClassLoader loader) : 클래스를 로드할 뿐만 아니라 초기화하는 데에도 사용된다. 또한, ClassLoader 중 하나를 선택할 수 있는 옵션을 제공하는데 만약 ClassLoader 매개변수가 null이면 Bootstrap ClassLoader가 사용된다.

 

 

이러한 클래스로더는 아래의 원칙에 의하여 작동한다.

 

 

Delegation Model (위임 모델)

 

그림5. geeksforgeeks.org에 명시된 그림

 

  • 클래스로더는 위임-우선 모델이라는 방식을 따른다. 상위 계층의 클래스로더에게 클래스 로드 요청을 위임하고 상위 계층에서 클래스를 찾을 수 없거나 로드할 수 없는 경우에는 하위 계층 클래스로더에서 로드하는 방식이다. 만약, 하위 계층 클래스로더에서도 클래스를 찾을 수 없다면, ClassNotFoundException을 발생시킨다.
  • 과정을 살펴보면, 개발자가 작성한 Main 클래스를 로딩하는 과정에서 Application ClassLoader → Extension ClassLoader → Bootstrap ClassLoader로 loadClass() 메서드를 위임하고 상위 계층의 클래스로더에서 loadClass() 메서드로 클래스를 찾지 못하면 하위 계층으로 내려간다.
  • Bootstrap ClassLoader → Extension ClassLoader → Application ClassLoader 순으로 클래스를 찾고, 찾지 못하면 최종적으로 ClassNotFoundException 발생시킨다. 이렇게 런타임 과정에서 JVM 예외를 발생시키기 때문에 명시적으로 처리하지 않으면 안돼서 ClassNotFoundException Runtime Exception 상속하지 않는 Checked Exception 것이다.

 

Visibility Principle (가시성 원칙)

 

  • 가시성 원칙은 하위 계층의 클래스로더는 상위 계층의 클래스로더가 로드한 클래스를 볼 수 있지만, 상위 계층의 클래스로더는 하위 계층의 클래스로더가 로드한 클래스를 볼 수 없는 것을 의미한다.
  • 쉽게 말해서, 최상위 계층의 클래스로더인 Bootstrap ClassLoader에서 rt.jar을 로드하고 개발자가 작성한 클래스는 Application ClassLoader가 로드한다고 하였다. rt.jar 내부에는 java.lang 같이 우리가 흔히 사용하는 라이브러리가 있는데, 만약, 개발자가 작성한 Main 클래스에서 상위 계층 클래스로더가 로드한 java.lang.String을 볼 수 없다면 String을 사용할 수 없을 것이다.
  • 그런데 java.lang.String에서는 Main 클래스를 굳이 볼 필요가 없다. 이런 이유로 하위에서 상위를 볼 수 있지만, 상위에서는 하위를 볼 수 없는 것이 가시성 원칙이다. 

 

Uniqueness Property (유일성 속성)

 

  • 유일성 속성은 클래스를 정확히 "한 번"만 로드하여 클래스가 유일하고 반복되지 않도록 하는 것이다.
  • 하위 계층의 클래스로더가 상위 계층의 클래스로더가 이미 로드한 클래스를 다시 로드하지 않아야된다는 의미이다. 이는 클래스의 유일성을 보장하도록 하는 것이다.
  • 유일성을 식별하는 기준은 클래스의 바이너리 이름이다. 에를 들어, toString() 메서드를 찍어봤을 때 java.lang.String이라고 나오는 것을 말한다.

 

Unload Impossibility (언로드 불가)

 

  • 클래스로더는 클래스를 로드할 수는 있지만, 언로드 할 수는 없다. 일반적으로 클래스로더가 로드한 클래스는 Garbage Collector의 수거 대상이 되지 않기 때문이다.
  • 대신, 현재 클래스로더를 삭제하고 아예 새로운 클래스로더를 생성하는 방법을 사용할 수 있다. 위에서 User-defined ClassLoader를 사용하는 이유에 대해서 클래스를 동적으로 로드하고 언로드하기 위하여 이야기했던게 바로 이것이다.

 

 

이렇게 클래스로더와 원칙에 관하여 이해하면 클래스로더가 Byte Code(.class)로부터 클래스를 로드하는 과정을 이해할 있다. 위에서 잠깐 언급한 내용인데 Java 8 클래스로더와 Java 9 이후는 어떻게 클래스로더가 다를까? Java 9에서는 모듈 시스템이 도입되었다고 했었다. 이로 인하여 무겁던 rt.jar 사라지고 로드할 있는 클래스의 범위가 전반적으로 축소되면서 클래스로더 별로 담당하는 부분이나 클래스로더의 이름이 바뀌게 되었다.

 

그림6. https://hackernoon.com/understanding-java-9-modules-7f573vfe에 명시된 그림

 

그림7. Java 9부터 Class Loader의 계층구조

 

Bootstrap Class Loader

 

  • Java 9부터의 모듈 시스템으로 rt.jar과 tools.jar 등이 없어지면서 로드할 수 있는 클래스의 범위가 전반적으로 축소되었다.
  • rt.jar과 tools.jar 안에 있던 내용들은 모듈 시스템에 맞게 더 효율적으로 재편되어 lib 디렉토리에 저장되었다.

 

Platform Class Loader

 

  • Extension ClassLoader → Platform ClassLoader로 이름이 변경되었다.
  • Java 9부터의 모듈 시스템으로 jre/lib/ext 등이 없어지면서 jre/lib/ext를 지원하지 않는다.
  • Java SE 플랫폼의 모든 클래스를 Platform ClassLoader를 통해 볼 수 있다.
  • Java SE 플랫폼이 아니지만, JCP(Java Community Process)에 의해 표준화 된 모듈 내의 클래스도 볼 수 있다.
  • 결과적으로 Java 8의 Extension ClassLoader에 비해 볼 수 있는 범위가 확장되었다.

 

System Class Loader

 

  • Application ClassLoader →  System ClassLoader로 이름이 변경되었다.
  • Class-Path 더불어 Module-Path 있는 클래스를 로드한다.

 

 

ClassLoader 작동 원리 - Link, Initialize

그림8. ClassLoader의 Link, Initialize 과정

 

Load 단계에서 Byte Code(.class) 파일을 가져와서 JVM의 메모리에 로드하고 난 이후, Link 단계에서는 Byte Code(.class) 파일 내의 symbolic references를 실제 메모리 주소로 변환하는 작업을 수행한다. 

 

Verifying (검증) : 읽은 Byte Code(.class) 파일이 Java 명세 및 JVM 명세에 명시된 대로 잘 구성되었는지 검사한다. 클래스 로드의 전 과정에서 가장 까다로운 검사를 수행하는 과정이라서 가장 복잡하고 시간이 많이 걸린다. Bytecode format, version number 등을 확인하는 단계이다. 만약 검증이 실패한다면 런타임 에러(java.lang.VerifyError)가 발생한다.

 

Preparing (준비) : 클래스가 필요로 하는 메모리를 할당하는 단계이다. 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다. 이때 중요한 점은 static variable이 default로 초기화된다는 것이다. 예를 들어, boolean 타입의 static variable을 true로 설정하더라도 preparing 단계에서 해당 변수는 false로 초기화된다.

 

Resolving (분석) : Resolving 단계에서는 class, field, method 그리고 constant pool 내 모든 symbolic references를 실제 메모리 주소로 변환한다. symbolic reference란 우리가 코드를 작성하면서 사용한 class, field, method의 이름을 지칭한다.

 

Link 단계가 끝나면 Initializing(초기화)을 실시하는데, static initializer들을 수행하고 statc variable들을 개발자가 설정한 값으로 초기화한다는 의미이다.

 

 

정리하자면, 최종적으로 Byte Code(.class) 파일이 클래스로더에 의해 JVM 메모리를 할당받고 올라가면서 JVM 메모리인 Runtime Data Areas에서 본격적으로 사용할 준비가 끝난 것이다. 3편에서 어떻게 사용되는지 본격적으로 알아보자.

 

 

참고

 

 

※ 본 게시글은 공부하면서 작성했기에 틀린 부분이 있을 수 있습니다. 질문 사항이나 댓글로 알려주시면 감사하겠습니다.

댓글