JVM의 이해
JVM은 Java Virtual Machine의 약자로, 이름 그대로 Java 코드를 실행해주는 가상 머신이다. 일반적인 가상 머신이 하드웨어의 동작을 소프트웨어로 구현하여 호스트 컴퓨터에 다른 운영체제를 실행할 수 있는 것처럼, JVM 역시 호스트의 환경과 관계없이 Java 프로그램을 실행할 수 있는 환경을 제공해준다. 이러한 특징 덕분에 'Write Once, Run Anywhere'라는 문구가 나오기도 했다. 또한, 이름과 달리 Java 뿐만 아니라 Kotlin, Scala 등 다양한 언어들을 지원한다. 이러한 언어들은 Java가 서버 개발에 널리 사용된 이후에 Java의 부족한 부분들을 개선하기 위해서 등장했기 때문이다.
어쨌든 JVM을 사용하는 개발자라면 JVM의 구조에 대해 이해하고 있어야 한다. 내가 작성한 소스코드와 JVM에서 실제로 실행되는 코드는 많이 다를 수 있기 때문이다. 컴파일 과정과 클래스 로딩, 런타임에 다른 코드가 삽입될 수도 있고, JVM의 최적화로 변경될 수도 있다. 이러한 가능성을 미리 알고 활용하기 위해서는 JVM의 동작을 살펴볼 필요가 있다. JVM은 크게 클래스 로더, 실행 엔진, 런타임 데이터 영역으로 나뉘어진다. 이번 글에서는 클래스 로더에 대해 살펴보도록 하자.
컴파일
소스코드 작성이 끝났다면, 가장 먼저 할 일은 컴파일이다. 컴파일은 JVM에서 하는 일은 아니지만, JVM에서 실행할 바이트코드를 만드는 과정이다. 컴파일은 javac를 이용해서 할 수 있다. 예를 들어, Test.java 파일을 컴파일하려면 아래와 같이 할 수 있다.
public class Test {
public static void main(String[] args) {
System.out.println("Hello Java!");
}
}
javac Test.java
Test.class 파일이 생성된 것을 확인할 수 있다. 반대로 바이트코드의 내용을 보고싶다면 javap로 역컴파일 할 수도 있다.
javap -v Test
바이트코드는 JVM에서 실행할 수 있는 형태의 이진 표현으로, C코드를 컴파일했을 때 나오는 실행가능 파일과는 다르다. 바이트코드는 특정한 형식을 가지고 있으며, 클래스 로딩시에 이 형식을 검사하는 과정을 거친다. 바이트코드의 형식은 아래의 링크에 자세하게 설명되어 있다.
[참고] https://blog.lse.epita.fr//2014/04/28/0xcafebabe-java-class-file-format-an-overview.html
0xCAFEBABE ? - java class file format, an overview
Lately, we’ve been having a look into java. First, we tried to understand the file-format. A java application is often presented in a .jar, which is basically a zip archive (you can also find .war files which are also zip archive). Inside this archive yo
blog.lse.epita.fr
클래스 로딩
컴파일된 바이트코드를 실행하려면 Java 바이너리를 이용하면 된다.
java Test
위 명령어를 실행하면 Java 바이너리가 실행되고, JVM은 Test.class 바이트코드를 메모리에 로딩한다. 이것을 클래스 로딩이라고 하며, JVM의 클래스 로더에 의해 이루어진다.
클래스 로더의 종류
JVM의 클래스 로더는 3개의 클래스로더의 체인으로 구성된다. 먼저, Bootstrap ClassLoader는 이름 그대로 JVM이 시작될 때 필요한 클래스들을 로딩한다. java.util 패키지나 java.lang 패키지의 클래스들이 해당된다. Bootstrap ClassLoader는 Java가 아닌 native 코드로 작성되었다. 만약 Bootstrap ClassLoader가 Java로 작성되었다면 BootstrapClassLoader.class를 로딩하기 위한 또 다른 클래스 로더가 필요했을 것이다.
System.out.println(ArrayList.class.getClassLoader());
null
실제로, java.util의 ArrayList의 클래스 로더를 출력해보면 Java 클래스가 아니기 때문에 null이 나오는 것을 확인할 수 있다.
다음은 Extension ClassLoader인데, Java 9 이상에서는 Platform ClassLoader라는 이름으로 바뀌었다. Extension ClassLoader는 Bootstrap ClassLoader의 자식클래스로, 표준 Java 라이브러리들을 확장하는 클래스들을 로딩한다. 대표적으로 Java에서 JavaScript를 사용할 수 있는 nashorn, zip파일이나 jar파일을 파일시스템처럼 사용할 수 있는 zipfs 등이 여기에 해당한다.
ScriptEngine nashorn = new ScriptEngineManager().getEngineByName("nashorn");
nashorn.eval("print('Hello JavaScript!');");
System.out.println(nashorn.getClass().getClassLoader());
Hello JavaScript!
jdk.internal.loader.ClassLoaders$PlatformClassLoader@13e39c73
위 코드는 Java 11버전으로 실행한 결과이다.
마지막으로, Application ClassLoader는 지정된 classpath 위치에 있는 클래스들을 로드한다. 우리가 작성하고 컴파일한 바이트코드는 대부분 Application ClassLoader에 의해 로딩된다고 보면 된다. Application ClassLoader는 Java 9 이후로 System ClassLoader로 이름이 바뀌었다.
System.out.println(MyClass.class.getClassLoader());
jdk.internal.loader.ClassLoaders$AppClassLoader@6a6824be
Application ClassLoader와 Extension ClassLoader는 모두 Java로 구현되었으며, JVM의 일부가 아닌 일반적인 객체에 불과하다. 따라서 필요에 따라 클래스 로더를 커스텀해서 사용할 수도 있다.
클래스 로더의 동작
클래스 로더는 loading, linking, initialization의 3가지 과정을 통해 클래스를 로딩한다.
먼저 loading은 바이트코드를 찾아 JVM 메모리에 적재하는 과정이다. 클래스 로더가 로딩할 클래스를 찾을 때는 단순한 클래스 이름이 아니라, 패키지명을 모두 포함하는 FQCN(Fully Qualified Class Name)을 사용한다. 이것은 클래스 이름의 중복을 피하기 위해서다. 예를 들어, Point라는 클래스는 java.awt와 sun.security.ec.point 패키지에 중복으로 선언되어있다. FQCN을 사용하면 이러한 클래스들을 명확하게 구분할 수 있다.
또한, 클래스 로더는 JVM이 시작될 때 모든 클래스를 로딩하지 않는다. 실제로 해당 클래스가 사용이 될 때 lazy-loading을 한다. 이러한 이유로 Java 코드를 테스트 할 때는 warm up이 필요하다.
linking은 또 다시 3단계로 나뉜다. 먼저, 메모리에 있는 바이트코드가 유효한지 확인하는 verification 단계에서는 바이트코드가 JVM 스펙에 맞게 작성되었는지 확인한다. 다음으로, 해당 클래스의 static 필드를 기본값으로 초기화하는 preparation 단계를 거치고, 마지막으로 바이트코드 내부의 심볼릭 참조를 직접 참조로 바꾸는 resolution 단계로 이루어진다.
마지막 initialization은 static 필드를 지정한 값으로 초기화하고, static block을 실행하여 초기화하는 과정이다. linking에서도 static 필드를 초기화하는 과정이 있지만, 개발자가 초기화한 값이 아닌 변수의 기본 값으로 초기화하는 과정이다. 즉, reference type은 null, boolean은 false, 숫자 타입은 0과 같은 식이다. initialization 과정에서의 초기화는 실제 의도한 값을 대입해준다는 차이가 있다.
위에서 살펴본 3개의 클래스 로더들은 상속관계를 가지고 있다. Bootstrap ClassLoader의 자식이 Extension ClassLoader이고, Extension ClassLoader의 자식이 Application ClassLoader이다. 클래스를 로딩할 때는 loadClass 메소드를 호출하는데, 이 메소드는 부모 클래스에게 클래스 로딩을 위임한다. 따라서 가장 먼저 Bootstrap ClassLoader에서 로딩 할 수 있는지 확인하고, 불가능한 경우 Extension ClassLoader가 찾고, 마지막으로 Application ClassLoader가 찾는다. 만약 클래스가 존재하지 않으면 ClassNotFoundException을 던진다.
부모 클래스에게 위임하는 이러한 구조는 몇 가지 특징을 갖는다. 먼저, 하나의 클래스가 하나의 클래스 로더에 의해 로딩되도록 만들어준다. 만약 클래스 로더의 순서가 없다면 내가 작성한 코드를 Extension ClassLoader에서도 로딩하고, Application ClassLoader에서도 로딩하여 중복될 가능성이 있다. 그러나 위임을 사용하면 Extension ClassLoader에서 로딩된 경우 Application ClassLoader에서는 로딩하지 않기 때문에 중복을 방지할 수 있다.
또한, 자식 클래스 로더에 의해 로딩된 클래스들은 부모 클래스 로더에 의해 로딩된 클래스에 접근이 가능하지만, 반대로 부모 클래스 로더에 의해 로딩된 클래스는 자식 클래스 로더에 의해 로딩된 클래스에 접근할 수 없다. 이것은 복잡한 Java 어플리케이션에서 격리 수준을 제공하여 보안에 도움이 될 수 있다.
3줄 요약
- Java 소스코드를 컴파일하면 바이트코드가 된다
- 클래스 로더가 바이트코드를 JVM 메모리에 로딩한다
- 클래스 로더는 상속관계와 위임으로 동작하는 특징이 있다
'Java > 자바 최적화' 카테고리의 다른 글
[Java] 자바 최적화(6) - 가비지 컬렉션 기초 (0) | 2023.03.26 |
---|---|
[Java] 자바 최적화(5) - 마이크로 벤치마킹 (0) | 2023.03.23 |
[Java] 자바 최적화(4) - 런타임 데이터 영역 (0) | 2023.03.18 |
[Java] 자바 최적화(3) - 실행 엔진 (0) | 2023.03.17 |
[Java] 자바 최적화(1) - 성능 지표 (0) | 2023.03.15 |