Search
Duplicate
📒

[Java Study] 01-x. JDK 구조(JRE, JVM), 클래스 파일과 바이트 코드

상태
수정중
수업
Java Study
주제
4 more properties
참고

JDK(Java Development Kit)

NOTE
JDK는 개발자들이 자바로 사용되는 SDK키트이다. JDK안에는 자바 개발 시 필요한 라이브러리들과 javac, javadoc등의 개발 도구들을 포함하고 있습니다.
JDK 구성요소
JDK를 설치받아서 디렉토리를 살펴보면 다음과 같은 구조로 되어 있습니다.
Jdk 21 폴더내용
bin: 자바 개발, 실행에 필요한 도구와 유틸리티 명령
include: 네이티브 코드 프로그래밍에 필요한 C언어 헤더 파일
lib: 실행 시간에 필요한 라이브러리 클래스
bin 패키지 내용
javac(자바 컴파일러): 자바 소스 코드를 바이트 코드(.class)로 변환하는 컴파일러 입니다.
java(자바 인터프리터): javac가 생성한 바이트 코드를 실행합니다.
jar: 자바 모듈 파일(.jmod)를 만들거나, 기존 모듈 파일의 내용을 보여줍니다.
jlink: 필요한 모듈만 포함된 맞춤형 JRE를 생성합니다.
javap(역 어셈블러): 컴파일된 클래스 파일을 읽고 원래의 코드 형태를 보여줍니다.

JRE(Java Runtime Environment)

NOTE
JRE는 Java 애플리케이션을 실행하기 위한 환경을 제공하는 소프트웨어 이며, 이미 개발된 Java 애플리케이션을 실행하는데 중점을 둔 구성요소 입니다.
기본적으로 JRE는 JDK에 포함되어 있기 때문에 JDK를 설치하면 함께 설치됩니다.
JRE 구조
클래스 로더(Class Loader): Java 애플리케이션이 실행될 때 필요한 클래스 파일을 동적으로 로드하는 역할을 합니다.
바이트 코드 검증기(Bytecode Verifier): JVM에서 바이트 코드를 실행하기 전에 해당 코드가 안전하고 유효한지 확인하는 역할을 합니다.

JVM(Java Virtual Machine)

NOTE
JVM은 Java 애플리케이션을 실행하기 위한 가상 머신으로, Java 프로그램이 다양한 운영체제와 하드웨어에서 동일하게 동작할 수 있도록 해줍니다.
JVM의 구조

JVM 컴파일

NOTE
JVM은 Java 애플리케이션을 실행하기 위한 가상 머신으로, Java 프로그램이 다양한 운영체제와 하드웨어에서 동일하게 동작할 수 있도록 해줍니다.
WORA(Write Once, Read AnyWhre) 특성
1.
Java Compiler가 Java로 작성된 소스 코드(.java) 파일을 Byte 코드(.class)파일로 컴파일 합니다.
2.
Byte 코드를 기계어로 변환시키기 위해 가상 CPU가 필요한데, 이것이 JVM의 역할이며 JVM이 Byte Code → 기계어(Binary Code)로 변환한다.

JVM의 동작 방식

NOTE
JVM의 동작 방식은 Java 애플리케이션이 실행될 때 바이트 코드를 읽고, 기계어로 변환하여 실행하는 과정입니다. JVM은 이 과정을 여러 단계를 거쳐 동작합니다.
JVM 구성도
JVM의 동작 방식을 깊게 들어가기전에 가장 간단하게 .java 코드가 어떻게 JVM에서 실행되는지 간단하게 실습해봅시다.
Java 구동원리(축약버전)
1.
소스코드(MyProgram.java)를 작성한다.
2.
javac(컴파일러)를 통해 .java파일로 .class 파일(바이트 코드)을 생성한다.
3.
java(인터프리터 실행)을 통해 바이트 코드를 해석해서 바이너리 코드로 변환한뒤 프로그램을 수행한다.
public class MyProgram { public static void main(String[] args) { System.out.println("Hello World"); } }
Java
복사
MyProgram.java
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // public class MyProgram { public MyProgram() { } public static void main(String[] var0) { System.out.println("Hello World"); } }
Bash
복사
MyProgram.class
javac MyProgram.java java Myprogram
Bash
복사

Class Loader

