1. 왜 자바는 등장했을까?
역사적 배경 (1990년대 초)
- 1991년, Green Project: Sun Microsystems의 James Gosling이 이끄는 팀이 가전제품용 소프트웨어 개발 시작
- 당시의 문제점들:
- C/C++로 작성된 코드는 플랫폼마다 다시 컴파일해야 함
- 셋톱박스, TV, VCR 등 다양한 하드웨어에서 동작해야 했는데, 각각 다른 아키텍처와 운영체제 사용
- 포인터 관리의 복잡성으로 메모리 누수, 세그멘테이션 오류 등이 빈번
- 네트워크 환경이 확산되면서 보안 문제가 대두
- 초기 이름: Oak (제임스 골스링의 사무실 밖 참나무에서 유래) → 상표권 문제로 Java로 변경
- 웹의 등장과 방향 전환:
- 1993년 Mosaic 브라우저 등장으로 웹 시대 개막
- 가전제품보다 웹 애플릿에 집중
- 1995년 5월 23일, Java 1.0 공식 발표
2. 해당 근거에 의거하여 등장한 자바의 주요 원칙은 무엇일까?
WORA (Write Once, Run Anywhere)
당시 가장 혁명적인 개념. 한 번 작성한 코드가 어디서든 실행된다는 철학.

3. 자바의 구동환경은 그래서 어떻게 되어있을까?
JDK, JRE, JVM의 관계

