1. Java란 무엇인가: 특징, 장점, 그리고 사용하는 이유에 대해
1-1. Java의 주요 특징과 장점

- 객체 지향적인 언어로 추상화, 다형성, 캡슐화를 지원 -> 확장성이 뛰어나고 대규모 엔터프라이즈 개발에 적합
- Write Once, Run Anywhere -> JVM을 통한 플랫폼 독립성 확보
- Garbage Collection 기반 메모리 관리 -> 개발자가 메모리를 직접 해제하지 않아도 되는 편의성
- 풍부한 라이브러리와 생태계 -> 표준 API, 오픈 소스 프레임워크를 통한 생산성
🧠 객체지향과 엔터프라이즈 개발의 궁합
Java는 추상화와 인터페이스 중심의 설계를 장려합니다. 이를 통해 유연하고 확장 가능한 아키텍처를 만들 수 있으며, 팀 간 모듈 분리도 용이합니다. 엔터프라이즈 시스템처럼 변화와 확장을 전제로 한 개발에 매우 적합합니다.
🧠 운영체제 관점에서 보는 Java의 특별함
일반 애플리케이션(C/C++ 등)은 실행 시 운영체제의 시스템 콜(API)을 직접 호출하여 파일, 메모리, 프로세스 등을 제어합니다. 이때 애플리케이션은 OS에 강하게 종속되며, 다른 플랫폼에서 실행하려면 재컴파일이 필요합니다.
반면, Java 애플리케이션은 JVM 위에서 동작합니다. 개발자가 작성한 바이트코드는 JVM이 해석하거나 JIT 컴파일을 통해 운영체제에 맞는 시스템 콜로 변환해 실행합니다. 이로 인해 Java 프로그램은 OS에 독립적으로 실행될 수 있습니다.
1-2. Java는 어디에 주로 쓰일까?
- 백엔드 시스템 -> Spring Framework
- 안드로이드 앱 개발 -> Java & Kotlin
2. JDK, JRE, JVM 차이점과 관계: Java 개발자를 위한 개념 정리

2-1. JDK (Java Development Kit)
- Java 프로그램을 개발할 수 있도록 제공되는 전체 패키지
- JRE + 컴파일러 + 디버거 + 문서화 도구 등으로 구성
2-2. JRE (Java Runtime Environment)
- Java 프로그램을 실행하기 위한 환경
- JVM과 Java 표준 라이브러리를 포함
2-3. JVM (Java Virtual Machine)
- 컴파일러에 의해 변환된 바이트코드를 실행하는 환경
- 운영체제 위에서 동작하며, Java의 플랫폼 독립성을 실현하는 핵심 요소
💡 요약하자면
1. Java로 작성한 코드는 JDK에 포함된 컴파일러에 의해 바이트코드로 변환됩니다.
2. 이 바이트코드는 JRE 안에 있는 JVM에서 실행됩니다.
이 구조 덕분에 Java는 운영체제에 상관없이 동일하게 실행될 수 있는 플랫폼 독립성을 가집니다.
3. Java 코드가 실행되는 과정: 컴파일부터 JVM 동작까지 한눈에 보기

