자바의 근본을 파헤쳐보자~자바의 기원부터 메모리관리까지~
2025. 10. 3. 14:31

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 (약한 세대 가설)

  1. 대부분의 객체는 금방 죽는다 (짧은 생명주기)
  2. 오래된 객체에서 젊은 객체로의 참조는 드물다

실제 통계:

  • 생성된 객체의 약 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으로의 승급

승급 조건

  1. Age 임계값 도달: 기본적으로 age가 15 이상이 되면
  2. 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