2주차 Unit 1.6 — Literal Pool Area

Psj·2026년 5월 15일

F-lab

목록 보기
55/230

Unit 1.6 — Literal Pool Area

F-LAB JAVA · 2주차 · Phase 1 · 자바 변수 ↔ 메모리 영역의 매핑
🏁 Phase 1 마지막 Unit


📌 학습 목표

이 Unit을 끝내면 다음을 답할 수 있어야 한다.

  • String Constant Pool은 정확히 어느 메모리 영역에 있는가?
  • String a = "abc"; String b = "abc"; 일 때 String 객체는 메모리에 몇 개?
  • "abc" 리터럴과 new String("abc") 의 메모리 차이는?
  • String.intern() 은 정확히 무엇을 하는가?
  • Integer Cache 의 동작과 -128 ~ 127 경계의 의미는?
  • Java 6 → 7 → 8 → 9 에서 String 처리는 어떻게 진화했나?
  • Pool을 잘못 쓰면 어떤 메모리 누수가 일어나는가?

🎯 핵심 한 문장

Literal Pool은 "자주 쓰이는 불변 값을 재활용하기 위한 캐시"다.
String 리터럴, 작은 Integer, Boolean 등이 미리 만들어져 풀에 저장되며,
같은 값이 다시 등장하면 새로 만들지 않고 풀의 기존 객체를 가리킨다.
메모리 절약 + == 비교 가능 + 빠른 접근의 3가지 이점.

비유 — 도서관의 공용 도서

시나리오비유
리터럴 사용공용 도서를 빌려 읽음 (도서 1권 = 모두가 같은 책)
new String직접 같은 책을 사서 가짐 (책 1권을 매번 새로 인쇄)
intern() 호출자기 책을 도서관에 기부해서 공용으로 만듦

도서관(Pool)에는 같은 책이 단 1권만 존재. 책 100명이 빌려 봐도 1권으로 충분.
같은 제목의 책을 직접 사면(=new String) 도서관 책과 별개의 사본이 생김.


🧭 9개 섹션 로드맵

1. Literal Pool의 탄생 배경
2. String Constant Pool 깊이 보기
3. 리터럴 vs new String — 메모리 추적
4. String.intern() 메커니즘
5. JVM의 다른 풀들 — Integer Cache · Boolean · null
6. String의 진화 — Java 6/7/8/9의 변화
7. ILIC 실무 — Pool 활용과 함정
8. 흔한 실수 + 디버깅
9. 면접 질문 + 자기 점검 + Phase 1 정리

1️⃣ Literal Pool의 탄생 배경

1.1 문제 — String이 너무 많이 만들어진다

자바 프로그램에서 가장 자주 등장하는 객체는 무엇일까?

jmap -histo:live <PID> | head