🧠 JVM의 핵심 구성 요소 요약
🔹 Class Loader Subsystem
- .class 파일(바이트코드)을 JVM 내부로 로딩하는 역할을 합니다.
- 클래스를 메모리에 올리고, 필요한 의존 클래스를 동적으로 찾아 로드합니다.
🔹 Runtime Data Area
- JVM이 실행 중 사용하는 모든 메모리 영역의 집합입니다.
- Heap, Stack, Method Area 등으로 구성되며, 자바 프로그램이 실제 동작하는 공간입니다.
🔹 Execution Engine
- 로딩된 바이트코드를 실제 실행하는 컴포넌트입니다.
- 인터프리터 또는 JIT 컴파일러를 사용하여 바이트코드를 기계어로 변환해 실행합니다.
🔹 JNI (Java Native Interface)
- C, C++ 등 네이티브 코드와 상호작용할 수 있도록 도와주는 인터페이스입니다.
- OS나 외부 라이브러리 기능을 호출할 때 사용됩니다.
🔹 Native Libraries
- OS가 제공하는 시스템 수준의 **기능(파일 I/O, 네트워크 등)**을 사용하는 네이티브 코드입니다.
- JNI를 통해 JVM과 연결되어 호출됩니다.
⚙️ Java 코드 실행 시 내부 동작 흐름
1. Class Loader가 .class 파일을 메모리에 로드합니다.
2. 로딩된 클래스는 Runtime Data Area에 배치됩니다.
3. Execution Engine이 해당 바이트코드를 읽어 인터프리터 또는 JIT 컴파일러로 실행합니다.
4. 필요한 경우 JNI를 통해 Native Libraries를 호출하여 OS 기능을 사용합니다.
-> 주로 운영체제의 저수준 기능이나 기존 C/C++ 라이브러리 연동 시 사용됩니다.
이 과정을 통해 자바 애플리케이션은 운영체제에 독립적으로 실행되며, JVM이 중간에서 모든 연결과 실행을 담당합니다.
4. JVM의 메모리 구조: Runtime Data Area
4-1. Runtime Data Area란?
- JVM이 실행 중 사용하는 논리적 메모리 공간들의 집합
- Java 애플리케이션이 실행되면서 필요한 데이터를 저장하거나 추적하는 실질적인 작업 공간
- 실행 중 클래스, 객체, 메서드 호출, 지역 변수 등이 이 영역에 적재되고 삭제됨
- 이 영역을 이해하면 GC, OOM, 성능 최적화의 원리를 파악할 수 있습니다.
4-2. 주요 영역
| 영역 | 설명 |
| Heap | - 객체 인스턴스가 저장되는 공간 + StringPool - GC 대상이 되며, JVM 전체에서 공유 |
| Stack | - 각 스레드마다 존재 - 메서드 호출 시 스택 프레임에 쌓임 (FILO) - 지역 변수를 관리 |
| Method Area | - 클래스 정보, Static 변수, Constant Pool - 보통 클래스 최초 사용 시점에 적재 |
| PC Register | - 각 스레드의 현재 실행 위치를 추적 |
| Native Method Stack | - JNI를 통한 C/C++ 등 네이티브 메서드 호출용 스택 |
🧠 PC Register와 Native Method Stack이 필요한 이유와 동작 방식
🔹 PC Register – 스레드별 실행 위치 추적용 레지스터
- JVM은 멀티스레드를 지원하며, 각 스레드는 자신만의 PC Register를 가집니다.
- 이 레지스터는 현재 실행 중인 바이트코드 명령어의 위치를 추적합니다.
- 예를 들어, 정수 더하기(iadd)나 메서드 호출(INVOKEVIRTUAL) 같은 바이트코드를 실행할 때
-> PC Register는 “지금 이 명령을 실행 중이다”는 위치 정보를 저장합니다.
👉 OS의 실제 레지스터와는 무관한, JVM 내부 전용 가상 레지스터입니다.
🔹 Native Method Stack – 네이티브 코드 실행용 전용 스택
1. Java 코드에서 native 키워드를 통해 C/C++ 등의 외부 메서드를 호출합니다.
2. JVM은 Native Method Stack에 새로운 프레임을 쌓고 실행 흐름을 넘깁니다.
3. 이때 Java Stack은 그 위치에서 일시 대기하며, 네이티브 코드가 완료되면 결과를 return 합니다.
4. JVM은 다시 Java Stack으로 복귀하여 이전 흐름을 이어서 실행합니다.
👉 이는 OS 콜백이 아니라, JVM 내부의 함수 호출 기반 흐름 제어 방식입니다.
5. JVM 클래스 로딩 과정 완전 이해: 로딩, 링크, 초기화

5-1. 클래스 로딩은 언제(when), 왜(why) 요청되는가?
public class Main {
public static void main(String[] args) {
// Dynamic Class Loading
SomeClass some1 = new SomeClass();
// Already in Method Area
SomeClass some2 = new SomeClass();
}
}
- Java에서 클래스는 필요할 때 동적으로 로딩 -> 동적 클래스 로딩
- 미리 모든 클래스를 로딩하는게 아니라 참조 시점에 클래스 로딩 요청이 발생
❓ 그럼 언제 로딩이 발생할까?
- new 키워드를 통해 생성자 호출 시
- 클래스의 static 필드 또는 메서드에 접근할 때
- 리플렉션 API를 통해 클래스 정보를 조회할 때
👉 이때 JVM은 .class 파일을 찾아 로딩하며, 클래스 정보는 Method Area에 저장
♻️ 단, 클래스는 한 번만 로딩
- JVM은 동일한 클래스가 이미 로딩되어 있는 경우, Method Area에 이미 존재하는 클래스 정보를 재사용
- 클래스는 JVM 내에서 단 한 번만 로딩되며, 이후에는 메모리상에 존재하는 정보를 참조하는 방식으로 동작
👉 클래스 재사용성 보장 + 메모리 절약 + 실행 속도 향상
5-2. JVM은 클래스를 어떻게(how) 찾아오는가?