NOTE
클래스 로더는 컴파일된 자바의 클래스 파일을 동적으로 로드하고, JVM의 메모리 영역인 Runtime Data Areas에 배치하는 작업을 수행합니다.
클래스 로더 흐름
Loading(로드): 클래스 파일을 가져와서 JVM의 메모리에 로드합니다.
Linking(링킹): 클래스 파일을 사용하기 위해 검증하는 과정입니다.
Verifying(검증): 읽어들인 클래스가 JVM 명세에 명시된 대로 구성되어 있는지 검사한다.
Preparing(준비): 클래스가 필요로하는 메모리를 할당한다.
Resolving(분석): 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
Initialization(초기화): 클래스 변수들을 적절한 값으로 초기화하며 JVM이 새로 로드뢴 클래스의 바이트코드를 실행하게 됩니다.
javac -d out wgjd.sitecheck/module-info.java \ wgjd.sitecheck/wgjd/sitecheck/*.java \ wgjd.sitecheck/wgjd/sitecheck/*/*.java java -cp out wgjd.sitecheck.SiteCheck http://github.com/well-grounded-java
Bash
복사
public static void main(String[] args) { var classLoaderThis = ClassLoading.class.getClassLoader(); var classLoaderObj = Object.class.getClassLoader(); var classLoaderHttp = HttpClient.class.getClassLoader(); System.out.println("classLoaderThis = " + classLoaderThis); System.out.println("classLoaderObj = " + classLoaderObj); System.out.println("classLoaderHttp = " + classLoaderHttp); }
Java
복사
classLoaderThis = jdk.internal.loader.ClassLoaders$AppClassLoader@5679c6c6 classLoaderObj = null classLoaderHttp =jdk.internal.loader.ClassLoaders$PlatformClassLoader@3cda1055
Object에 대한 클래스로더 null이 출력되는 이유는 보안때문인데, 부트스트랩 클래스로더는 실제로는 보안 검증을 수행하지 않으며 로드하는 클래스에 대한 특별한 보안 제한을 적용하지 않는다.
클래스 로더는 JAR 파일이나 클래스패스의 다른 위치에서 리소스를 로드하는데 사용되는 경우가 많다. 이는 try-with-resources와 결합해서 다음과 같은 코드를 생성하는 패턴에서 볼 수 있다.

Class 객체

// MyClass 클래스를 현재 실행 상태로 로드한다. Class<?> clazz = Class.forName("MyProgram");
Java
복사
로딩과 링킹 프로세스의 최종 결과는 새로 로드되고 링크된 타입을 타나내는 Class 객체입니다. 성능상의 이유로 Class 객체의 일부 측멱은 필요할 때만 초기화가 됩니다.
Class 객체는 메서드, 필드, 생성자 등에 대한 간접적인 접근을 위해 리플렉션 API를 사용할 수 있습니다.

ClassLoader 종류

ClassLoaderClassLoader를 확장하는 자바의 클래스일 뿐이며, 그 자체로 자바의 타입이다.
ClassLoader 클래스에는 클래스 파일의 저수준 구문 분석을 담당하는 로드와 링크 부분을 포함해서 몇 가지 네이티브 메서드가 있지만, 사용자 클래스로더는 이런 부분을 재정의할 수 없습니다.
BootStrapClassLoader
JVM을 시작하는 초기에 인스턴스화되므로 일반적으로 JVM 자체의 일부라고 생각하는 것이 가장 좋습니다.
java.base를 로드하는데 사용됩니다.
PlatformClassLoader
최소한의 시스템이 부트스트랩된 후 애플리케이션이 의존하는 나머지 플랫폼 모듈을 로드합니다.
이 클래스 로더는 로더에 의해 실제 로드됐는지 부트스트랩에 됐는지에 상관없이 모든 플랫폼 클래스에 접근하기 위한 기본 인터페이스다.
AppClassLoader
애플리케이션 클래스를 로드하고 대부분의 최신 자바 환경에서 작업을 수행합니다.

사용자 정의 Class Loader