거의 모든 자바 앱에서:

 num     #instances         #bytes  class name
   1:       1500000       45000000  [B (byte arrays)
   2:        800000       19200000  java.lang.String      ← 거의 항상 상위
   3:        300000        4800000  java.util.HashMap$Node
   ...

String은 전체 객체의 20~40%.
→ 만약 매번 새 객체로 만들면 메모리 폭발.

1.2 String의 특징 — 같은 값이 반복된다

// 어떤 ILIC Service 메서드
log.info("Shipment created: {}", shipmentId);
log.info("Shipment created: {}", anotherId);
log.info("Shipment created: {}", yetAnotherId);

// 모두 "Shipment created: {}" 라는 같은 문자열을 씀

같은 리터럴이 코드 곳곳에 등장. 매번 새 객체를 만든다면 너무 낭비.

1.3 해결책 — 풀 (Pool)

1번만 만들고 재사용.

String a = "abc";    // 풀에 "abc" 만들고 그 참조 반환
String b = "abc";    // 풀의 기존 "abc" 참조 반환

a == b;              // true  ← 같은 객체!

메모리 절약 + 참조 비교 가능 (== 만으로 동등성 판단).

1.4 어떤 값이 풀에 들어갈 자격이 있나?

조건:

  • 불변이어야 함 — 변경되면 모든 사용처가 영향 받음
  • 자주 쓰임 — 빈도가 낮으면 풀 자체가 오버헤드
  • 상대적으로 작음 — 너무 크면 풀 자체가 부담

자바의 답:

  • String 리터럴
  • 작은 Integer (-128 ~ 127)
  • Boolean (TRUE / FALSE 단 2개)
  • null (개념적 단일 인스턴스)

→ 이번 Unit에서 모두 다룬다.


2️⃣ String Constant Pool 깊이 보기

2.1 위치 — Java 버전에 따라 변화

이게 시험 단골:

Java 6 이전:    String Pool ⊂ Method Area (PermGen)
Java 7 이후:    String Pool ⊂ Heap  ★ 현재
Java 8 이후:    PermGen → Metaspace, String Pool은 그대로 Heap

왜 옮겼나?

  • PermGen은 크기 고정 → String 많으면 OutOfMemoryError: PermGen
  • Heap은 동적 확장 + GC 가능 → String Pool도 GC 대상
Java 6 이전 - PermGen에 String Pool:
  - String이 너무 많으면 PermGen 가득 차서 OOM
  - 잘 안 쓰는 String도 절대 안 사라짐

Java 7+ - Heap에 String Pool:
  - 사용 안 하는 String은 GC 대상
  - 동적으로 크기 조정

2.2 Heap의 어디?

Heap:
  ┌──────────────────────────────┐
  │ Young Generation              │
  │   ├─ Eden                     │
  │   └─ Survivor                 │
  ├──────────────────────────────┤
  │ Old Generation                │
  │   ┌────────────────────────┐ │
  │   │ String Constant Pool    │ │  ← 보통 Old Gen 근처
  │   │  "abc", "hello", ...    │ │     (해시 테이블 구조)
  │   └────────────────────────┘ │
  └──────────────────────────────┘

내부 구현은 해시 테이블.

# 풀 크기 조정
-XX:StringTableSize=1009    # 기본값, 소수

큰 앱이라면 늘려서 충돌 줄임.

2.3 무엇이 들어가나

// 코드 안의 모든 String 리터럴
"Shipment"
"BL-001"
""
"abc"

// + intern() 호출된 String
someDynamicString.intern();

컴파일 시점에 풀 등록:

A.java의 모든 String 리터럴
   ↓ javac
A.class의 상수 풀에 String 항목 기록
   ↓ JVM 클래스 로딩 시
String Pool에 자동 등록

2.4 풀 안의 String 객체 자체는 Heap 객체

String Pool은 "표"일 뿐:
  ┌────────────────────────────────┐
  │ Hash → String 객체 참조          │
  │   8923 → 0x7f4a2c01 ("abc")    │
  │   1234 → 0x7f4a3d12 ("hello") │
  └────────────────────────────────┘

실제 String 객체:
  Heap의 어딘가:
    String @0x7f4a2c01  ← value = ['a','b','c']
    String @0x7f4a3d12  ← value = ['h','e','l','l','o']

→ 풀은 참조의 목록일 뿐, 실제 String 데이터는 Heap에 있는 일반 객체.
→ 따라서 "공유" = "같은 Heap 객체를 여러 변수가 가리킴".

2.5 확인 코드

String a = "Shipment";
String b = "Shipment";
String c = new String("Shipment");

System.out.println(a == b);                       // true   ← 같은 풀 객체
System.out.println(a == c);                       // false  ← c는 새 객체
System.out.println(a.equals(c));                  // true   ← 값은 같음
System.out.println(a == c.intern());              // true   ← intern은 풀 참조 반환

→ Pool 이해의 핵심 4줄.


3️⃣ 리터럴 vs new String — 메모리 추적

3.1 리터럴

String a = "abc";

JVM 동작:

Step 1: String Pool 검색 — "abc" 있나?
Step 2-A (있으면): 기존 객체 참조 반환
Step 2-B (없으면): 새 String 객체 생성 + 풀에 등록 + 참조 반환
Step 3: 변수 a에 그 참조 저장

메모리:

Stack:
  a = 0x7f4a2c01 ──┐
                   │
Heap (String Pool에 등록됨):
  "abc" @0x7f4a2c01 ◄┘

3.2 new String

String b = new String("abc");

JVM 동작:

Step 1: "abc" 리터럴이 풀에 있는지 확인 → 있다면 그대로, 없다면 풀에 등록
Step 2: Heap에 별도의 새 String 객체 생성
Step 3: 그 새 객체의 value는 풀 "abc"의 value를 복사 (또는 공유)
Step 4: 변수 b에 새 객체 참조 저장

메모리:

Stack:
  b = 0x7f4a4e23 ──┐
                   │
Heap:              │
  "abc" @0x7f4a2c01 (풀에 있음, b는 안 가리킴)
  String @0x7f4a4e23 (b가 가리킴) ◄┘  ← 별도 객체!

객체 2개.

  • 풀의 "abc" (다른 리터럴 사용자가 가리킴)
  • new로 만든 별개 String (b만 가리킴)

3.3 둘 다 등장하는 경우

String a = "abc";              // 풀의 객체
String b = new String("abc");  // 새 객체
String c = "abc";              // 풀의 객체 (a와 같음)
String d = new String("abc");  // 새 객체 (b와 다른 또 다른 새 객체)
Heap:
  "abc" @0x...01 (풀)  ◄── a, c가 가리킴
  String @0x...02 (새) ◄── b만 가리킴
  String @0x...03 (새) ◄── d만 가리킴

비교:

a == b;       // false
a == c;       // true
b == d;       // false (둘 다 new지만 별개 객체)
a.equals(d);  // true

3.4 핵심 — String Pool 객체는 절대 하나뿐

String s = "Shipment";          // 풀 등록 (있으면 기존, 없으면 신규)
String t = "Shipment";          // 기존 풀 객체
String u = "Ship" + "ment";     // 컴파일 타임 상수 폴딩 → "Shipment" 리터럴 → 풀 객체
String v = "Ship" + dynamicMent;// 런타임 결합 → 새 객체 (풀 X)

s == t;    // true
s == u;    // true   ← 컴파일러가 "Shipment"로 미리 합침
s == v;    // false  ← 동적 결합은 new로 처리

컴파일 타임에 결정되는 String은 풀, 런타임 결합은 새 객체.

3.5 메모리 절약 효과 — 실측

가상 시나리오: 1만 건의 화물 로그.

// 방법 A — 리터럴 사용
for (int i = 0; i < 10_000; i++) {
    log.info("Shipment processed", shipment);
    //          ↑ 풀의 같은 객체 사용
}
// 방법 B — 매번 new String (의도적 나쁜 예)
for (int i = 0; i < 10_000; i++) {
    log.info(new String("Shipment processed"), shipment);
    //       ↑ 매번 새 객체!
}
방법String 객체 수메모리
A (리터럴)1개≈ 50 bytes
B (new)10,001개≈ 500 KB

→ 1만 배 차이. 리터럴을 적극 사용하라는 결론.


4️⃣ String.intern() 메커니즘

4.1 정의

public native String intern();

동작: 호출하는 String 객체의 값이 풀에 있으면 그 풀 참조 반환, 없으면 풀에 등록 후 반환.

String dynamic = new String("hello");   // 풀 외부의 객체
String interned = dynamic.intern();      // 풀의 "hello" 참조

dynamic == interned;     // false  (서로 다른 객체)
"hello" == interned;     // true   (둘 다 풀 객체)

4.2 시나리오 — 언제 쓰는가

시나리오 1: 동적으로 만든 String을 풀로 옮기기

String fromDb = resultSet.getString("status");  // DB에서 가져온 동적 String
String pooled = fromDb.intern();                 // 풀 등록 또는 기존 사용

// 이후 비교를 == 으로 빠르게
if (pooled == "ACTIVE") { ... }

거의 안티 패턴. equals 가 가독성·안정성 면에서 우수.

시나리오 2: 메모리 절약 — 중복 많은 데이터

// 100만 개 객체가 모두 status = "ACTIVE" 또는 "INACTIVE"
public class Shipment {
    private String status;   // 두 값만 있음

    public void setStatus(String s) {
        this.status = s.intern();    // 풀로 통일 → 객체 2개로 충분
    }
}

→ 100만 개 객체가 모두 같은 2개의 String 객체 참조 → 메모리 절약.

4.3 위험 — intern 남용의 함정

// ❌ 모든 String을 intern
public void process(String userInput) {
    String s = userInput.intern();   // ← 사용자 입력을 풀에 영구 등록!
    // ...
}

문제:

  • 사용자 입력이 매번 달라 → 풀에 새 String 계속 쌓임
  • 풀은 Old Generation에 가까이 → GC 어려움
  • 결과: String Pool 메모리 누수 → OOM 가능
사용자 입력 예: "userId-12345", "session-abcd", ...
→ 모두 풀에 영구 등록 → 풀 크기 폭발

규칙:

  • 고정된 작은 집합의 값일 때만 intern (status, type, category 등)
  • 사용자 입력, 임의 동적 String은 절대 intern X
  • 의심되면 그냥 equals 사용

4.4 Java 7+의 intern 성능 개선

Java 6 이전:

  • Pool이 PermGen → GC 거의 안 됨 → intern 객체 영원
  • StringTableSize 작음 → 충돌 많음

Java 7+:

  • Pool이 Heap → GC 가능
  • 더 큰 StringTableSize 기본값

→ 그래도 사용자 입력 intern은 여전히 위험.

4.5 String Deduplication — JVM의 자동 풀링

G1 GC의 옵션:

-XX:+UseStringDeduplication

JVM이 자동으로 같은 값을 가진 String 객체들의 내부 char[]/byte[] 를 공유.

String 객체 1000개가 모두 "ACTIVE"
   ↓ Deduplication 활성화 후 GC
String 객체 1000개의 value 필드가 모두 같은 byte[] 가리킴

→ intern 안 써도 비슷한 효과. G1에서만 가능, Java 8u20+.
→ 운영에서 String 메모리 줄이고 싶으면 이 옵션이 더 안전.


5️⃣ JVM의 다른 풀들

5.1 Integer Cache

Integer a = 100;
Integer b = 100;
Integer c = 1000;
Integer d = 1000;

a == b;    // true   ← Integer Cache 사용
c == d;    // false  ← 캐시 밖

메커니즘

Integer.valueOf(int)-128 ~ 127 의 정수를 미리 만들어 캐시.

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

오토박싱(Integer a = 100)도 내부적으로 valueOf 호출.

Integer Cache (Method Area의 static):
  cache[0]   = Integer(-128)
  cache[1]   = Integer(-127)
  ...
  cache[128] = Integer(0)
  ...
  cache[255] = Integer(127)

왜 -128 ~ 127?

  • 통계적으로 가장 많이 쓰이는 범위 (인덱스, 카운터, 작은 값)
  • byte 범위(-128 ~ 127)와 일치 → 자연스러운 경계
  • 256개 객체 × ~16B = 4KB → 매우 작은 캐시

캐시 상한 조정

-XX:AutoBoxCacheMax=10000

이 옵션으로 캐시 상한을 늘릴 수 있음. (하한은 -128 고정).

Integer a = 1000;
Integer b = 1000;
a == b;    // 기본: false. AutoBoxCacheMax=10000 시: true

5.2 Long, Short, Byte, Character도 캐시

public final class Long {
    private static class LongCache {
        static final Long[] cache = new Long[256];  // -128 ~ 127
    }
}
  • Long: -128 ~ 127 (조정 불가)
  • Short: -128 ~ 127
  • Byte: 전체 범위 (-128 ~ 127, 256개 모두)
  • Character: 0 ~ 127 (ASCII)
Long l1 = 100L;
Long l2 = 100L;
l1 == l2;       // true

Long l3 = 1000L;
Long l4 = 1000L;
l3 == l4;       // false

5.3 Boolean

public final class Boolean {
    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);

    public static Boolean valueOf(boolean b) {
        return b ? TRUE : FALSE;
    }
}