- JVM은 클래스 로더라는 계층 구조를 통해서 .class 파일을 탐색하고 로딩
- 클래스 로딩은 위에서 아래로 위임하는 구조로 상위 로더에서부터 하위 로더로 탐색을 시도
| BootStrap ClassLoader | JVM이 내장한 최상위 로더 |
| Extension ClassLoader | 확장 라이브러리를 위한 로더 |
| Application ClassLoader | 애플리케이션 클래스패스의 파일 (대부분 개발자가 작성) |
5-3. 클래스 로딩 전체 과정(Load → Link → Initialize)
- Loading
- 클래스 파일을 찾아서 바이트코드로 읽는 과정
- 클래스 로더로 탐색하여 Method Area에 적재
- Linking
- Verify: 바이트 코드가 유효한지 검사하는 단계
- Prepare: static 필드 메모리 할당, 기본값으로 초기화
- Resolve: 심볼릭 참조를 실제 메모리 참조로 변환
- Initializaion
- static 블록이나 static 필드가 실제로 초기화되는 시점
- 실제 코드가 실행되는 최초의 로직이 시작
🧠 Linking vs Initilization
Linking -> static 변수에 대해 메모리 할당 + 기본값(0, null 등)으로 초기화
👉 이 단계에서 static 변수에 대한 메모리만 잡히고, 내가 지정한 값은 아직 적용되지 않습니다.
Initialization -> static {} 블록이나 static int count = 10; 과 같은 사용자 정의 초기화
👉 이 단계에서 프로그래머가 지정한 값으로 적용됩니다.
6. JVM 메모리 동작: 메서드 호출부터 객체 생성까지

6-1. 예제 코드
public class Main {
public static void main(String[] args) {
User user = new User("DongHwan");
user.greet();
}
}
public class Constants {
public static final String PREFIX = "Hello";
public static final String DYNAMIC = new String(" from JVM");
static {
System.out.println("Constants loaded");
}
}
public class User {
private String name;
public User(String name) {
this.name = name;
}
public String buildMessage() {
return Constants.PREFIX + ", " + name + Constants.DYNAMIC;
}
public void greet() {
String message = buildMessage();
System.out.println(message);
}
}
6-2. 동작 흐름 with 예제

[1] 컴파일 시점
- public static final String PREFIX = "Hello"가 ConstantPool에 저장
- 이미 ConstantPool에 등록되어서 실행 시점에 Constants 클래스를 로딩하지 않음
- public static final로 선언된 Primitive 타입과 String 리터럴은 컴파일 시점에 상수로 인식됨
- Primitive 타입은 바이트코드에 인라인
- String 리터럴은 각 클래스의 상수 풀(Constant Pool)에 등록된 뒤, 실행 시 최초로 사용될 때 Heap의 String Pool에 초기화되며, 바이트코드의 ldc + putstatic 조합을 통해 해당 필드에 할당
[2] 애플리케이션 실행 시점
- public static void main(String[] args)가 실행
- JVM이 Main 클래스를 로딩하고, main() 메서드를 호출
- Stack에 main()의 StackFrame이 생성되고, 지역 변수 공간이 준비
[3] new User("Donghwan"); 호출 시점
- User.class가 아직 로딩되지 않았다면 이 시점에 로딩 (Method Area)
- new 키워드로 Heap 영역에 User 인스턴스 생성
- User(String name) 생성자 호출로 새로운 Frame을 Stack에 Push -> Stack에 main()과 User(String name)이 존재
- 생성을 마치고 User(String name)을 Stack에서 Pop -> Stack에 main()만 존재
[4] user.greet() 호출 시점
- greet() 메서드 호출로 Stack에 새로운 Frame Push -> Stack에 main()과 user.greet() 존재
- 내부에서 buildMessage() 호출로 새로운 Frame Push -> Stack에 main()과 greet(), buildMessage() 존재
- 이 시점에 Constants.PREFIX는 이미 상수풀에 존재하지만 Constants.DYNAMIC은 참조가 필요 -> String 클래스 초기화 때문
- Constants 클래스 로딩 후, Constants의 static {} 블록 실행 → "Constants loaded" 출력
- DYNAMIC = new String(...) 실행 → " from JVM"이 Heap에 생성
[5] 문자열 연결 및 출력
- PREFIX + name + DYNAMIC을 이어서 새 문자열 생성 -> String 불변 속성으로 StringPool에 문자열 생성
- buildMessage() 메서드 결과 반환으로 Stack에서 Pop -> Stack에 main()과 greet()존재
- System.out.println() 호출로 Native Method Stack에 Stack 생성 -> JVM Stack 대기
[6] Stack Frame 정리 및 종료
- System.out.println() 결과 Return으로 Native Stack Pop -> JVM Stack 동작
- greet() 메서드 결과 반환으로 Stack에서 Pop -> Stack에 main() 존재
- main() 메서드 결과 반환으로 Stack에서 Pop -> Stack이 비어서 스레드 종료