TIL 230523

전선웅·2023년 5월 23일
0

TIL

목록 보기
1/12

오늘 무엇을 배웠나

  • JVM / JRE / JDK 의 간단한 구성요소
  • 변수

개괄 수준에서 JVM, JRE, JDK를 이해하고 넘어가기에는 아쉬웠다. 중요한 걸 놓치는 것 같았다. (실제로 매우 중요한 것 같다.) JVM이 무엇인지, 어떻게 작동하는지 세부사항을 조사해보았다. 알 수 있는 선에서 최대한 체계적으로 정리를 해본다. 이 부분에 대해서는 정보가 빈약해서 공신력 있는 기술 블로그랑 오라클 공식문서를 참고했다.

1. JVM

Java Virtual Machine(JVM)은 자바 바이트코드(.class 파일에 있는 코드)를 실행할 수 있는 실행 환경입니다. JVM은 플랫폼에 의존적이며, 이는 하드웨어와 운영 체제에 맞게 구현되었다는 것을 의미합니다. 의존적이라는 말의 의미는 운영체제 별로 사용될 수 있는 JVM이 다르다는 것을 말합니다. 즉, Windows용은 Windows에서만, Linux용은 Linux에서만 작동할 수 있습니다. 하지만 JVM이 다를지라도 같은 바이트 코드라면 실행결과는 서로 같습니다. 즉, 한 번 작성한 Java 코드는 어떤 플랫폼의 JVM에서도 동일하게 실행될 수 있다는 말입니다.

1.1. JVM Structure and Operational Mechanics

1.1.1. Class Loader

클래스 로더는 Java 어플리케이션 실행 시 필요한 클래스 파일들을 동적으로 로드하며, 이를 JVM 메모리에 올리는 역할을 담당합니다. 이러한 동적 로드는 프로그램 실행 도중에 필요한 클래스나 라이브러리를 메모리에 로드하는 방식을 가리킵니다. 이 때, 어떤 클래스나 라이브러리가 필요한지는 프로그램이 실행되는 동안 결정됩니다. 클래스 로더는 클래스를 JVM의 메서드 영역으로 로드하고, 링크 작업을 수행하여 실행 가능한 형태로 변환합니다.

클래스 로더는 동적으로 클래스를 로딩하고 링크하는 과정에서 다양한 로딩 전략을 사용할 수 있으며, 클래스의 가시성과 충돌을 관리합니다. 클래스 로더의 구성은 JVM의 중요한 구성 요소이며, 자바 애플리케이션의 클래스 로딩과 관련된 핵심 역할을 수행합니다.

클래스 로더는 이러한 작업을 수행하기 위해, 클래스가 처음으로 참조될 때 해당 클래스를 찾아 메모리에 로드하는 '지연 로딩(Lazy Loading)' 기법을 사용합니다. 필요할 때만 클래스가 로드되며, 이를 통해 메모리 사용량을 최적화하고 프로그램 시작 시간을 단축시키는 효과를 얻을 수 있습니다.

동적 바인딩은 런타임에 메소드, 함수, 변수 등의 참조가 결정되는 방식을 말합니다. 이는 객체 지향 프로그래밍에서 다형성(Polymorphism) 구현을 가능하게 하는 중요한 개념입니다.

클래스 로더가 클래스를 로딩하는 과정에서 동적 바인딩이 사용됩니다. 특히, 상속 관계에 있는 클래스들을 로딩할 때, 하위 클래스의 메소드가 상위 클래스의 메소드를 오버라이드(재정의) 했을 경우, 이 메소드는 런타임에 동적으로 바인딩됩니다. 즉, 어떤 메소드가 호출될지는 프로그램이 실행되는 동안에 결정됩니다.

이러한 동적 바인딩은 프로그램의 유연성과 확장성을 높이는 데 큰 역할을 합니다. 특히, 다형성을 통해 상위 클래스 타입의 참조 변수로 하위 클래스 타입의 객체를 참조할 수 있게 됩니다. 이는 코드의 재사용성을 높이고, 변경에 대한 유연성을 제공합니다. 또한, 동적 바인딩은 인터페이스를 통한 프로그래밍과 같은 디자인 패턴을 가능하게 합니다.