실행 과정
Source.java
↓ javac (컴파일러)
Source.class (바이트코드)
↓ JVM
↓
1. Class Loader: .class 파일을 메모리에 로드
2. Bytecode Verifier: 바이트코드 검증 (보안, 규칙 준수)
3. Execution Engine:
- Interpreter: 바이트코드를 한 줄씩 해석 실행
- JIT Compiler: 자주 실행되는 코드를 네이티브 코드로 컴파일
4. Garbage Collector: 사용하지 않는 객체 메모리 회수
왜 이렇게 설계했나?
- 바이트코드: 플랫폼 중립적인 중간 코드. JVM만 있으면 어디서든 실행
- JIT 컴파일러: 초기에는 느렸지만, JIT로 성능 향상 (Hot Spot 최적화)
- 보안 검증: 네트워크로 전송된 코드도 안전하게 실행
4. 자바는 메모리 관리를 어떻게 할까?
JVM 메모리 구조 (Runtime Data Areas)
구체적 메모리 할당 예시
public class MemoryExample {
static int staticVar = 100; // Method Area
public void method() {
int localVar = 10; // Stack (원시 타입 값)
String str = "Hello"; // Stack에 참조, Heap에 객체
Person p = new Person("김철수");
// p: Stack에 참조 주소 저장
// new Person("김철수"): Heap에 객체 생성
}
}
메모리 배치:
Method Area:
staticVar = 100
MemoryExample 클래스 메타데이터
Stack (method() 호출 시):
localVar = 10
str = 0x1234 (힙 주소 참조)
p = 0x5678 (힙 주소 참조)
Heap:
0x1234: "Hello" (String 객체)
0x5678: Person 객체 {name: "김철수"}
4-2. Garbage Collection 상세 동작 원리
Heap 메모리의 세대별 구조 (Generational GC)
왜 이렇게 나눴을까?
Weak Generational Hypothesis (약한 세대 가설)
- 대부분의 객체는 금방 죽는다 (짧은 생명주기)
- 오래된 객체에서 젊은 객체로의 참조는 드물다
실제 통계:
- 생성된 객체의 약 98%가 첫 Minor GC에서 제거됨
- 따라서 자주 죽는 객체들을 따로 관리하면 효율적!
Minor GC의 동작 과정 (Young Generation)
초기 상태
Eden: [객체들이 계속 생성됨]
S0: [비어있음]
S1: [비어있음]
1단계: Eden이 가득 참 → 첫 번째 Minor GC 발생
// 코드 실행 중...
Person p1 = new Person("김철수"); // Eden에 생성
Person p2 = new Person("이영희"); // Eden에 생성
Person p3 = new Person("박민수"); // Eden에 생성
// ... Eden이 가득 참!
GC 동작:
GC 전:
Eden: [p1, p2, p3, ... 수많은 객체]
S0: [비어있음]
S1: [비어있음]
GC 실행:
1. GC Root부터 추적 (Mark)
2. 살아있는 객체만 S0으로 복사
3. Eden 전체 클리어
GC 후:
Eden: [비어있음] ← 깨끗!
S0: [p1(age:1), p2(age:1)] ← 살아남은 객체들, age 증가
S1: [비어있음]
2단계: Eden이 또 가득 찬다 → 두 번째 Minor GC
GC 전:
Eden: [p4, p5, p6, ... 새로운 객체들]
S0: [p1(age:1), p2(age:1)]
S1: [비어있음]
GC 실행:
1. Eden + S0의 살아있는 객체를 S1으로 복사
2. age 증가
3. Eden과 S0 클리어
GC 후:
Eden: [비어있음]
S0: [비어있음]
S1: [p1(age:2), p4(age:1)] ← S0과 S1 역할 교체!
3단계: 계속 반복 (S0 ↔ S1 스왑)
GC 전:
Eden: [p7, p8, ...]
S0: [비어있음]
S1: [p1(age:2), p4(age:1)]
GC 후:
Eden: [비어있음]
S0: [p1(age:3), p4(age:2), p7(age:1)] ← 다시 S0으로!
S1: [비어있음]
왜 Survivor가 2개일까?
단일 Survivor의 문제:
Eden → Survivor (단일) 방식이라면:
1. Eden의 살아있는 객체를 Survivor로 복사
2. Eden 클리어
3. 다음 GC 때: Eden + Survivor의 객체 중 살아있는 것만...
→ 어디로? Survivor는 이미 사용 중!
→ 메모리 단편화 발생 (Fragmentation)
2개 Survivor의 해결책:
항상 한쪽은 비어있음 (From ↔ To 스왑)
→ 살아있는 객체만 깨끗하게 복사 (Copy & Clean)
→ 메모리 단편화 없음 (Compaction 효과)
Promotion: Old Generation으로의 승급
승급 조건
- Age 임계값 도달: 기본적으로 age가 15 이상이 되면
- Survivor 공간 부족: S0/S1이 가득 차면 조기 승급
Minor GC 반복...
S0: [p1(age:15)] ← 오래 살아남았다!
다음 Minor GC:
p1 → Old Generation으로 Promotion!
Old Generation: [p1] ← 이제 여기서 관리
Age는 어떻게 관리될까?
java
// 객체 헤더에 age 정보 저장 (4비트)
// 0000 (age: 0) → 0001 (age: 1) → ... → 1111 (age: 15)
Major GC (Full GC) - Old Generation
언제 발생하는가?
- Old Generation이 가득 찰 때
- System.gc() 명시적 호출 시 (권장하지 않음!)
- Metaspace 부족 시
특징
Minor GC vs Major GC
Minor GC:
- 대상: Young Generation (작은 공간)
- 빈도: 매우 자주 (수 밀리초마다)
- 속도: 빠름 (보통 10ms 이하)
- Stop-The-World: 짧음
Major GC (Full GC):
- 대상: Old Generation + Young Generation (전체 힙)
- 빈도: 드물게 (분 단위)
- 속도: 느림 (수백 ms ~ 수 초)
- Stop-The-World: 길어서 문제!
5. 메모리 관리의 중요성을 보여주는 사례는?
사례 1: String의 불변성과 String Pool
String의 특별한 설계
java
String s1 = "Hello"; // String Pool (Method Area)
String s2 = "Hello"; // 같은 객체 재사용
String s3 = new String("Hello"); // Heap에 새 객체
System.out.println(s1 == s2); // true (같은 참조)
System.out.println(s1 == s3); // false (다른 참조)
왜 String은 불변(Immutable)일까?
① String Pool 공유를 위해
String a = "안녕";
String b = "안녕"; // 같은 객체 재사용으로 메모리 절약
만약 가변이라면:
a.setValue("잘가"); // 가변이라면?
System.out.println(b); // "잘가" 출력 - 예상치 못한 동작!
② 보안 (Security)
String username = "admin";
authenticate(username); // username이 중간에 변경되지 않음을 보장
③ Thread-Safety
- 불변 객체는 자동으로 스레드 안전
④ HashCode 캐싱
// String은 hashCode를 한 번만 계산하고 캐싱
// HashMap, HashSet 등에서 성능 향상
사례 2: String 연결의 함정 (메모리 낭비)
문제 상황
String result = "";
for (int i = 0; i < 1000; i++) {
result += i; // 매번 새 String 객체 생성!
}
무슨 일이 일어나나?
반복 1: "" + 0 → "0" (새 객체 1)
반복 2: "0" + 1 → "01" (새 객체 2, 객체 1은 버려짐)
반복 3: "01" + 2 → "012" (새 객체 3, 객체 2는 버려짐)
...
반복 1000까지
결과:
- 1000개의 임시 String 객체 생성 (대부분 즉시 가비지)
- 시간 복잡도: O(n²) (매번 전체 문자열 복사)
- 메모리: 약 500KB의 임시 객체 (1+2+3+...+999 문자 복사)
해결책: StringBuilder
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
sb.append(i); // 내부 char 배열에 추가, 객체 생성 없음
}
String result = sb.toString(); // 마지막에 한 번만 String 생성
장점:
- 시간 복잡도: O(n)
- 메모리: 단 1개의 StringBuilder + 1개의 최종 String
- 약 500배 적은 가비지 생성
실제 성능 비교
// 나쁜 예: 약 4000ms, 500,000개 임시 객체
long start = System.currentTimeMillis();
String bad = "";
for (int i = 0; i < 10000; i++) {
bad += i;
}
System.out.println(System.currentTimeMillis() - start);
// 좋은 예: 약 2ms, 거의 객체 생성 없음
start = System.currentTimeMillis();
StringBuilder good = new StringBuilder();
for (int i = 0; i < 10000; i++) {
good.append(i);
}
String result = good.toString();
System.out.println(System.currentTimeMillis() - start);
사례 3: 메모리 누수 예방
Java에서도 메모리 누수가 발생한다
public class Cache {
private Map<String, Object> cache = new HashMap<>();
public void put(String key, Object value) {
cache.put(key, value); // 계속 쌓임, GC 안됨
}
// 해결책: WeakHashMap 사용 또는 명시적 제거
}
리스너 등록 후 해제 안 함
button.addActionListener(listener);
// 나중에 button 제거해도 listener는 메모리에 남음
// 해결: button.removeActionListener(listener);
'개발공부 > JAVA' 카테고리의 다른 글
| 자바의 제어문 1) 조건문(if) (0) | 2025.10.04 |
|---|---|
| 자바의 연산자 (0) | 2025.10.04 |
| 인코딩과 디코딩 (0) | 2025.10.04 |
| JAVA의 변수와 자료형 (0) | 2025.10.04 |
| 자바의 기본적인 출력 메소드 (0) | 2025.10.04 |