NOTE
사용자 정의 클래스 로딩은 ClassLoader를 서브 클래싱하고 findClass()를 오버라이드 하는 방식으로 진행할 수 있습니다.
수많은 사용자 정의 클래스로더는 findClass()를 재정의하는 것이 전부입니다. 예를들어 네트워크를 통해 클래스를 찾는것이 여기에 퐇마될 수 있습니다.
ClassLoader에 정의된 defineClasss() 메서드의 경우 ‘로드와 링크’ 프로세스를 수행하는 접근 가능한 메서드이기 때문에 클래스 로딩의 핵심입니다.
바이트 배열을 받아 클래스 객체로 변환한다.
클래스패스(classpath)에 없는 새로운 클래스를 런타임에 로드하는 데 사용되는 기본 메커니즘이다.
JVM으로 들어가는 진입점이며, C코드에 대해 액세스를 제공한다. 핫스폿은 C++메서드를 통해 새 클래스를 로드하기 위해 C System Dictionary를 사용한다.
public class CustomerClassLoader { public static class SadClassLoader extends ClassLoader { public SadClassLoader() { super(SadClassLoader.class.getClassLoader()); } // findClass 메서드를 오버라이드하여 클래스 로딩 시 커스텀 메시지를 출력 @Override protected Class<?> findClass(String name) throws ClassNotFoundException { System.out.println("커스텀 클래스로더에서 클래스를 찾는중입니다: " + name); throw new ClassNotFoundException(name); // 이 예제에서는 단순히 예외를 던짐 } // loadClass 메서드를 오버라이드하여 클래스 로딩을 직접 처리 @Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { try { // 부모 로더에게 먼저 로딩을 시도 return super.loadClass(name, resolve); } catch (ClassNotFoundException e) { // 부모 로더가 못 찾으면 커스텀 로직으로 시도 System.out.println(name + " :: 직접 로드 시도"); Class<?> c = findClass(name); // 직접 로드 시도 if (resolve) { resolveClass(c); } return c; } } } public static void main(String[] args) { // 테스트할 클래스 이름 지정 - 로드할 클래스를 문자열로 지정 String className = "MyProgram2"; // 존재하는 클래스 이름 var loader = new SadClassLoader(); try { // 커스텀 클래스 로더를 사용하여 클래스 로드 시도 Class<?> clazz = loader.loadClass(className); System.out.println("클래스 찾음: " + clazz); } catch (ClassNotFoundException e) { e.printStackTrace(); // 클래스 로딩 실패 시 예외 출력 } } }
Java
복사
findClass(커스텀 로딩 구현): 특정 클래스 이름을 바탕으로 해당 클래스를 로드하는 역할을 합니다.
클래스 로더의 loadClass가 클래스 로딩을 실패했을때 호출됩니다.
loadClass(로딩 흐름의 제어): 클래스 로딩의 전체 과정을 제어하는 메서드이며, 클래스 로딩시 가장 먼저 호출됩니다.
이미 로드된 클래스가 있는지 확인한다.
부모 로더에게 클래스 로딩을 위임하여, 부모가 클래스를 로드할 기회를 먼저 줍니다.
부모 로더가 클래스를 찾지 못하면, 커스텀 로더의 findClass를 통해 시도합니다.

DI 프레임워크

DI의 주요개념
시스템 내의 기능 단위에는 적절한 기능을 수행하기 위해 의존하는 의존성과 구성 정보가 있다.
많은 객체 시스템에서는 코드로 표현하기 어렵거나 어색한 의존성이 있다.
객체의 런타임 와이어링
가상의 DI 프레임워크에서 애플리케이션 실행하기
// DIMain 클래스는 DI vmfpdladnjzmdml wlsdlqwja java -cp <CLASSPATH> org.wgjd.DIMain /path/to/config.xml
Java
복사
<beans> <bean id="dao" class="app.ch04.PaymentsDAO"> <constructor-arg index="0" value="jdbc:postgresql://db.wgjd.org/payments"/> <constructor-arg index="1" value="org.postgresql.Driver"/> </bean> <bean id="service" class="app.ch04.PaymentsService"> <constructor-arg index="0" value="dao"/> </bean> </beans>
Java
복사
DI 프레임워크는 구성 파일을 사용하여 어떤 객체를 생성할지 결정하고, 해당 코드는 service, dao 빈을 만들어야 한다.
클래스 로딩은 2단계로 나누어서 진행된다.
1.
DIMain 클래스와 이 클래스가 참조하는 모든 프레임워크 클래스를 로드한다.
2.
DIMain이 실행을 시작하고 구성 파일의 위치를 main()의 매개변수로 받는다.
이 시점에서 프레임워크는 JVM에서 실행중이지만, config.xml에 지정된 사용자 클래스는 아직 건드리지 않았다. 실제로 DIMain이 구성 파일을 검사하기 전까지 프레임워크가 어떤 클래스를 로드할지는 모른다.
config.xml에 지정된 구성을 불러오기 위해서는 2번째 단계의 클래스 로딩이 필요하다. 이 예제에서는 클래스 로더를 사용한다.
config.xml 파일의 일관성을 검사하고 오류가 없는지 확인한다.
모든 것이 정상이면 사용자 정의클래스로더가 CLASSPATH에서 타입을 로드하려고 시도한다. 이중 하나라도 실패하면 전체 프로세스가 중단되어 런타임 오류가 발생한다.
작업이 성공하면 DI프레임워크는 필요한 객체를 올바른 순서로 인스턴스화 할 수 있다.
모든 과정이 완료되면 애플리케이션 콘텍스트가 설정되고 실행 가능해진다.
모듈 시스템은 플랫폼 내에서 상대적으로 낮은 수준의 메커니즘읜 클래스 로딩과는 다른 수준에서 작동하도록 설정됐다.
모듈은 프로그램 단위 간의 대규모 의존성에 관한 것이고 클래스 로딩은 소규모에 관한 것이다. 그러나 두 메커니즘이 어떻게 교차하는지 이해하는것이 중요하다.

클래스 파일 살펴보기

NOTE
클래스 파일은 바이너리 덩어리이므로 직접 작업하기가 쉽지 않다.
런타임 모니터링 개선을 위해 애플리케이션에 추가적인 메서드를 공개해야 한다고 가정
javap는 java에서 제공하는 디어셈블러 도구로, 컴파일된 .class 파일을 분석하여 해당 클래스의 메서드, 필드, 그리고 바이트코드 정보를 읽기 쉽게 출력해줍니다.
public class MyProgram { public static void main(String[] args) { System.out.println("Hello World"); } private void test() { System.out.println("test"); } }
Java
복사
javap MyProgram.java # Compiled from "MyProgram.java" # public class MyProgram { # public MyProgram(); # public static void main(java.lang.String[]); # } # -p: 클래스의 모든 필드와 메서드 표시 javap -p MyProgram.java # --public: public 멤버만 표시 javap --public MyProgram.java # --public: public 멤버만 표시 javap --public MyProgram.java
Bash
복사

메서드 시그니처

javap -s 옵션을 사용하면 클래스의 필드와 메서드에 대한 시그니처가 출력되며, 이 시그니처에는 Java타입의 기술자가 포함됩니다. 이 타입 기술자는 JVM에서 사용되는 형식을 나타냅니다.
# -s: 각 필드와 메서드 시그니처를 표시한다. javap -s MyProgram.java # Compiled from "MyProgram.java" # public class MyProgram { # public MyProgram(); # descriptor: ()V # # public static void main(java.lang.String[]); # descriptor: ([Ljava/lang/String;)V # }
Bash
복사
Java 타입
기술자
설명
boolean
Z
단일 비트 부울(boolean) 값
byte
B
8비트 부호 있는 정수
char
C
16비트 유니코드 문자
short
S
16비트 부호 있는 정수
int
I
32비트 부호 있는 정수
long
J
64비트 부호 있는 정수
float
F
32비트 부동 소수점
double
D
64비트 부동 소수점
void
V
반환값이 없는 경우
참조형 (클래스)
L<classname>;
클래스의 전체 이름을 포함한 참조형
배열
[<type>
배열, 예를 들어 int[][I
제네릭 타입 변수
T<name>;
제네릭에서 타입 변수, 예: T
제네릭의 와일드카드
*
제네릭의 와일드카드 문자
제네릭 한정자
+<type>
상위 한정 (extends)
제네릭 하위 한정자
-<type>
하위 한정 (super)

상수 풀

상수 풀은 클래스 파일의 다른 요소에서 편리한 바로 가기를 제공하는 영역이다.
상수 풀은 Java 클래스 파일에서 다양한 상수(문자열, 숫자, 클래스 및 메서드 참조 등)을 저장하는 곳이며, 이는 JVM 클래스 파일을 읽고 실행할 때 필요한 정보를 효율적으로 관리하기 위해 사용합니다.
# -v: 클래스에 대한 상세한 정보 출력 javap -v MyProgram.class
Bash
복사
인덱스
타입
값/참조
설명
#1
Methodref
#2.#3
java/lang/Object 클래스의 <init> 메서드 참조
#2
Class
#4
java/lang/Object 클래스 참조
#3
NameAndType
#5:#6
생성자 이름과 시그니처 <init>:()V
#4
Utf8
java/lang/Object
클래스 이름을 UTF-8로 인코딩
#5
Utf8
<init>
생성자 이름
#6
Utf8
()V
생성자의 시그니처, 반환 타입이 void이고 파라미터 없음
#7
Fieldref
#8.#9
java/lang/System 클래스의 out 필드 참조
#8
Class
#10
java/lang/System 클래스 참조
#9
NameAndType
#11:#12
필드 이름과 타입 out:Ljava/io/PrintStream;
#10
Utf8
java/lang/System
클래스 이름을 UTF-8로 인코딩
#11
Utf8
out
필드 이름
#12
Utf8
Ljava/io/PrintStream;
필드 타입 PrintStream
#13
String
#14
문자열 리터럴 "Hello World"
#14
Utf8
Hello World
문자열 내용 UTF-8 인코딩
#15
Methodref
#16.#17
java/io/PrintStream 클래스의 println 메서드 참조
#16
Class
#18
java/io/PrintStream 클래스 참조
#17
NameAndType
#19:#20
메서드 이름과 시그니처 println:(Ljava/lang/String;)V
#18
Utf8
java/io/PrintStream
클래스 이름을 UTF-8로 인코딩
#19
Utf8
println
메서드 이름
#20
Utf8
(Ljava/lang/String;)V
메서드 시그니처, 파라미터는 String, 반환 타입은 void
#21
String
#22
문자열 리터럴 "test"
#22
Utf8
test
문자열 내용 UTF-8 인코딩
#23
Class
#24
MyProgram 클래스 참조
#24
Utf8
MyProgram
클래스 이름을 UTF-8로 인코딩
#25
Utf8
Code
메서드의 코드 정보를 나타내는 이름
#26
Utf8
LineNumberTable
소스 코드의 라인 번호 테이블 정보
#27
Utf8
main
메서드 이름 main
#28
Utf8
([Ljava/lang/String;)V
main 메서드의 시그니처, 파라미터는 String 배열, 반환 타입은 void
#29
Utf8
SourceFile
소스 파일 정보
#30
Utf8
MyProgram.java
소스 파일 이름
Class: 클래스 상수다. 클래스 이름을 가리킨다.
NameAndType: 이름과 유형 쌍을 설명한다.
Filedref: 필드를 정의한다. 이 필드의 클래스와 NameAndType을 가리킨다.
Methodref: 메서드를 정의한다. 이 필드의 클래스와 NameAndType을 가리킨다.

클래스 파일 로딩 및 상수 풀 사용 순서

클래스 로드 → 필요한 정보 확인 → 메서드/필드 접근 → 문자열 사용 → 프로그램 실행
1.
클래스 로딩: JVM이 클래스 파일을 메모리에 로드하면서 상수 풀을 읽습니다.
MyProgram 클래스 로드
상수 풀도 함께 로드되며 System.out, “Hello World”, println() 메서드 등이 저장되어 있다.
2.
상수 풀 확인: 클래스 이름, 메서드, 필드, 문자열 등 필요한 모든 참조 정보를 상수 풀에서 확인합니다.
ex) System.out → Fieldref, “Hello World” → String
3.
필드 및 메서드 참조: 프로그램이 필드를 읽거나 메서드를 호출할 때 상수 풀의 정보를 사용하여 정확한 위치를 찾습니다.
main 메서드가 실행된다
4.
메서드 실행: 상수 풀의 정보를 이용해 메서드를 호출하고 실행합니다.
5.
문자열 사용: 문자열을 출력하거나 처리할 때 상수 풀의 문자열 리터럴을 참조합니다.
6.
프로그램 실행: 상수 풀에서 필요한 모든 정보를 참조하여 프로그램의 바이트 코드를 실행합니다.

바이트 코드

NOTE
바이트 코드 정리
바이트 코드는 사람이 읽을 수 있는 소스코드와 기계코드 중간에 있는 중간 표현이다.
바이트코드는 javac 명령어에 의해 생성된다.
일부 고급 언어 기능들은 컴파일되어 바이트코드에 나타나지 않는다 (for, while)은 분기 명령어로 바뀌면서 사라진다.
바이트코드는 JIT 방식으로 기계어 코드로 추가적으로 컴파일할 수 있다.
# -c(바이트 코드), -p(모든 필드와 메서드) javap -c -p MyProgram # Compiled from "MyProgram.java" # public class MyProgram { # public MyProgram(); ## 정적블록 라인 # Code: # 0: aload_0 # 1: invokespecial #1 // Method java/lang/Object."<init>":()V # 4: return # public static void main(java.lang.String[]); # Code: # 0: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream; # 3: ldc #13 // String Hello World # 5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V # 8: return
Bash
복사

load 및 store 오퍼레이션 코드

public class MyProgram { // ... public int getI() { return i; } public void setI(int i) { this.i = i; } }
Java
복사
# public int getI(); # Code: # 0: aload_0 # 1: getfield #23 // Field i:I # 4: ireturn # public void setI(int); # Code: # 0: aload_0 # 1: iload_1 # 2: putfield #23 // Field i:I # 5: return
Bash
복사

실행흐름 제어 코드

고수준 언어의 제어 구조는 JVM 바이트코드에 존재하지 않는다. 대신 실행 흐름은 다음과 같이 처리된다.
if: 특정 조건이 일치하면 지정된 분기 오프셋으로 점프한다.
goto: 조건 없이 분기 오프셋으로 점프한다.
tables: switch 구문 처리에 사용된다.
lookupswitch: switch 구문 처리에 사용된다.

Invoke 코드

invoke는 JVM 바이트코드에서 메서드를 호출하는 명령어 계열의 접두사로 사용됩니다.
Invoke 오퍼레이션 코드는 일반적인 메서드 호출을 처리하기 위해 4개의 오퍼레이션 코드와 자바 7에 추가된 invokedynamic 코드로 구성된다.
invokestatic: 정적 메서드 호출
invokevirtual: 일반 인스턴스 메서드 호출
invokeinterface: 인터페이스 메서드 호출
invokespecial: 생성사와 같은 특수 메서드 호출
invokedynamicL 동적 호출

플랫폼 관련 작업 수행 코드

new: 지정된 인덱스의 상수에 지정된 타입의 새로운 객체를 위한 메모리를 할당한다.
monitorenter: 객체를 잠근다.
monitorexit: 객체의 잠금을 해제한다.
바이트코드 수준에서의 생성자는 특별한 이름인 <init>을 가진 메서드로 변환된다. 이는 사용자 자바 코드에서 호출할 수는 없지만 바이트코드에서 호출할 수 있다.

리플렉션

NOTE
리플렉션 역시 java.lang.Module에 정의되며, Class 객체에서 직접 접근이 가능하다.
public class NativeMethodChecker { // 클래스 로더 // defineClass 클래스를 통해 클래스를 메모리내에서 정의 public static class EasyLoader extends ClassLoader { public EasyLoader() { super(EasyLoader.class.getClassLoader()); } public Class<?> loadFromDisk(String fName) throws IOException { var b = Files.readAllBytes(Path.of(fName)); return defineClass(null, b, 0, b.length); } } // 클래스 로더로 .class 파일을 읽어서 해당 클래스 메소드 출력 public static void main(String[] args) { if (args.length > 0) { var loader = new EasyLoader(); for (var file : args) { System.out.println(file +" ::"); try { var clazz = loader.loadFromDisk(file); for (var m : clazz.getMethods()) { if (Modifier.isNative(m.getModifiers())) { System.out.println(m.getName()); } } } catch (IOException | ClassFormatError x) { System.out.println("Not a class file"); } } } } }
Java
복사

리플렉션의 문제점

자바 컬렉션 이전에 만들어진 매우 오래된 API로, 배열 타입이 여기저기에 존재한다.
어떤 메서드 오버로드를 호출할지 결정하는 것은 쉽지 않다.
API 접근 제어를 무시하는데 사용할 수 있는 setAccessible() 메서드를 제공한다.
리플렉션 호출에 대한 예외처리가 어렵다. (체크 예외가 런타임 예외로 변경됨)

실행 엔진(Execution Engine)

NOTE
실행 엔진은 클래스 로더를 통해 런타임 데이터 영역에 배치된 바이트 코드를 명령어 단위로 읽어서 실행합니다. 자바 바이트 코드(.class)는 기계가 바로 수행할 수 없기 때문에, JVM내부에서 실행 가능한 형태로 변경해줍니다.
실행 엔진은 인터프리터와 JIT 컴파일러 2가지 방식으로 혼합하여 바이트 코드를 실행합니다.

인터프리터

바이트 코드 명령어를 하나씩 읽어서 해석하고 바로 실행합니다.
JVM안에서 바이트코드는 기본적으로 인터프리터 방식으로 동작하며, 같은 메소드라도 여러번 호출이 된다면 매번 해석하고 수행되어 전체적인 속도가 느립니다.

JIT(Just-In-Time) 컴파일러

JIT 컴파일러는 인터프리터의 단점을 보완하기 위해 도입되었습니다. 이 방식은 반복되는 코드를 발견하면 바이트 코드 전체를 컴파일하여 네이티브 코드로 변환합니다. 변환된 코드는 더 이상 인터프리팅되지 않고, 캐시에 저장되어 직접 실행됩니다.
자바 프로그램은 자바 가상 머신이라는 추가적인 단계를 거치기 때문에 일반 프로그램보다 실행 속도가 느립니다. 이러한 단점을 보완하기 위해 JIT 컴파일러를 사용합니다.
자주 실행되는 코드(메서드)를 네이티브 코드로 변환하여 실행속도를 높일 수 있습니다.
필요할 때만 컴파일을 수행하여 메모리와 CPU 자원을 효율적으로 사용합니다.

런타임 데이터 영역(RunTime Data Area)

NOTE
런타임 데이터 영역은 JVM의 메모리 영역으로 자바 애플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역입니다,

쓰레드 공유 영역

JVM 시작시 생성되며 프로그램 종료까지 살아있습니다. 또한 명시적으로 null이 선언되면 GC 대상이 됩니다.
Method(Static): JVM에서 읽어들인 클래스, 인터페이스에 대한 런타임 상수 풀, Static 변수등을 보관한다.
Runtime Constant Pool: Method Area영역에 포함되지만 독자적 중요성을 뜁니다. JVM은 런타임 상수 풀을 이용해서 해당 메서드나 필드의 실제 메모리 상 주소를 찾아 참조합니다.

쓰레드 개별 영역

Stack Area: 메서드 호출 시 생성되는 스레드 수행정보를 기록하는 Frame을 저장하며, 메서드 연산중 발생하는 지역변수, 임시 데이터를 저장합니다.
PC 레지스터: JVM에서 각 스레드가 실행 중인 바이트 코드 명령의 현재 위치를 추적하는 공간입니다.

PC 레지스터

PC 레지스터는 쓰레드가 시작될 때 생성되며, 현재 수행중인 JVM 명령어 주소를 저장하는 공간이다.
일반적으로 CPU에서 명령어를 수행하는 과정으로 이루어진다. 이 때 CPU는 연산을 수행하는 동안 필요한 정보를 레지스터라고 하는 CPU내의 기억장치를 사용하게 된다.
하지만 자바의 PC 레지스터는 위의 CPU 레지스터와 다르다.
자바는 하나의 프로세스이므로 JVM의 리소스를 사용해야 한다.
그래서 자바는 CPU에서 직접 연산을 수행하는게 아닌, 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 만든것이다.

네이티브 메서드 스택

네이티브 메서드 스택은 자바 코드가 컴파일되어 생성되는 바이트 코드가 아닌 실제 수행할 수 있는 기계어로 작성된 프로그램을 실행시키는 영역이다.
또한 자바 이외의 언어로 작성된 네이티브 코드를 실행하기 위한 공간이기도 하다.
JIT 컴파일러에 의해 변환된 Native Code 역시 여기에서 실행이 된다.
일반적으로 메소드를 실행하는 경우 JVM 스택에 쌓이다가 메소드 내부에서 네이티브 메소드를 사용한다면 네이티브 스택에 쌓인다.
네이티브 메소드는 JNI와 연결되어 있는데, JNI가 사용되면 네이티브 메서드 스택에 바이트 코드로 전환되어 저장된다.

1. String Constant Pool (문자열 상수 풀)

위치: 힙(Heap) 영역 내의 특별한 공간
설명: String Constant Pool은 JVM의 힙 내에 위치한 특별한 메모리 영역으로, 문자열 리터럴을 저장하고 재사용하는 역할을 합니다. 힙 영역은 객체들이 저장되는 곳이며, String Constant Pool은 힙의 일부분으로 문자열 리터럴의 중복 생성을 방지하고 메모리 사용을 최적화합니다.

2. Runtime Constant Pool (런타임 상수 풀)

위치: 메서드 영역(Method Area) 내
설명: Runtime Constant Pool은 JVM의 메서드 영역에 존재하며, 클래스와 인터페이스의 런타임 상수 정보를 관리합니다. 클래스가 로드될 때, 해당 클래스의 상수 풀(Constant Pool) 정보가 메서드 영역에 로드되어 런타임 상수 풀로 사용됩니다. 이 풀은 메서드 호출, 필드 접근, 타입 정보 등 다양한 상수 참조를 효율적으로 관리합니다.

3. Integer Cache Pool (정수 캐시 풀)

위치: 힙(Heap) 영역 내의 특정 캐시 영역
설명: Integer Cache Pool은 -128에서 127 범위의 Integer 객체를 캐싱하여 힙 내에서 재사용합니다. 이 캐시는 힙의 일부로서, 작은 범위의 정수 값이 자주 사용되므로 메모리와 성능 최적화를 위해 만들어졌습니다.

4. Boolean Cache Pool (불리언 캐시 풀)

위치: 힙(Heap) 영역 내
설명:Boolean 객체는 truefalse 두 개의 값만 존재하므로, 이 값들은 힙 내에서 캐싱되어 사용됩니다. 불필요한 객체 생성을 방지하고, 효율적으로 재사용됩니다.

5. Float/Double NaN Cache

위치: 힙(Heap) 영역 내
설명:FloatDouble 타입의 특별한 값인 NaN(Not-a-Number)은 힙 내에서 캐싱되어 사용됩니다. 동일한 NaN 값은 동일한 객체로 관리하여 메모리 사용을 줄입니다.

6. Class Constant Pool (클래스 상수 풀)

위치: 클래스 파일 내에 존재하며, 메서드 영역으로 로드됨
설명: 클래스 파일 자체에 포함된 상수 풀은 파일의 일부로 존재하며, 클래스가 로드될 때 메서드 영역으로 로드되어 런타임 상수 풀의 일부로 사용됩니다. 클래스 상수 풀에는 클래스 이름, 메서드 시그니처, 문자열 리터럴 등이 포함되어 있으며, 이는 런타임 상수 풀에서 참조됩니다.

7. Method Handle Cache (메서드 핸들 캐시)

위치: 힙(Heap) 영역 내의 특정 캐시
설명: 메서드 핸들 캐시는 JVM이 메서드 참조를 최적화하기 위해 힙 내에 관리하는 캐시입니다. 메서드 핸들은 주로 동적 메서드 호출과 관련된 최적화를 위해 사용되며, 메서드 핸들 캐시를 통해 자주 사용하는 메서드 참조를 효율적으로 관리합니다.

JNI (Java Native Interface)

NOTE
JNI는 자바가 다른 언어로 만들어진 애플리케이션과 상호작용할 수 있는 인터페이스 제공 프로그램이다.
JNI는 JVM이 Native Method를 적재하고 수행할 수 있도록 한다.

Native Method Library

NOTE
C, C++로 작성된 라이브러리를 칭한다.
만일 헤더가 필요하다면 JNI는 이 라이브러리를 로딩해 실행한다.

가비지 컬렉션

NOTE
가비지 컬렉션 ⇒ JVM의 Heap 영역에서 동적으로 할당했던 메모리 영역 중 필요 없게 된 메모리 영역을 주기적으로 삭제하는 프로세스!
아무런 참조 변수를 가지고 있지 않은 객체 ⇒ 쓰레기 객체

가비지 컬렉션의 단점

NOTE
1.
개발자가 메모리가 언제 해제되는지 정확하게 알 수 없다.
2.
가비지 컬렉션(GC)가 동작하는 동안에는 다른 동작을 멈추기 때문에 오버헤드가 발생한다.
이로인해 gc가 너무 자주 실행되면 성능 하락의 문제가 발생한다.
실시간으로 계속 동작해야하는 시스템의 경우, 잠깐의 정지로 결과가 달라지므로 gc의 사용이 적합하지 않알 수 있다

Mark And Sweep 알고리즘

NOTE
Mark And Sweep ⇒ 가비지 컬렉션이 동작하는 원리로, 루트에서부터 해당 객체에 접근가능한지에 대한 여부를 메모리 해제의 기준으로 삼는다!
Mark 과정
Root로부터 그래프 순회를 통해 연결된 객체들을 찾아내어 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
Sweep 과정
참조하고 있지 않은 객체 즉 Unreachable 객체들을 Heap에서 제거한다.
Compact 과정
Sweep 후에 분산된 객체들은 Heap의 시작 주소로 모아 메모리가 할당된 부분과 그렇지 않은 부분으로 압축한다.