→ 단 2개의 객체. 항상 캐시 사용.

Boolean a = true;
Boolean b = true;
a == b;    // true (항상)

5.4 Float / Double는 캐시 없음

Double a = 1.0;
Double b = 1.0;
a == b;    // false   ← 캐시 없음

부동소수점은 IEEE 754의 특성상 캐시가 의미 없음 (NaN, 정밀도 문제).

5.5 정리 표

타입캐시 범위비고
Integer-128 ~ 127AutoBoxCacheMax로 상한 조정
Long-128 ~ 127조정 불가
Short-128 ~ 127조정 불가
Byte전체 (-128 ~ 127)모든 값 캐시
Character0 ~ 127ASCII 범위
BooleanTRUE, FALSE단 2개
Float(캐시 없음)
Double(캐시 없음)

6️⃣ String의 진화 — Java 6/7/8/9의 변화

6.1 Java 6 이전 — String Pool은 PermGen

  • 크기 고정 → OutOfMemoryError: PermGen
  • GC 거의 안 됨 → intern 객체 누적

6.2 Java 7 — String Pool을 Heap으로

Before:  PermGen ⊃ String Pool
After:   Heap ⊃ String Pool

장점:

  • 동적 확장
  • GC 대상 가능
  • StringTableSize 기본값 증가 (1009)

