ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • CH2. JVM 이야기
    DevBook/Optimizing Java 2024. 4. 14. 00:13

    이 장에서는 JVM이 자바 코드를 실행하는 방법을 소개한다.

    성능을 공부하려면, 기본 JVM 기술 스택의 구조를 알아야 한다.

     

    들어가기 전에, JVM을 큰 그림으로 한번 보자.

     

     

    2.1 인터프리팅과 클래스로딩

    • JVM은 스택 기반의 해석 머신
    • 레지스터는 없지만 일부 결과를 실행 스택에 보관하며, 이 스택의 맨 위에 쌓인 값들을 가져와 계산을 함

     

    1) 인터프리팅

    • JVM 인터프리터(해석기)의 기본 로직은 'while 루프 안의 switch문' 으로 비유할 수 있음
      • 이는 JVM이 바이트코드를 실행하는 방식을 설명하기 위한 비유임 
      • 각 바이트코드에 대한 실행 로직은 switch문에서 결정되며, while 루프를 통해 계속해서 새로운 바이트코드를 읽어들여 실행
        • while 루프
          • 프로그램이 계속해서 실행되는 루프
          • JVM이 실행 중에 계속해서 명령어를 해석하고 실행하는 과정을 나타내고, JVM은 바이트코드를 한 줄씩 읽어들여 실행하는 동작을 반복함
        • switch문
          • 현재 실행 중인 바이트코드의 종류를 판별하여 해당하는 작업을 수행하는 역할
          • 바이트코드의 종류에 따라 다른 작업이 이루어지므로, switch문을 통해 각 바이트코드에 대한 실행 로직이 결정됨
      • 평가 스택을 이용해 중간값들을 담아두고 가장 마지막에 실행된 명령어와 독립적으로 프로그램을 구성하는 opcode를 하나씩 순서대로 처리

     

    2) 클래스로딩

    클래스로딩이 필요한 이유

    • 자바 어플리케이션을 실행하면 운영체제는 가상 머신 프로세스(자바 바이너리)를 구동함
    • 자바 가상 환경이 구성되고 스택 머신이 초기화된 다음, 실제로 유저가 작성한 클래스 파일이 실행됨
    • 어플리케이션의 진입점(entry point)은 HelloWorld.class에 있는 main() 메서드이고, 제어권을 이 클래스로 넘기려면 가상 머신의 실행이 시작되기 전에 이 클래스를 로드해야
    • 여기에 자바 클래스로딩 메커니즘이 관여

     

    클래스로더 알아보기

    • 자바 프로세스가 새로 초기화되면 줄지어 연결된 클래스로더가 차례차례 작동함
    • 작동 순서 : 부트스트랩 클래스로더 --> 확장 클래스로더 --> 어플리케이션 클래스로더
    • 클래스로더별 특징
      • 부트스트랩 클래스로더 (Bootstrap Class Loader)
        • 부트스트랩 클래스가 자바 런타임 코어 클래스를 로드
          • 런타임 코어 클래스는 자바 8 이전까지는 rt.jar 파일에서 가져오지만, 자바 9 이후부터는 런타임이 모듈화되고 클래스로딩 개념 자체가 많이 달라짐
          • 어떻게 달라졌을까?
            • Java 9에서는 모듈 시스템이 도입되었음. 모듈 시스템은 코드를 모듈화하여 라이브러리 간의 종속성을 잘 관리할 수 있도록 도움
            • 이로인해 부트스트랩 클래스로더가 모듈 시스템의 모듈 경로를 검사하도록 변경되었음
            • 이전에는 Java runtime Jar 파일(rt.jar)과 같은 기본적으로 제공되는 클래스를 로드했지만, Java 9부터는 모듈 경로에서 모듈을 로드함. 따라서 런타임 코어 클래스를 이러한 모듈에서 로드하게 됨
        • 부트스트랩 클래스로더의 주임무는 다른 클래스로더가 나머지 시스템에 필요한 클래스를 로드할 수 있게 최소한의 필수 클래스(ex: java.lang.Object, Class, Classloader)만 로드
      • 확장 클래스로더 (Extension Class Loader)
        • 부트스트랩 클래스로더를 자기 부모로 설정하고 필요할 때 클래스로딩 작업을 부모에게 넘김
        • 널리 쓰이지는 않지만, 확장 클래스로더를 이용하면 특정한 OS나 플랫폼에 네이티브 코드(native code)를 제공하고 기본 환경을 오버라이드할 수 있음
          • 자바 8에 탑재된 자바스크립트 런타임 내시혼(Nashorn)을 확장 클래스로더가 로드함
      • 어플리케이션 클래스로더 (Application Class Loader)
        • 끝으로 해당 클래스로더가 생성되고 지정된 클래스패스에 위치한 유저 클래스를 로드
        • 주의
          • 시스템 클래스는 로드하지 않기 때문에 어플리케이션 클래스로더를 '시스템' 클래스로더라고 부르지 않도록 주의하자!

     

    클래스로딩 방식

    • 자바는 프로그램 실행 중 처음 보는 새 클래스를 dependency(의존체)에 로드
      • 추가하자면, 이는 자바의 '동적 로딩'을 의미
      • 자바 프로그램이 실행될 때, 프로그램이 사용하는 클래스들은 필요할 때 동적으로 로드됨
      • 이때 자바는 프로그램 실행 중 처음으로 사용되는 클래스를 발견하면 해당 클래스와 그 클래스가 의존하는 다른 클래스들을 메모리에 로드함
    • 클래스를 찾지 못한 클래스로더는 기본적으로 자신의 부모 클래스로더에게 대신 룩업(찾아보기)을 넘김
    • 이렇게 부모의 부모로 거슬러 올라가 결국 부트스트랩도 룩업하지 못하면 ClassNotFoundException 예외가 발생
    • 따라서 빌드 프로세스 수립 시 운영 환경과 동일한 클래스패스로 컴파일하는 것이 좋음

    cf)

    https://velog.io/@ddangle/Java-%ED%81%B4%EB%9E%98%EC%8A%A4-%EB%A1%9C%EB%8D%94%EB%9E%80

    https://oneny.tistory.com/51

     

    2.2 바이트코드 실행

    • 자바 소스 코드는 실행되기까지 많은 변환 과정을 거침
    • 첫 단계는 자바 컴파일러 javac를 이용해 컴파일하는 것으로, 보통 전체 필드 프로세스의 한 부분으로 수행함

     

    javac의 역할

    • 자바 소스 코드를 바이트코드로 구성된 .class 파일로 바꾸는 것
    • javac는 컴파일하는 동안 최적화하는 거의 하지 않기 때문에 결과로 생성된 바이트코드는 쉽게 해독할 수 있음
    • 바이트코드는 특정 컴퓨터 아키텍처에 특정하지 않은, 중간 표현형(Intermediate Representation)임
      • 컴퓨터 아키텍처의 지배를 받지 않으므로 이식성이 좋아 컴파일된 소프트웨어는 JVM 지원 플랫폼 어디서든 실행 가능하고 자바 언어에 대해서도 추상화되어 있음

     

    컴파일러가 생성한 클래스 파일 구조

    • JVM은 클래스를 로드할 때 올바른 형식을 준수하고 있는지 검사

    cf) https://blog.lse.epita.fr/2014/04/28/0xcafebabe-java-class-file-format-an-overview.html

    • 매직넘버 (Magic)
      • 모든 클래스 파일은 해당 파일이 클래스 파일(.class)임을 나타내는 4바이트 16진수인 0xCAFEBABE라는 매직 넘버로 시작
    • 클래스 파일 포맷 버전 (Major/Minor version)
      • 그 다음 4바이트는 클래스 파일을 컴파일할 때 꼭 필요한 메이저/마이너 버전 숫자임
      • 클래스를 실행하는 대상 JVM이 컴파일한 JVM보다 버전이 낮으면 안되기 때문에 클래스로더의 호환성 보장을 위해 검사하고 호환되지 않는 버전의 클래스 파일을 만나면 런타임에 UnsupportedClassVersionError 예외가 발생
    • 상수 풀 (Constant Pool)
      • 코드에 등장하는 상수값(ex: 클래스명, 인터페이스명, 필드명)이 포함됨
      • JVM은 코드를 실행할 때 런타임에 배치된 메모리 대신, 상수 풀 테이블을 찾아보고 필요한 값을 참조함
    • 엑세스 플래그 (Access Flags)
      • 클래스에 적용한 수정자를 결정함
      • 구성
        • 플래그 첫 부분은 일반 프로퍼티로, public 클래스인지, 그 다음은 상속이 금지된 final 클래스인지 나타냄
        • 이 클래스 파일이 인터페이스인지, 추상 클래스인지 표시함
        • 플래그 끝 부분은 클래스 파일이 소스 코드에 없는 합성 클래스인지, annotation 타입인지, enum인지 각각 나타냄
    • this 클래스, 슈퍼클래스, 인터페이스 엔트리
      • 클래스에 포함된 타입 계층을 나타내며, 각각 상수 풀을 가리키는 인덱스로 표시함
    • 필드, 메서드
      • 시그니처 비슷한 구조를 정의하고 여기에 수정자도 포함되어 있음
    • 속성
      • 더 복잡하고 크기가 고정되지 않은 구조를 나타내는데 쓰임
      • ex) 메서드는 Code 속성으로 특정 메서드와 연관된 바이트코드를 나타냄

     

    예시로 살펴보기

    > 자바 코드

    public class HelloWorld {
        public static void main(String[] args) {
            for (int i = 0; i < 10; i++) {
                System.out.println("Hello World");
            }
        }
    }

     

    > 클래스 파일

    • javap라는 역어셈블러를 이용해 클래스 파일 내부를 볼 수 있음
      • javap란
        • 클래스 파일을 분석해 해당 클래스의 구조를 보여주는 데 사용됨
        • 클래스 파일(javac에 의해 컴파일된 자바 코드)의 바이트코드를 디코딩하여 해당 클래스의 구조와 멤버를 표시함 (클래스의 메서드, 필드, 상위 클래스, 인터페이스 등을 보여줌)
          • -v 옵션을 추가해 클래스 파일 헤더 전체 정보, 상수 풀 세부 정보 등 더 자세한 내용까지 확인 가능
    Compiled from "HelloWorld.java"
    public class HelloWorld {
      public HelloWorld();
        Code:
           0: aload_0
           1: invokespecial #1                  // Method java/lang/Object."<init>":()V
           4: return
    
      public static void main(java.lang.String[]);
        Code:
           0: iconst_0
           1: istore_1
           2: iload_1
           3: bipush        10
           5: if_icmpge     22
           8: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
          11: ldc           #13                 // String Hello World
          13: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
          16: iinc          1, 1
          19: goto          2
          22: return
    }
    • 소스 파일에는 메서드가 main() 하나뿐이지만 컴파일 후 변환된 클래스 파일에는 javac가 클래스 파일에 디폴트(기본) 생성자를 자동 추가하므로 메서드가 총 2개 생성
    • public HelloWorld();
      • aload_0 : 제일 먼저 생성자에서 this 레퍼런스를 스택 상단에 올려놓음
      • invokespecial : super 생성자들을 호출하고 객체를 생성하는 등 특정 작업을 담당하는 인스턴스 메서드 실행
      • HelloWorld 클래스는 디폴트 생성자를 오버라이드한 코드가 없으므로 Object 디폴트 생성자가 매치됨
    • public static void main(java.lang.String[]);
      • iconst_0 : 정수형 상수 0을 평가 스택에 push
      • istore_1 : 위 상수값을 오프셋 1에 위치한 지역 변수(루프의 i)에 저장
        • 지역 변수 오프셋은 0부터 시작하며, 인스턴스 메서드에서 0번째 엔트리는 무조건 this임
      • iload_1 : 오프셋 1의 변수를 스택으로 다시 로드
      • bipush  : 상수 10을 push
      • if_icmpge : 두 값을 비교함 (정수값이 10보다 같거나 큰가)
      • getstatic : System.out의 정적 메서드 해석
      • ldc : 상수 풀에서 "Hello World"라는 문자열을 로드
      • invokevirtual : 이 클래스에서 속한 인스턴스 메서드 실행 
      • iinc : 정수값 1 증가
      • goto : 2번 명령(iload_1)으로 되돌아감
      • 해당 과정이 if_icmpge 테스트가 성공할 때(i >= 10일 경우)까지 반복되다가 마지막에 22번 명령으로 제어권이 넘어가 메서드가 반환됨

     

    2.3 핫스팟 입문

    Java HotSpot

    • 핫스팟은 프로그램의 런타임 동작을 분석하고 성능에 가장 유리한 방향으로 영리한 최적화를 적용하는 가상 머신
    • 핫스팟 VM의 목표는 개발자가 자연스럽게 자바 코드를 작성하고 바람직한 설계 원리를 따르도록 하는 것
    • 자바 핫스팟 등장 이후 자바는 C/C++ 같은 언어에 필적할 만한 성능을 보여주며 진화를 거듭함

     

    사전 개념

    • 언어는 크게 2가지로 구분할 수 있음
      • '제로 코스트(비용이 들지 않는) 추상화' 사상에 근거한 '기계에 가까운' 언어 - a)
      • 개발자의 생산성에 무게를 두고 엄격한 저수준 제어 '일을 대행하는' 언어 - b)
    • a)의 특징
      • 컴퓨터와 OS가 실제로 어떻게 작동해야 하는지 개발자가 저수준까지 세세하게 일러주어야 함
      • 해당 언어로 작성한 소스 코드를 빌드하면 플랫폼에 특정한(해당 플랫폼에서만 사용 가능한) 기계어로 컴파일됨
        • 이것은 AOT(Ahead-Of-Time, 사전) 컴파일 이라고 함

     

    2.3.1 JIT 컴파일러란?

    등장 배경

    • 자바 프로그램은 바이트코드 인터프리터가 가상화한 스택 머신에서 명령어를 실행하며 시작됨
    • CPU를 추상화한 구조라서 다른 플랫폼에서도 클래스 파일을 문제없이 실행할 수 있지만, 프로그램이 성능을 최대로 내려면 네이티브 기능을 활용해 CPU에서 직접 프로그램을 실행시켜야

     

    역할

    • 이를 위해 핫스팟은 프로그램 단위(메서드와 루프)를 인터프리티드 바이트코드 --> 네이티브 코드로 컴파일
    • 해당 기술이 바로 JIT(Just-In-Time, 적시, 그때그때 하는) 컴파일

     

    동작 방식

    • 핫스팟은 인터프리티드 모드로 실행하는 동안 어플리케이션을 모니터링하며 가장 자주 실행되는 코드 파트를 발견해 JIT 컴파일을 수행함
    • 이렇게 분석을 하는 동안 미리 프로그래밍한 추적 정보가 취합되며 더 정교하게 최적화할 수 있음
    • 특정 메서드가 어느 한계치(임계점)를 넘어가면 프로파일러가 특정 코드 섹션을 컴파일/최적화

     

    장점

    • 무엇보다 컴파일러가 해석 단계에서 수집한 추적 정보를 근거로 최적화를 결정하는 것이 가장 큰 장점임
      • 상황별로 수집한 다양한 정보를 토대로 핫스팟이 더 올바른 방향으로 최적화 가능
    • 자바처럼 프로필 기반 최적화(PGO, profile-guided optimization)를 응용하는 환경에서는 대부분의 AOT 플랫폼에서 불가능한 방식으로 런타임 정보를 활용할 수 있어 동적 인라이닝(dynamic inlining) 또는 가상 호출(virtual call) 등으로 성능을 개선할 수 있음
    • 핫스팟 VM은 시동 시 CPU 타입을 정확히 감지해 가능하면 특정 프로세서의 기능에 맞게 최적화를 적용할 수 있음
      • 프로세서 기능을 정밀하게 감지하는 기법은 JVM 인트린직(intrinsics)라고 함

     

    2.4 JVM 메모리 관리

    • 핫스팟의 컴파일 서브 시스템과 더불어 초창기부터 자바를 독보적인 언어로 만들었던 특징은 바로 '자동 메모리 관리' 기능
    • 자바는 'Garbage Collection' 이라는 프로세스를 이용해 힙 메모리를 자동 관리
    • 가비지 수집이란 한 마디로, JVM이 더 많은 메모리를 할당해야 할 때 불필요한 메모리를 회수하거나 재사용하는 불확정적(nondeterministic) 프로세스
    • GC는 자바 성능 최적화의 중심 주제이므로 6, 7, 8장에서 자세하게 다룰 예정..!

     

    2.5 스레딩과 자바 메모리 모델(JMM)

    자바의 멀티스레드

    • 자바는 1.0부터 멀티스레드 프로그래밍을 기본 지원함
    • 원래 자바 환경 자체가 JVM처럼 멀티스레드 기반인 까닭에 자바 프로그램이 작동하는 방식은 복잡해졌고 성능 분석 작업을 하기도 까다로워졌음
    • 주류 JVM 구현체에서 자바 어플리케이션 스레드는 각각 정확히 하나의 전용 OS 스레드에 대응
    • 공유 스레드 풀을 이용해 전체 자바 어플리케이션 스레드를 실행하는 방안(그린 스레드)도 있지만, 만족할 만한 수준의 성능은 나오지 않는 것으로 밝혀짐 --> 이를 계기로 JDK 21의 virtual thread가 등장하게 된 것..!

     

    자바 메모리 모델(JMM)

    • 1990년대 후반부터 자바의 멀티스레드 방식은 다음 3가지 기본 설계 원칙에 기반
      • 자바 프로세스의 모든 스레드는 가비지가 수집되는 하나의 공용 힙을 가진다.
      • 한 스레드가 생성한 객체는 그 객체를 참조하는 다른 스레드가 액세스할 수 있다.
      • 기본적으로 객체는 변경 가능(mutable)하다. 즉, 객체 필드에 할당된 값은 final 키워드로 불변(immutable) 표시하지 않는 이상 바뀔 수 있다.
    • JMM은 서로 다른 실행 스레드가 객체 안에 변경되는 값을 어떻게 바라보는지를 기술한 공식 메모리 모델
    • ex) 스레드 A와 스레드 B가 둘 다 객체 obj를 참조할 때 스레드 A가 obj 값을 바꾸면 스레드 B는 무슨 값을 참조하게 될까요?
      • OS 스케줄러가 CPU 코어에서 강제로 스레드를 방출 할 수 있기 때문에 의외로 복잡하다. 스레드 A가 아직 처리 중인 객체를 스레드 B가 시작되면서 참조할 때 잘못된, 무효 상태의 객체를 바라보게 될 수 있다.
      • 상호 배타적 락(mutual exclusion lock)은 코드가 동시 실행되는 도중 객체가 손상되는 현상을 막을 수 있는 자바의 유일한 방어 장치지만 실제로 어플리케이션에 사용하려면 복잡하다.
      • JMM의 상세한 작동 원리와 실제로 스레드/락을 다루는 방법은 12장에서 소개한다..!

     

    2.6 JVM 구현체 종류

    오라클이 제작한 핫스팟 이외에도 제각기 다른 방법으로 구현한 자바 구현체가 많음

    • OpenJDK, 오라클 자바(Oracle), 줄루(Zulu), 아이스티(IcedTea), 징(Zing), J9, 애비안(Avian), 안드로이드(Android)

     

    2.7 JVM 모니터링과 툴링

    • JVM은 실행 중인 어플리케이션을 instrumentation, 모니터링, 관측하는 다양한 기술을 제공함
      • instrumentation : 오류 진단이나 성능 개선을 위해 어플리케이션에 특정한 코드를 끼워 넣는 것
    • 이런 종류의 툴에 쓰이는 몇 가지 중요한 기술 리스트
      • JMX (Java Management Extensions, 자바 관리 확장)
      • Java Agent
      • JVMTI (JVM 툴 인터페이스)
      • SA (Serviceability Agent)

     

    JMX

    • JVM을 관리하는 기본 수단으로, JVM과 그 위에서 동작하는 어플리케이션을 제어하고 모니터링하는 강력한 범용 툴
    • 메서드를 호출하고 매개변수를 변경할 수 있음

    Java Agent

    • 자바 언어로 작성된 툴 컴포넌트로, java.lang.instrument 인터페이스로 메서드 바이트코드를 조작
    • 다음과 같이 JVM에 시작 플래그를 추가해 설치함
      • -javaagent:<에이전트 jar 파일이 위치한 경로>=<옵션>
    • 특징
      • 에이전트 jar 파일에서 manifest.mf 파일은 필수임
      • Premain-Class 속성에 에이전트 클래스명을 반드시 지정해야 하고, 이 클래스는 자바 에이전트의 등록 후크 역할을 수행하는 public static premain() 메서드를 구현해야 함
        • 자바 instrument API는 JVM이 로드한 바이트코드를 조작하는 기능을 제공하며, premain() 메서드를 이름처럼 어플리케이션 main() 메서드 이전에 실행됨

    JVMTI

    • 자바 instrument API로 가져올 수 없는 정보의 경우 JVMTI를 대신 사용할 수 있음
    • JVMTI는 JVM의 네이티브 인터페이스이기 때문에 JVMTI를 사용하는 에이전트는 C/C++ 같은 네이티브 컴파일 언어로 작성해야 함
      • 네이티브 에이전트가 JVM 이벤트를 모니터링하며 알림을 받을 수 있도록 만든 통신 인터페이스이기 때문.
    • 네이티브 에이전트 설치 플래그
      • -agentlib:<에이전트 라이브러리명>=<옵션>
      • -agentpath:<에이전트 경로>=<옵션>
    • 특징
      • JVMTI 에이전트를 네이티브 코드로 개발해야 하므로 실행중인 어플리케이션 및 JVM에 영향을 미치지 않도록 주의해야 함

    SA

    • 자바 객체, 핫스팟 자료 구조 모두 표출 가능한 API와 툴을 모아놓은 것
    • SA를 이용하면 대상 JVM에서 코드를 실행할 필요가 없음
    • 핫스팟 SA는 symbol lookup 같은 기본형을 이용하거나 프로세스 메모리를 읽는 방식으로 디버깅함
    • SA는 코어 파일(크래시 덤프 파일) 및 아직 실행중인 자바 프로세스까지 디버깅할 수 있음

     

    2.7.1 VisualVM

    • NetBeans 플랫폼 기반의 시각화 툴인 VisualVM은 jconsole을 대체하는 툴임
    • VisualVM은 JVM attach mechanism 을 이용해 실행 프로세스를 실시간 모니터링
      • attach mechanism : 자바 Attach API를 이용해 어플리케이션을 타깃 JVM에 부착하는 장치를 말함
      • 원격 프로세스에 연결하려면 원격지로부터 JMX를 통한 인바운드 접속이 허용돼야 함 (원격 호스트에서 jstatd가 실행되고 있어야 함)
    • VisualVM은 플러그인 아키텍처로 구성되어 다른 툴을 쉽게 추가해 핵심 기능을 보충할 수 있음
      • ex) VisualGC 플러그인

     

    VisualVM이 제공하는 다섯 가지 탭

    1) Overview

    • 자바 프로세스에 관한 요약 정보 표시
    • 프로세스에 전달한 전체 플래그와 시스템 프로퍼티, 실행 중인 자바 버전 제공

     

    2) Montior

    • CPU, 힙 사용량 등 JVM을 고수준에서 원격 측정한 값들이 표시됨
    • 로드/언로드 된 클래스 개수 및 실행 중인 스레드 개수 등 현황도 알 수 있음

     

    3) Thread

    • 실행 중인 어플리케이션 각 스레드(어플리케이션 스레드 + VM 스레드)가 시간대별로 표시됨
    • 필요 시 thread dump를 뜰 수 있음

     

    4) Sample and Profiler

    • CPU 및 메모리 사용률에 관한 단순 샘플링 결과가 표시됨

    'DevBook > Optimizing Java' 카테고리의 다른 글

    14장. 고성능 로깅 및 메시징  (0) 2024.07.31
    CH10. JIT 컴파일의 세계로  (0) 2024.06.24
    CH1 성능과 최적화  (0) 2024.04.09

    댓글

Designed by Tistory.