클래스 로더는 동적 바인딩을 지원하며, 이를 통해 런타임에 동적으로 클래스를 로딩하고 링크합니다. 이 과정에서 클래스 로더는 클래스의 바이트코드를 읽어 들이고, 메서드와 변수의 참조를 해결합니다. 이렇게 로딩된 클래스는 JVM 메모리에 저장되며, 필요한 시점에 동적 바인딩을 통해 참조하고 호출할 수 있습니다.

따라서 클래스 로더와 동적 바인딩은 자바의 핵심적인 실행 과정을 이루는 두 기능이며, 이 둘의 결합은 자바가 동적이고 확장 가능한 프로그래밍 언어가 될 수 있게 합니다.

클래스 로딩 프로세스에는 로딩, 링크 및 초기화의 세 단계가 있습니다.

Loading

클래스 로더는 일련의 계층적 구조로 구성되어 있으며, 다음과 같은 구성 요소를 가집니다.

부트스트랩 클래스 로더(Bootstrap Class Loader)는 부트스트랩 클래스 로더는 JVM의 최상위에 위치하는 클래스 로더입니다. 또한 JVM을 초기화하고 자바 표준 라이브러리(JDK)의 핵심 클래스들을 로딩합니다. 부트스트랩 클래스 로더는 C/C++로 구현되어 있으며, 자바 코드로 직접 접근하거나 수정할 수 없습니다.

확장 클래스 로더(Extension Class Loader)는 확장 클래스 로더는 JDK에서 제공되는 확장 클래스들을 로딩하는 역할을 합니다. 확장 클래스 로더는 부트스트랩 클래스 로더의 하위 계층에 위치하며, 주로 JDK의 확장 기능을 위해 사용됩니다. 그러나 자바 9 이후로는 확장 메커니즘이 제거되었으며, 대신 모듈 시스템이 도입되었습니다. 이에 따라 확장 클래스 로더는 플랫폼 클래스 로더(Platform ClassLoader)로 이름이 변경되었고, 이 로더는 부트스트랩 클래스 로더가 로드하는 Java SE Platform API를 제외한 모든 JDK 클래스를 로드합니다.

애플리케이션 클래스 로더(Application Class Loader)는 애플리케이션 클래스 로더는 사용자가 작성한 애플리케이션 코드와 사용자 정의 클래스들을 로딩합니다. 이 클래스 로더는 플랫폼 클래스 로더의 하위 계층에 위치하며, 대부분의 자바 애플리케이션에서 사용됩니다. 애플리케이션 클래스 로더는 보통 클래스패스(Classpath)에 정의된 디렉토리나 JAR 파일에서 클래스를 로딩합니다.

클래스 로더는 일반적으로 계층 구조로 구성되며, 각 클래스 로더는 부모 클래스 로더를 가리키는 참조를 유지합니다. 이를 통해 클래스 로더는 필요한 클래스를 찾기 위해 계층 구조를 따라 위쪽으로 올라갈 수 있습니다. 이러한 구조적 특성에 기반하여 로딩된 클래스들을 메모리에 캐싱하고, 클래스 로더는 한 번 로딩한 클래스를 캐싱하므로, 같은 클래스를 다시 로딩할 필요 없이 빠르게 접근할 수 있습니다.

Linking

클래스 로더의 링킹(Linking) 과정은 JVM에 로드된 바이트코드가 정확하게 실행되도록 하는 중요한 과정입니다. 이 단계는 세 단계로 나눌 수 있습니다

검증(Verification) 단계에서는 .class 파일이 올바른 형식을 가지고 있는지, 문법적으로 정확한지 확인합니다. 즉, JVM이 이 바이트코드를 안전하게 실행할 수 있는지를 검증하는 것입니다. 이 과정은 클래스의 바이트코드가 JVM 규칙을 준수하고, 예를 들어 스택 오버플로우나 언더플로우 같은 오류가 발생하지 않도록 보장합니다.