6.3 Java 8 — PermGen → Metaspace

Java 7:  Heap, PermGen
Java 8+: Heap, Metaspace (네이티브 메모리)

String Pool은 그대로 Heap에 위치

6.4 Java 9 — Compact Strings (JEP 254)

기존 String 내부:

public final class String {
    private final char[] value;   // ← UTF-16, 2 bytes per char
}

ASCII 문자열도 char 1개당 2바이트 → 메모리 낭비.

Java 9+:

public final class String {
    private final byte[] value;   // ← Latin-1 또는 UTF-16
    private final byte coder;     // 0=Latin-1, 1=UTF-16
}
  • ASCII/Latin-1만 쓰는 String → 1 byte/char
  • 한글/이모지 → 기존처럼 2 bytes/char
"hello"       // 5 bytes (Latin-1)
"안녕"         // 4 bytes (UTF-16, 2 chars × 2)

→ 대부분의 영문 String 메모리 절반으로 감소.

6.5 Java 9 — String Concat의 진화 (JEP 280)

기존 + 연산:

String s = "Hello, " + name + "!";

// 컴파일러가 변환:
StringBuilder sb = new StringBuilder();
sb.append("Hello, ");
sb.append(name);
sb.append("!");
String s = sb.toString();

Java 9+:

// 컴파일러가 변환:
String s = INDY[makeConcatWithConstants("Hello, ", name, "!")];
//              ↑ invokedynamic
  • 런타임에 JVM이 최적 전략 선택 (StringBuilder, char[] 직접 조작, etc.)
  • 짧은 String 연결이 더 빠르고 메모리 효율 ↑

