본문 바로가기

더 나은 엔지니어가 되기 위해/지금은 안쓰는 자바

[더 자바] JVM 이해하기

인프런에서 백기선님의 더 자바, 코드를 조작하는 다양한 방법을 공부하며 개인적으로 정리한 글입니다.

JVM, JRE, JDK, Java

4개를 좀 구분해서 알아놓을 필요가 있다.

  • JVM (Java Virtual Machine)
    • .class 파일을 OS 에 맞는 machine code 로 변환 (인터프리터 & JIT 컴파일러)
    • 플랫폼(OS) 에 종속적
  • JRE (Java Runtime Environment)
    • JVM + 핵심 라이브러리
    • 개발 관련 x. 실행 관련 o
    • java 는 보통 JRE 단위로 배포됨
  • JDK (Java Development Kit)
    • JRE + 개발 툴(java c)
    • 자바 11부터는 JDK 단위로 배포됨.
  • Java
    • 소스 코드(java 언어) 자체는 플랫폼에 독립적
    • javac 에 의해 .class 로 만들어 짐.
    • JVM 자체 연관이 타이트하지 않음. (JVM 은 .class 만 다루므로...)
      • ex. Kotlin 과 kotlinc 로 .class 파일을 만들 수 있음.

.java 가 프로세스가 되기까지의 과정

  • .java -> (javac) -> .class -> (jvm) -> process

Byte 코드와 Binary 코드의 차이

  • Byte 코드는 JVM 같은 가상 머신이 이해할 수 있는 코드( = .class )
  • Binary 코드는 CPU 가 이해할 수 있는 코드

JVM 구조

1) 클래스 로더 시스템

바이트 코드를 읽어오며 메모리에 적절히 배치하는 역할.
크게 다음과 같이 나눠서 살펴볼 수 있다.

  • 로딩
    • .class 를 읽어옴.
  • 링크
    • 코드 내부의 레퍼런스를 연결함.
  • 초기화
    • 클래스에 있는 static 값들을 초기화 함.

2) 메모리

크게 다음과 같이 나눠서 살펴볼 수 있다.
(힙, 메소드)는 전체 공유자원으로 분류되고, (스택, PC, 네이티브 메소드 스택)은 쓰레드 단위의 자원으로 분류된다.

  • 메소드
    • 클래스 수준의 정보를 저장
      • 클래스 이름, 부모 클래스 이름, 메소드, 변수 등
      • static 변수, 일반 변수 등
    • 객체(인스턴스) 수준의 정보를 저장
  • 스택
    • 인스턴스 및 지역 변수의 참조 주소 들을 저장.
    • 쓰레드마다 런타임 스택을 만들고, 스택 프레임(메소드 call)을 쌓는다.
      • 에러 났을 때, 에러 메시지보면, 런타임 스택에 메시지 쌓여있는걸 확인할 수 있음.
  • PC
    • 쓰레드마다 가지고 있는 Program Counter.
    • 현재 실행할 부분을 가르키고 있다.
  • 네이티브 메소드 스택
    • 네이티브(native) 메소드 호출할 때 사용하는 별도의 스택
    • 네이티브 메소드는 java 가 아닌 c와 같은 언어(low-level) 로 구현된 메소드임.
      • 대표적인 예시로, Thread.currentThread() 임.
        public static native Thread currentThread() 로 선언되어 있음.

3) 실행 엔진

  • 인터프리터
    • 바이트 코드를 한줄 한줄 읽어서 네이티브 코드로 변환
  • JIT (Just In Time) 컴파일러
    • 바이트 코드에서 반복되는 코드 부분은 JIT 컴파일러가 미리 네이티브 코드로 변환 시켜놓음.
    • 반복되는 코드가 읽힐 순서가 왔을 때, 인터프리터로 읽지않고 바로 네이티브 코드를 바로 사용한다.
    • 인터프리터 읽을 때의 속도 효율성을 JIT 컴파일러가 보완하는 형태.
  • GC (Garbage Collector)
    • 더 이상 참조되지 않는 객체를 모아서 메모리 정리를 한다.
    • 경우에 따라 성능 효율을 위해 커스터마이징을 해야함.

클래스 로더

JVM 내부 구조의 클래스 로더 시스템 부분을 좀 더 자세히 살펴본다.

로딩 -> 링크 -> 초기화 순으로 진행된다.

  • 로딩
    • .class 파일을 읽어서 바이트 코드 -> 바이너리 코드로 만들고 이를 "메소드" 영역에 저장한다.
    • 저장하는 데이터는 다음과 같다.
      • Fully-Quailified Class Name
        • 클래스 로더, 클래스 패키지 경로, 패키지 이름, 클래스 이름을 모두 포함한 값
        • ex. java.lang.Character$Subset
      • 클래스 | 인터페이스 | 이늄 을 구분하여 저장
      • 메소드와 변수
    • 로딩이 끝나면 해당 클래스 타입의 객체를 생성하여 "힙" 영역에 저장
    • BootStrap -> Extension -> Application Loader 순으로, 앞의 Loader 가 로딩할 수 없으면 그 다음 Loader 가 읽어내는 식.
      사실상 Application Loader 가 읽어낸다고 함.
  • 링크
    • Verify
      • .class 파일 형식이 유효한지 검사한다.
    • Perpare
      • static 변수와 기본 값에 필요한 메모리를 준비한다.
    • Resolve (Optional)
      • 심볼릭 메모리 레퍼런스를 실제 메모리 레퍼런스로 교체한다.
      • Optional 인 이유는, 이 때 교체(binding) 될 수도 있고, 이후 사용이 일어날 때에 동적으로 교체될 수도 있다.
  • 초기화
    • static 변수를 초기화 한다.