준비(Preparation) 단계에서는 클래스나 인터페이스에 필요한 메모리를 할당하고, 이를 기본 값으로 초기화합니다. 예를 들어, 필드는 기본값으로 초기화되지만, 이 단계에서는 메소드는 초기화되지 않습니다.

해석(Resolution) 단계에서는 심볼릭 메모리 참조가 직접 참조로 변환됩니다. 심볼릭 참조(Symbolic Reference)는 프로그램에서 클래스, 메서드, 필드 등을 식별하는데 사용되는 이름 또는 식별자입니다. 심볼릭 참조는 해당 요소의 이름이나 식별자로 표현되며, 실제 메모리 주소나 구체적인 위치를 나타내지 않습니다. 심볼릭 참조는 일종의 약속된 이름으로, 프로그램에서 요소들을 식별하고 참조할 수 있도록 돕습니다. 예를 들어, 클래스의 심볼릭 참조는 클래스의 이름을 가리킵니다. 메서드나 필드의 심볼릭 참조는 해당 요소의 이름과 시그니처(매개변수 타입, 반환 타입 등)를 가리킵니다. 해석 단계에서 클래스, 메소드, 필드 등의 심볼릭 참조는 실행 시점에서 메모리상의 실제 참조로 대체됩니다. 이 과정은 선택적인 것으로, 이를 지연할 수 있고 실제로 필요한 시점에 진행되기도 합니다.

위 세 단계를 통해, JVM은 클래스 파일이 안전하게 실행될 수 있음을 보장하며, 필요한 메모리를 할당하고, 심볼릭 참조를 실제 참조로 변환하여 실행을 준비합니다.

Initialization

초기화(Initialization)는 JVM의 클래스 로더 과정 중 마지막 단계로, 로딩과 링킹이 성공적으로 완료된 후 실행됩니다. 이 단계에서 클래스 또는 인터페이스의 정적 변수가 처음으로 사용되거나, 새로운 인스턴스가 생성될 때 해당 클래스의 정적 초기화 블록(Static Initialization Block)이 실행됩니다.

정적 초기화 블록은 클래스가 JVM에 로드될 때 한 번만 실행되며, 클래스의 정적 변수를 초기화하는 데 사용됩니다. 이 블록은 클래스 선언에서 중괄호({})로 묶인 부분으로, 이 안에 초기화 코드를 작성할 수 있습니다.

이 초기화 단계는 매우 중요한데, 이 단계에서 발생하는 모든 예외나 오류는 JVM이 처리할 수 없기 때문입니다. 예를 들어, 초기화 블록에서 발생하는 예외는 클래스 로더에 의해 잡히지 않고, 해당 예외를 직접 처리해야 합니다. 따라서, 초기화 블록에는 중요한 초기화 작업이 포함되기도 하지만, 오류를 방지하기 위해 이 블록에서 발생할 수 있는 예외상황을 최대한 줄이는 것이 좋습니다.

1.1.2. Runtime Data Areas

VM의 런타임 데이터 영역(Runtime Data Areas)은 JVM이 Java 어플리케이션을 실행하는 동안 생성하고 사용하는 데이터를 담기 위한 여러 구조들을 포함하고 있습니다. 일반적으로 이 데이터 영역은 클래스 로더에 의해 로드된 클래스의 정보를 포함하며, 클래스 로더가 전달한 데이터뿐만 아니라 프로그램 실행 도중 생성되는 데이터도 다룰 수 있습니다.

런타임 데이터 영역은 다음과 같이 5개의 영역으로 구분됩니다.

1) 메서드 영역(Method Area): JVM이 시작될 때 생성되며, 각 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 데이터, 그리고 메서드와 생성자의 코드 등을 로드합니다. 이 정보는 클래스 로더에 의해 로드된 후 저장되는 곳입니다.

2) 힙 영역(Heap Area): 모든 클래스 인스턴스와 배열이 할당되는 영역입니다. 이는 런타임에 동적으로 할당되며, 가비지 컬렉션을 통해 더 이상 참조되지 않는 객체를 자동으로 해제합니다.