6.6 시각화 — 같은 코드의 진화

String s = "안녕하세요";
버전위치내부 표현메모리
Java 6PermGen Poolchar[] (2B × 5)≈ 10 + 헤더
Java 7Heap Poolchar[]동일
Java 9+Heap Poolbyte[] + coder동일 (한글)
String s = "hello";
버전위치내부 표현메모리
Java 6PermGenchar[] (2B × 5)≈ 10 + 헤더
Java 9+Heapbyte[] (1B × 5)≈ 5 + 헤더 (절반!)

→ Java 9+ ASCII 문자열은 메모리 절반.


7️⃣ ILIC 실무 — Pool 활용과 함정

7.1 자연스러운 활용 — 의식하지 않아도 됨

@Service
public class ShipmentService {

    public Shipment process(ShipmentRequest req) {
        log.info("Processing shipment: {}", req.getBlNo());
        //        ↑ 풀의 문자열 자동 활용

        validate(req);
        return repository.save(new Shipment(req));
    }
}

"Processing shipment: {}" 는 코드 안 리터럴 → 자동으로 풀에 1개.
앱 전체에서 호출 횟수에 무관하게 1개의 String 객체.

리터럴을 그대로 쓰면 항상 효율적. 의식하지 않아도 됨.

7.2 enum의 활용 — Pool 대신 enum

// ❌ String 상수
public class ShipmentStatus {
    public static final String DRAFT = "DRAFT";
    public static final String IN_TRANSIT = "IN_TRANSIT";
    public static final String DELIVERED = "DELIVERED";
}

shipment.setStatus(ShipmentStatus.DRAFT);

// 비교 시
if (shipment.getStatus().equals(ShipmentStatus.DRAFT)) { ... }
// ✅ enum
public enum ShipmentStatus {
    DRAFT, IN_TRANSIT, DELIVERED
}