3) 스택 영역(Stack Area): Java 메서드를 실행하는 동안 각 스레드에 대한 스택 프레임이 생성되는 영역입니다. 스택 프레임에는 메서드 호출, 로컬 변수, 부분 결과 등이 포함됩니다.

4) PC 레지스터(Program Counter Register): 현재 실행중인 JVM 명령의 주소를 저장하는 영역입니다. PC 레지스터의 필요성은 주로 멀티스레딩 환경에서 드러납니다. JVM은 멀티스레딩을 지원하며, 각 스레드는 별도의 작업을 수행합니다. 따라서 각 스레드가 실행 중인 명령을 정확히 추적하려면 각 스레드에 대한 별도의 PC 레지스터가 필요합니다.

5) 네이티브 메서드 스택(Native Method Stacks): JVM이 네이티브 메서드를 호출할 때 사용하는 영역입니다.

이러한 각각의 데이터 영역들은 클래스 로더에 의해 로드된 클래스와 그 인스턴스, 그리고 메서드의 실행에 필요한 데이터를 제공하므로 클래스 로더와 깊은 연관성을 가지고 있습니다. 클래스 로더가 클래스를 로드하면 그 정보는 메서드 영역에 저장되며, 이 정보를 바탕으로 JVM은 힙 영역에 객체를 생성하거나 스택 영역에서 메서드를 실행하는 등의 작업을 수행합니다.

1.1.3. Execution Engine

VM(Java Virtual Machine)의 Execution Engine(실행 엔진)은 자바 바이트코드를 실제로 실행하는 컴포넌트입니다. Execution Engine은 JVM의 핵심 요소 중 하나로, 자바 프로그램을 명령어 단위로 해석하고 실행하는 역할을 담당합니다.

Execution Engine는 인터프리터(Interpreter)와 JIT 컴파일러, 그리고 가비지 컬렉션(Garbage collection)으로 구성됩니다.

인터프리터는 자바 바이트코드를 한 줄씩 읽어들이고 해석하여 실행하는 역할을 합니다. 인터프리터는 바이트코드를 읽고 해당하는 기계어 명령어를 실행하는 과정을 반복합니다. 이러한 방식은 바이트코드를 빠르게 해석하여 실행할 수 있지만, 반복적으로 해석과 실행을 수행해야 하므로 실행 속도가 비교적 느릴 수 있습니다.

이를 JIT 컴파일러가 보완합니다. JIT 컴파일러는 인터프리터의 실행 속도를 향상시키기 위해 동적으로 자바 바이트코드를 기계어로 컴파일하는 역할을 합니다. JIT 컴파일러는 인터프리터가 반복적으로 실행되는 코드를 식별하고, 해당 코드를 기계어로 변환하여 최적화된 형태로 실행합니다. 이는 인터프리터의 해석과정을 거치지 않고 직접 기계어로 실행되므로 실행 속도가 향상됩니다. JIT 컴파일러는 코드의 실행 빈도와 프로그램의 동작을 분석하여 최적화를 수행합니다.

가비지 컬렉션은 메모리 누수나 다른 메모리 관련 오류를 방지하고, 예측 가능한 성능과 메모리 효율성을 제공합니다. 더 이상 사용되지 않는 객체와 그 객체가 점유하고 있는 메모리 주소를 식별합니다. 그 다음, 가비지로 식별된 메모리는 회수되고 재사용 가능한 메모리 풀로 반환됩니다.

1.1.4. Java Native Interface(JNI)와 Native Method Libraries

Java Native Interface(JNI)와 Native Method Libraries는 Java의 플랫폼 독립성을 유지하면서도, 플랫폼에 특화된 기능이나, C/C++ 같은 기존 라이브러리를 활용할 수 있게 하는 기능을 제공합니다.