shipment.setStatus(ShipmentStatus.DRAFT);

if (shipment.getStatus() == ShipmentStatus.DRAFT) { ... }
//                       ↑ == 가능. enum은 본질적으로 풀 (각 값 단 1개)

enum의 메모리 동작:

  • 각 enum 상수는 클래스 로딩 시 단 1번 생성
  • Method Areastatic 영역에 저장
  • 진정한 싱글톤 → == 비교 안전

→ String 상수 대신 enum 사용 권장. 메모리 + 안전성 + 가독성 모두 우위.

7.3 DB에서 가져온 status 처리

// ❌ intern 남용
@Entity
public class Shipment {
    private String status;

    public void setStatus(String s) {
        this.status = s.intern();   // 풀에 강제 등록 — 위험
    }
}
// ✅ enum 변환
@Entity
public class Shipment {
    @Enumerated(EnumType.STRING)
    private ShipmentStatus status;   // JPA가 자동 변환
}

→ DB의 String → Entity의 enum. 코드 어디서도 String을 비교할 일 없음.

7.4 동적 문자열 처리 — Pool과 무관

public String generateBlNo(Shipment s) {
    return "BL-" + s.getId() + "-" + LocalDate.now().getYear();
    //         ↑ 동적 결합 → 매번 새 객체 (풀 X)
}

이 결과:

  • 매번 새 String 객체 (호출 횟수만큼)
  • Pool 외부의 Heap 객체

→ 운영에서 호출량이 많으면 메모리 압박. 보통 작은 비용이라 문제 없음.

7.5 큰 문자열의 풀 등록 위험

public void log(String userInput) {
    String key = "input:" + userInput;
    cache.put(key.intern(), result);   // ❌ key를 풀에 등록
}

사용자 입력이 매번 다름 → 풀에 영원히 쌓임 → 메모리 누수.

해결:

  • intern 빼고 그냥 일반 String 사용
  • 또는 캐시에 WeakHashMap 사용해 사용자 입력 키 자동 회수

7.6 ILIC의 운영 권장 — 운영 옵션

# 1. String Deduplication (G1 GC + Java 8u20+)
java -XX:+UseG1GC -XX:+UseStringDeduplication MyApp

# 2. String Pool 크기 모니터링
java -XX:+PrintStringTableStatistics MyApp
# 종료 시 풀 사용량 출력

# 3. AutoBoxCacheMax 조정 (특정 워크로드만)
java -XX:AutoBoxCacheMax=2048 MyApp

대부분의 일반 앱은 기본값으로 충분. 메모리 분석 후 결정.


8️⃣ 흔한 실수 + 디버깅

실수 1 — == 로 String 비교

String status = getStatus();   // DB 또는 외부 입력
if (status == "ACTIVE") { ... }    // ❌ false일 가능성 높음

→ 외부에서 온 String은 풀 객체가 아닐 가능성 높음. 항상 equals.

실수 2 — Pool 객체와 new 객체 헷갈림

String a = "hello";
String b = new String("hello");

a == b;          // false  ← 자주 헷갈림
a.equals(b);     // true

new String(...)반드시 새 객체. 풀에 같은 값이 있어도 별개.

실수 3 — Integer 캐시 경계 오해

Integer a = 127;
Integer b = 127;
a == b;          // true   ← 캐시

Integer c = 128;
Integer d = 128;
c == d;          // false  ← 캐시 밖

Integer 비교는 항상 equals 또는 primitive 비교.

// 안전
int x = a.intValue();
int y = b.intValue();
x == y;          // primitive 비교 — 항상 안전

실수 4 — intern 남용으로 메모리 누수

// ❌
for (UserRequest r : requests) {
    cache.put(r.getUserId().intern(), data);
    //                      ↑ 사용자 ID를 풀에 영구 등록
}

10만 사용자 → 10만 String이 풀에 영구.
해결: intern 제거.

실수 5 — StringBuilder를 잘못 쓰면 풀 효과 없음

// 의도: 풀에 등록되길 기대?
StringBuilder sb = new StringBuilder("Shipment").append("-").append(id);
String result = sb.toString();   // 새 객체 (풀 외)

String pooled = result.intern(); // 풀 등록 가능 — 하지만 권장 X

→ StringBuilder의 결과는 풀 외. 정적 리터럴이 아니면 동적.

실수 6 — Java 9 Compact Strings 무시

String s = "한글입니다";
// Java 9+: UTF-16 (2B/char)  → 10 bytes

String t = "ASCII text";
// Java 9+: Latin-1 (1B/char) → 10 bytes

ASCII와 한글이 같은 길이라면 한글이 2배 메모리.

→ 운영 시 인코딩 고려해서 메모리 추정.

실수 7 — 풀 크기를 모니터링 안 함

운영에서 String이 의심되면:

-XX:+PrintStringTableStatistics

종료 시 출력:

SymbolTable statistics:
Number of buckets       : 64 = 1024 bytes, each 16
Number of entries       : 12345 = ...
...

StringTable statistics:
Number of buckets       : 65536 = 524288 bytes, each 8
Number of entries       : 50000 = 800000 bytes
Number of literals      : 50000 = ...

→ Entries 수와 메모리 사용량 확인. 비정상 증가 시 intern 남용 의심.

디버깅 — String 누수 추적

# 1. heap dump
jmap -dump:format=b,file=heap.hprof <PID>

# 2. Eclipse MAT 분석
# - "Histogram" → String 인스턴스 카운트
# - 비정상 많으면 → "List Objects with → outgoing references" 추적
# - 누가 들고 있는지 확인 (Map? List? 캐시?)

9️⃣ 면접 질문 + 자기 점검 + Phase 1 정리

9.1 면접 단골 질문 매핑

Q핵심 답변
String Pool은 어디에 있나?Java 7+: Heap. Java 6 이전: PermGen
String a = "abc"; String b = "abc"의 객체 수?1개. 둘 다 풀의 같은 객체 가리킴
new String("abc")의 차이?풀과 별개의 새 객체 생성
intern()의 동작?풀에 있으면 그 참조, 없으면 등록 후 참조 반환
Integer 캐시 범위?-128 ~ 127 (AutoBoxCacheMax로 상한 조정 가능)
Integer 캐시가 그 범위인 이유?자주 쓰이는 작은 값 + byte 범위와 일치
Java 9 Compact Strings?char[] → byte[] + coder. ASCII는 메모리 절반
String Deduplication?G1 GC가 같은 값의 byte[]를 공유. 자동 풀링 효과
String 비교는 == vs equals?항상 equals. 풀 객체가 아닐 수 있음
intern을 언제 쓰지 말아야?동적/사용자 입력. 메모리 누수 위험

9.2 자기 점검 체크리스트

기본 이해

  • String Pool의 위치(Heap)와 역사적 변화를 안다
  • 리터럴과 new String의 메모리 차이를 안다
  • intern()의 동작과 위험을 안다
  • Integer Cache의 범위와 동작을 안다
  • Compact Strings의 효과를 안다

실전 적용

  • String 비교에 ==을 쓰지 않는다
  • Integer 비교도 equals 또는 primitive 변환
  • enum으로 String 상수를 대체할 수 있다
  • DB의 status를 enum으로 매핑한다
  • String Deduplication 옵션을 알고 활용한다

면접 대비 — 5분 답변

  • String Pool의 위치 변화 (Java 6 → 7 → 8)
  • 리터럴 vs new String 메모리 차이
  • Integer Cache 메커니즘
  • Compact Strings (Java 9+)
  • intern 남용의 위험

🎯 핵심 요약 — 3줄 정리

1. Literal Pool = "자주 쓰이는 불변 값의 캐시"

  • String Pool은 Heap에 위치 (Java 7+)
  • 같은 리터럴은 단 1개의 객체로 공유
  • == 비교 가능 + 메모리 절약 + 빠른 접근

2. Pool은 String만 있지 않다

  • Integer Cache: -128 ~ 127
  • Long, Short, Byte, Character도 캐시
  • Boolean은 TRUE/FALSE 단 2개

3. 운영의 핵심 메시지

  • 리터럴은 적극 활용 (의식 안 해도 됨)
  • intern 남용은 메모리 누수 (사용자 입력 금지)
  • String 상수보다 enum 권장
  • Java 9+ Compact Strings로 ASCII 메모리 절반