JNI는 Java 프로그램이 C, C++, 어셈블리 같은 네이티브 언어로 작성된 메소드를 호출할 수 있는 프로그래밍 프레임워크입니다. 이를 통해 Java 프로그램이 운영 체제의 특정 기능을 사용하거나 기존의 라이브러리를 활용할 수 있게 됩니다. JNI는 JVM 구조의 일부인데, 그 이유는 JVM이 이런 네이티브 메소드 호출을 관리하고, 네이티브 메소드와 Java 어플리케이션 사이의 데이터를 변환해주는 역할을 하기 때문입니다.

Native Method Libraries는 JNI를 통해 호출될 수 있는 C, C++ 등으로 작성된 라이브러리입니다. Java 어플리케이션이 플랫폼에 특화된 기능을 사용하거나, 기존 라이브러리를 활용할 때 이들 라이브러리가 사용됩니다.

2. JRE

Java Runtime Environment (JRE)는 Java 애플리케이션을 실행하기 위한 소프트웨어 환경입니다. JRE는 Java 프로그램이 실행될 때 이를 지원하고 실행하는 데 필요한 라이브러리 파일들, Java Virtual Machine(JVM), 그리고 Java Class Libraries를 포함하고 있습니다.

2.1. Composition

2.1.1. JVM

위 내용 참고

2.1.2. Java Class Libraries

Java Class Libraries는 Java 언어의 표준 API를 구현한 라이브러리 집합입니다. 이 라이브러리들은 파일 I/O, 네트워크 프로그래밍, 데이터베이스 연결, 그래픽 인터페이스 등 다양한 기능을 제공하며, 이를 통해 개발자들은 플랫폼에 독립적인 Java 애플리케이션을 개발할 수 있습니다.

2.2. The difference between JVM and JRE

JVM 자체에는 자바 프로그램 실행에 필요한 표준 클래스 라이브러리나 API 등이 포함되어 있지 않습니다. JVM이 운영체제와 직접 소통하여 자바 코드를 실행시키는 도구라면, JRE는 이 도구를 사용하여 실제로 자바 애플리케이션을 실행하기 위한 환경을 제공합니다.

3. JDK

Java Development kit(JDK)는 자바 개발에 필요한 도구들을 한데 모은 키트입니다. JRE와 Java Development Tools를 합친 개념이며 따라서 오늘 다루는 개념 모두를 포괄하는 개념입니다.

3.1. Composition

3.1.1. JRE

위 내용 참고

3.1.2. Java Delopment Tools

JDK(Java Development Kit)에는 Java 개발에 필요한 여러 도구와 유틸리티가 포함되어 있습니다. javac, java, jar 등이 그 예입니다.

javac는 자바 컴파일러로, 자바 소스 코드를 컴파일하여 바이트코드로 변환합니다. java는 Java 가상 머신을 실행하고, 컴파일된 바이트코드를 실행하여 자바 애플리케이션을 실행합니다. jar는 Java 아카이브 도구로, 여러 클래스 파일과 리소스를 하나의 JAR 파일로 패키징하거나 JAR 파일을 추출하는 등의 작업을 수행합니다.

JDK에는 이 외에도 다양한 도구와 유틸리티가 포함되어 있으며, Java 개발과 관련된 다양한 작업을 지원합니다. 이러한 도구들은 개발자에게 효율적인 개발 환경을 제공하고 자바 애플리케이션 개발을 용이하게 합니다.

3.2. The difference between JDK and JRE

JDK는 Java 프로그래밍에 필요한 전체 환경을 제공하며, JRE는 Java 프로그램을 실행하는 데 필요한 환경을 제공합니다. 그래서 Java 개발자는 JDK를, Java 애플리케이션을 단순히 실행만 하려는 사용자는 JRE를 설치합니다. JRE만을 사용하던 사용자가 JDK를 설치함으로써, 단순 실행만 할 수 있던 환경에서 Java를 컴파일하고 디버그할 수 환경을 구축하게 됩니다.

<출처 및 참고자료>

https://www.javacodegeeks.com/2018/04/jvm-architecture-jvm-class-loader-and-runtime-data-areas.html

https://www.freecodecamp.org/news/jvm-tutorial-java-virtual-machine-architecture-explained-for-beginners/

GPT-4

profile
꿈 많고, 꿈 크고, 꿈 좋은

0개의 댓글