🏁 Phase 1 졸업 — 자바 변수 ↔ 메모리 영역 매핑 완주

Phase 1에서 본 것 (Unit 1.1 ~ 1.6)

Unit 1.1 — 자바 변수의 3종류
  ↓ 지역 · 인스턴스 · 클래스 변수

Unit 1.2 — 변수별 저장 위치
  ↓ Stack · Heap · Method Area 매핑

Unit 1.3 — Method Area의 3개 존  ★
  ↓ Class Metadata · Static · Non-Static Zone

Unit 1.4 — Stack Area의 동작
  ↓ LVA · Operand Stack · Frame Data

Unit 1.5 — Heap과 객체-Metadata 연결
  ↓ Object Header · Class Pointer · VMT · invoke 4종

Unit 1.6 — Literal Pool Area
  ↓ String Pool · Integer Cache · Compact Strings

Phase 1 졸업 자가 진단

즉답할 수 있어야 할 질문:

  1. Shipment s = new Shipment("BL-001"); s.calculate(); 한 줄이 실행될 때 메모리 어디에 무엇이 생기는가?
  2. 같은 클래스 객체 100개를 만들면 메서드 바이트코드는 몇 개?
  3. 인스턴스 메서드 호출 시 객체는 어떻게 자기 클래스의 메서드를 찾는가?
  4. String a = "abc"; String b = "abc"; a == b 의 결과와 이유는?
  5. 톰캣 worker 200개가 모두 BLOCKED인 thread dump를 보면 어디부터 분석하는가?

모두 답할 수 있다면 Phase 1 졸업. Phase 2로 진입.


📚 다음으로...

Phase 2 — JVM 메서드 실행 메커니즘

Phase 1에서 메모리 구조를 봤다면, Phase 2는 그 구조 위에서 메서드 호출이 어떻게 일어나는지 한 단계 더 정밀하게 본다.

  • Unit 2.1 — 메서드 호출의 2단계 처리
  • Unit 2.2 — Static vs 인스턴스 메서드 호출 경로
  • Unit 2.3 — 인스턴스 메서드 호출의 전 과정 (Case Study)
  • Unit 2.4 — new 연산자가 실제로 하는 일

그 다음 Phase 3 — 바이트코드와 상수 풀 ★ 2주차의 정점

javap -c -v Shipment.class

위 명령으로 출력되는 바이트코드를 눈으로 읽고 해석할 수 있게 되는 것이 Phase 3의 목표.

  • 상수 풀(Constant Pool)의 정확한 구조
  • 심볼 참조 → 실제 참조 해소(Resolution)
  • 바이트코드 명령어 카테고리 전체 정리
  • invokevirtual #N#N 이 가리키는 것

→ Unit 1.5에서 본 invoke 명령들이 Phase 3에서 상수 풀과 결합해 완전한 그림으로 완성된다.

2주차 전체 진행 상황

Phase 1 — 자바 변수 ↔ 메모리 매핑
  ✅ Unit 1.1 자바 변수의 3종류
  ✅ Unit 1.2 변수별 저장 위치
  ✅ Unit 1.3 Method Area의 3개 존 ★
  ✅ Unit 1.4 Stack Area의 동작
  ✅ Unit 1.5 Heap과 객체-Metadata 연결
  ✅ Unit 1.6 Literal Pool Area              ← 여기

Phase 2 — JVM 메서드 실행 메커니즘
  ⏭ Unit 2.1 메서드 호출의 2단계 처리
  ⏭ Unit 2.2 Static vs 인스턴스 호출 경로
  ⏭ Unit 2.3 인스턴스 메서드 호출의 전 과정
  ⏭ Unit 2.4 new 연산자의 실제 동작

Phase 3 — 바이트코드와 상수 풀 ★ 정점
  ⏭ Unit 3.1 바이트코드란 무엇인가
  ⏭ Unit 3.2 상수 풀의 생성과 구조
  ⏭ Unit 3.3 심볼 참조
  ⏭ Unit 3.4 바이트코드 실전 분석

Phase 4 — G1 GC 심화
Phase 5 — 컬렉션 내부 구조
Phase 6 — Reflection & Iterator
Phase 7 — Buffer
profile
Software Developer

0개의 댓글