[5주차] JVM & GC

goodstar·2024년 11월 25일
0

Java 스터디

목록 보기
12/14
post-thumbnail

1. JVM 구조에 대해서 설명해 주세요.

JVM은 자바 애플리케이션 실행을 위한 가상 머신으로, 클래스 로더(Class Loader), 실행 엔진(Execution Engine), 메모리 영역(Memory Area), JNI(Java Native Interface)로 구성됩니다.

  1. 클래스 로더(Class Loader):
    • .class 파일을 읽어 메모리에 로드.
    • 검증, 준비, 해석 후 초기화 단계를 거침.
  2. 메모리 영역(Memory Area):
    • 실행 중 필요한 데이터와 객체를 저장하고 관리.
    • Method Area, Heap, Stack, PC Register, Native Method Stack 으로 이루어짐.
  3. 실행 엔진(Execution Engine):
    • 바이트코드를 해석하거나(JIT 컴파일) 기계어로 변환해 실행.
  4. JNI: 외부 네이티브 코드와 상호작용.

Java 실행과정
Java 소스 파일 작성 (.java) -> Java 컴파일러 실행(.class 변환) -> JVM이 실행되며, 클래스 로더(Class Loader)가 클래스 로딩 -> JVM 실행 엔진 동작

JVM 동작 방식 요약

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, JVM!");
    }
}
  1. 클래스 로딩

    • 클래스 로더가 HelloWorld.class메서드 영역(Method Area)에 로드.
    • 클래스 이름, 메서드 정보, 정적 변수 등이 저장됩니다.
  2. 메서드 호출과 스택 메모리 활용

    • main() 메서드가 호출되며 스택(Stack)에 메서드 호출 기록(Frame)이 생성됩니다.
    • args 배열은 Heap에 생성되고, 메서드 내 지역 변수로 스택에 참조가 저장됩니다.
  3. Heap 메모리와 객체 생성

    • "Hello, JVM!" 문자열은 String Pool(Heap 내부의 Constant Pool)에 저장됩니다.
    • System.out 객체는 이미 JVM 실행 시 Heap에 생성되어 있으며 이를 참조합니다.
  4. PC 레지스터와 명령어 실행

    • PC 레지스터가 main() 메서드의 현재 실행 중인 명령어를 추적하며 System.out.println() 호출.
  5. 네이티브 메서드 스택 활용

    • System.out.println()은 내부적으로 네이티브 코드(JNI)를 호출하며, 네이티브 메서드 스택에서 실행됩니다.
  6. Garbage Collector와 종료

    • main() 메서드가 종료되면 스택 메모리의 Frame이 제거되고, 참조되지 않는 객체는 Garbage Collector에 의해 회수됩니다.

2. 클래스 로더란?

클래스 로더(Class Loader)는 JVM이 .class 파일을 읽어 메모리에 로드하고, 실행 준비를 하는 역할을 합니다.

클래스 로딩 과정

  1. 로드(Loading):

    • .class 파일을 읽어 메서드 영역(Method Area)에 클래스 정보를 저장합니다.
  2. 링크(Linking):

    • 검증(Verification): 클래스 파일이 JVM 명세에 맞는지 확인.
    • 준비(Preparation): 정적 변수에 기본값 할당 및 메모리 구조 생성.
    • 해결(Resolution): 심볼릭 참조를 실제 메모리 참조로 변환.
  3. 초기화(Initialization):

    • 정적 변수와 정적 블록을 정의된 값으로 초기화하며, 클래스가 실행 준비를 마칩니다.

클래스 로더의 종류

  1. Bootstrap Class Loader:

    • JVM 내장 클래스 로더로, JAVA_HOME/lib에 있는 기본 Java 클래스(예: rt.jar)를 로드.
  2. Extension Class Loader:

    • JAVA_HOME/lib/ext의 확장 클래스(JAR 파일 포함)를 로드.
  3. Application Class Loader:

    • classpath에 지정된 사용자 정의 클래스와 애플리케이션 코드를 로드.

특징

  • 계층적 구조: 상위 클래스 로더가 먼저 클래스를 로드하는 부모 위임 모델 사용.
  • 동적 로딩: 필요 시 클래스를 동적으로 로드.
  • 사용자 정의: ClassLoader를 상속해 네트워크, 암호화된 파일 등 다양한 소스로 클래스 로딩 가능.

3. JVM 메모리 구조를 자세히 설명해 주세요.

JVM 메모리는 Method, Heap, Stack, PC Register, Native Method Stack으로 나뉩니다.

1) Method Area (메서드 영역)

  • 내용:
    • 클래스와 인터페이스의 메타데이터, 즉 클래스 이름, 부모 클래스 정보, 메서드 및 필드 정보, 접근 제어자 등이 저장됩니다.
    • 상수 풀(Constant Pool): 컴파일 타임에 생성된 상수 값(문자열 리터럴, 심볼릭 참조 등)을 저장하여 런타임 중 재사용됩니다.
    • 정적 변수(static 변수)와 메서드 코드(바이트코드)도 이곳에 저장됩니다.
  • 특징:
    • JVM 시작 시 초기화되고 모든 스레드가 공유합니다.
    • 메모리 부족 시 OutOfMemoryError: Metaspace 발생.
  • 사용 예:
    • 클래스 로더가 클래스를 로드하면 메서드 영역에 해당 클래스의 정보를 저장합니다.
    • String 리터럴이 상수 풀에 저장되어 동일한 리터럴을 참조할 때 메모리를 절약.

2) Heap (힙)

  • 내용:

    • 애플리케이션 실행 중 생성되는 모든 객체와 배열이 저장됩니다.
    • 객체의 인스턴스 변수(필드)도 힙에 저장됩니다.
    • JVM의 Garbage Collector(GC)가 이 영역을 관리합니다.
  • 세분화:

    • Young Generation: 새롭게 생성된 객체가 위치하며, 다시 Eden, Survivor로 나뉩니다.
    • Old Generation: Young Generation에서 살아남아 더 오래된 객체가 이동되는 공간입니다.
  • 특징:

    • 모든 스레드가 공유합니다.
    • 메모리 부족 시 OutOfMemoryError: Java Heap Space 발생.
  • 사용 예:

    • new 키워드를 사용해 생성된 객체는 힙에 저장됩니다.
    • GC가 더 이상 참조되지 않는 객체를 청소해 메모리를 회수합니다.

3) Stack (스택)

  • 내용:
    • 각 스레드마다 독립적으로 할당되며, 메서드 호출 기록(Frame)을 관리합니다.
    • Frame에는 메서드 호출과 관련된 지역 변수, 매개변수, 연산 결과 등이 저장됩니다.
  • 동작:
    • 메서드가 호출될 때마다 Stack Frame이 생성되고, 메서드가 종료되면 해당 Frame이 제거됩니다.
  • 특징:
    • LIFO(Last In, First Out) 구조.
    • 메모리 부족 시 StackOverflowError 발생.
  • 사용 예:
    • 지역 변수: 메서드 내에서 선언된 변수(예: int x)는 Stack에 저장됩니다.
    • 재귀 호출: 반복적으로 Frame이 쌓이며 StackOverflowError 발생 가능.

4) PC Register (프로그램 카운터 레지스터)

  • 내용:
    • 각 스레드마다 존재하며, 현재 실행 중인 명령어의 주소를 저장합니다.
    • JVM 명령어를 실행할 때, 다음 명령어로 이동하는 데 사용됩니다.
  • 특징:
    • 스레드별로 독립적입니다.
    • Java 바이트코드를 기반으로 동작하며, 네이티브 메서드 실행 중에는 비워질 수 있습니다.
  • 사용 예:
    • 멀티스레드 환경에서 각 스레드의 실행 위치를 추적합니다.
    • 스레드 컨텍스트 스위칭 시 PC Register의 값이 변경됩니다.

예제 코드

public class Example {
    public static void main(String[] args) {
        int x = 10;
        int y = 20;
        int sum = x + y;
        System.out.println(sum);
    }
}

컴파일 후 바이트코드 (javap -c Example)

public static void main(java.lang.String[]);
   0: bipush        10          // 명령어: 10을 스택에 푸시
   2: istore_1                   // 명령어: 스택에서 10을 꺼내어 x에 저장
   3: bipush        20          // 명령어: 20을 스택에 푸시
   5: istore_2                   // 명령어: 스택에서 20을 꺼내어 y에 저장
   6: iload_1                    // 명령어: 변수 x의 값을 스택에 푸시
   7: iload_2                    // 명령어: 변수 y의 값을 스택에 푸시
   8: iadd                      // 명령어: 스택에서 두 값을 꺼내어 더함
   9: istore_3                   // 명령어: 결과를 sum에 저장
  10: getstatic     #2          // 명령어: System.out을 스택에 푸시
  13: iload_3                    // 명령어: sum의 값을 스택에 푸시
  14: invokevirtual #3          // 명령어: println 호출
  17: return                     // 명령어: main() 종료

PC 레지스터의 역할

  • PC 레지스터는 현재 실행 중인 명령어의 주소를 추적합니다.
  • 위 바이트코드 실행 과정에서, PC 레지스터는 다음과 같이 업데이트됩니다:
    1. bipush 10 실행 전: PC 레지스터는 0을 가리킴.
    2. bipush 10 실행 후: PC 레지스터가 2로 업데이트.
    3. istore_1 실행 후: PC 레지스터가 3으로 이동.
    4. 이러한 과정을 반복하여, 명령어 실행 위치를 지속적으로 추적합니다.

5) Native Method Stack (네이티브 메서드 스택)

  • 내용:
    • Java 코드가 아닌 네이티브 코드(예: C/C++) 실행과 관련된 데이터와 상태를 저장합니다.
    • JNI(Java Native Interface)를 통해 호출된 네이티브 메서드가 사용하는 공간입니다.
  • 특징:
    • 스레드별로 독립적입니다.
    • 메모리 부족 시 StackOverflowError 또는 OutOfMemoryError 발생 가능.
  • 사용 예:
    • Java가 네이티브 라이브러리(예: OpenGL, 시스템 호출)를 호출할 때 사용됩니다.
    • 예: System.loadLibrary() 호출 시 해당 네이티브 코드는 이 스택에서 실행됩니다.

4. GC란 무엇인가요?

GC(Garbage Collection)는 JVM(Java Virtual Machine)의 Heap 영역에서 더 이상 사용되지 않는 객체를 자동으로 정리(회수)하는 메모리 관리 기법입니다. GC 덕분에 Java 개발자는 메모리를 직접 해제할 필요 없이 객체를 생성하고 사용할 수 있습니다.

GC의 주요 개념

  1. Heap 영역의 구조

    • Heap은 JVM의 메모리 공간 중 객체를 저장하는 영역입니다. 이 공간은 크게 두 부분으로 나뉩니다:
      • Young Generation:
        • 새로 생성된 객체가 저장.
        • 짧은 생명 주기를 가진 객체를 저장하며, 대부분의 객체는 여기서 생성 후 빠르게 소멸.
        • Young Generation에서 수행되는 GC를 Minor GC라고 부릅니다.
      • Old Generation:
        • Young Generation에서 살아남은 오래된 객체가 이동.
        • 긴 생명 주기를 가진 객체를 저장하며, Old Generation에서 수행되는 GC를 Major GC 또는 Full GC라고 부릅니다.
  2. GC의 동작

    • Mark and Sweep Algorithm: GC는 객체의 참조 여부를 확인하여 사용되지 않는 객체를 식별(마킹)한 후, 이를 회수(스위핑)하여 메모리를 확보합니다.
      1. Mark 단계: 참조가 있는 객체와 없는 객체를 식별.
      2. Sweep 단계: 참조가 없는 객체를 Heap 메모리에서 제거.

5. GC의 장단점을 설명해 주세요.

GC는 메모리 관리를 자동화하지만, 성능 저하를 유발할 수 있습니다.

GC의 장단점

장점

  1. 자동으로 메모리 관리:

    • 개발자가 일일이 메모리를 해제하지 않아도 됨.
    • 코드 작성이 더 간단하고 실수를 줄일 수 있음.
  2. 메모리 누수 방지:

    • 사용하지 않는 객체를 자동으로 정리해 메모리가 부족해지는 문제를 예방.
  3. 플랫폼 독립적:

    • JVM이 메모리를 관리하므로 운영체제에 상관없이 동일한 방식으로 동작.

단점

  1. 성능 저하 가능성:

    • GC는 CPU를 사용해 메모리를 정리하기 때문에 프로그램 실행 속도가 느려질 수 있음.
    • 특히, Stop-the-World라는 GC 실행 중 프로그램이 멈추는 현상이 발생할 수 있음.
  2. 예상치 못한 타이밍에 실행:

    • GC 실행 시점은 JVM이 결정하므로, 중요한 작업 도중 실행되면 지연이 생길 수 있음.
  3. 설정이 어려울 수 있음:

    • 적합한 GC 알고리즘을 선택하거나 메모리 크기를 조정해야 하는 경우, 경험과 분석이 필요.

Stop-the-World는 GC 실행 중 JVM이 모든 애플리케이션 스레드를 일시 정지시키는 현상입니다.
GC는 객체의 참조를 추적하고, 사용되지 않는 객체를 안전하게 삭제해야 합니다.
객체 참조가 변경될 수 있는 환경에서는 메모리 관리를 신뢰할 수 없으므로, GC 수행 중에는 애플리케이션의 모든 작업을 멈춥니다. (메모리 안정성 보장)


6. GC를 모니터링해야 하는 이유가 무엇일까요?

GC의 성능 문제나 메모리 부족 문제를 사전에 파악하기 위해 모니터링이 필요합니다.

  1. GC 실행 빈도가 지나치게 높으면 성능 저하 발생 가능.
  2. 메모리 누수를 조기에 감지 가능.
  3. GC 힙 사용량 분석으로 메모리 문제를 해결.
java -XX:+PrintGCDetails MyApplication

GC 모니터링

  1. GC 로깅 활성화
    • JVM 옵션 추가: -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xlog:gc*:file=gc.log
  2. 모니터링 도구 사용
    • VisualVM, JConsole: 실시간 메모리 사용 및 GC 활동 확인.
    • Eclipse MAT: 힙 덤프 분석.
    • APM 도구: New Relic, Dynatrace로 GC 시각화 및 알림 설정.
  3. 코드로 GC 정보 확인
    • JMX를 활용해 GC 이름, 실행 횟수, 실행 시간 확인:
      ManagementFactory.getGarbageCollectorMXBeans();
  4. GC 로그 분석 도구
    • GCViewer, Garbage Cat로 GC 로그를 시각화 및 분석.
  5. 문제 해결 및 튜닝
    • High Pause Time: G1 GC, ZGC 사용.
    • Frequent GC: 힙 크기 조정, 객체 수명 최적화.

7. Out of Memory 에러 발생 시 대처 방법

Out of Memory 에러는 힙 크기 조정, 메모리 누수 점검, GC 정책 변경으로 해결 가능합니다.

1) 힙 크기 조정

조정 방법

JVM 실행 시 -Xms(초기 힙 크기)와 -Xmx(최대 힙 크기) 옵션을 설정합니다.

  • 기본값이 부족한 경우 점진적으로 증가시키며 테스트합니다.
  • 적정 크기 결정:
    • 애플리케이션이 사용하는 객체 크기와 요청량을 고려.
    • 실행 중인 서버의 메모리 여유 공간 확인.

예시

java -Xms512m -Xmx2048m MyApplication
  • 초기 힙 크기: 512MB
  • 최대 힙 크기: 2048MB

확인 방법

  • GC 로그 분석: -Xlog:gc* 또는 -verbose:gc로 메모리 사용량 확인.
  • 모니터링 도구: VisualVM이나 JConsole에서 힙 사용량을 확인하며 적정 크기 결정.

2) 메모리 누수 탐지

탐지 방법

메모리 누수는 더 이상 참조되지 않는 객체가 해제되지 않아 메모리가 부족해지는 현상입니다. 이를 탐지하려면 도구를 사용해 힙 덤프를 분석합니다.

  1. 힙 덤프 생성
    JVM 옵션으로 애플리케이션 충돌 시 힙 덤프를 자동으로 생성합니다:

    -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/dump

    힙 덤프란?

    JVM의 Heap 메모리 상태를 스냅샷으로 저장한 파일.
    객체 인스턴스와 참조 관계를 확인할 수 있어 메모리 누수 분석에 유용.

  2. Eclipse MAT로 분석

    • 힙 덤프 파일 열기.
    • Dominators 분석: 메모리를 가장 많이 차지하는 객체를 탐색.
    • Leak Suspects 보고서 확인: 누수 의심 객체를 자동으로 식별.
  3. VisualVM 사용

    • 실행 중인 애플리케이션의 힙 덤프를 수집하고 분석.
    • 메모리 누수를 일으키는 객체(예: 캐시에 남아있는 객체)를 탐지.

메모리 누수 해결 방법

  1. 코드 점검 및 수정:

    • 참조가 유지되는 컬렉션(List, Map)의 객체를 명시적으로 제거.
    • 캐시에 WeakHashMap 사용하여 참조가 끊어진 객체를 자동으로 GC.
  2. 객체 수명 관리:

    • 객체의 생명 주기를 명확히 정의하고, 필요하지 않은 참조를 null로 설정.

      객체의 생명주기란 객체가 생성되어 사용되고 더 이상 필요 없어질 때까지의 전체 과정을 의미합니다.
      생성(메모리 할당) -> 사용 -> 비활성화 -> 소멸(GC가 처리)

  3. GC 알고리즘 조정:

    • 많은 메모리를 사용하는 애플리케이션에서는 G1GC 또는 ZGC를 사용.

3) GC 정책 변경

GC 정책이란?

JVM이 메모리를 관리하는 방식으로, 애플리케이션 성격에 맞게 선택해야 합니다.

  1. 기본 정책

    • JDK 8 이하: Parallel GC
    • JDK 9 이상: G1GC
  2. G1GC로 전환

    • 많은 메모리 할당/해제 작업으로 Full GC가 자주 발생한다면 G1GC 사용.
    -XX:+UseG1GC
  3. 다른 GC 정책

    • CMS(Concurrent Mark-Sweep): 저지연 애플리케이션에 적합.
      -XX:+UseConcMarkSweepGC
    • ZGC: JDK 11 이상에서 사용 가능, 대용량 메모리 애플리케이션에 적합.
      -XX:+UseZGC
  4. GC 정책 선택 기준

    • 지연 시간(Latency) 우선: G1GC 또는 ZGC.
    • 처리량(Throughput) 우선: Parallel GC.

GC 로그로 분석 후 조정

  • GC 로그에서 Minor GC, Major GC 빈도 및 시간이 높은 경우 G1GC로 변경.
  • 힙 메모리 압박이 높은 경우, ZGC 도입 검토.

G1GC 주요 특징

  1. Region 기반 메모리 관리
    • 힙을 작은 Region으로 나누어 필요한 부분만 GC 수행.
  2. Pause Time 목표 설정
    • -XX:MaxGCPauseMillis=<N> 옵션으로 GC 지연 시간을 제어.
  3. Concurrent Marking
    • Old Generation 객체를 비차단 방식으로 추적, Full GC 빈도 감소.
  4. Garbage-First 알고리즘
    • 가비지가 많은 Region부터 우선 청소하여 효율 극대화.
  5. Full GC 완화
    • Mixed GC로 대부분의 Full GC를 대체, STW 시간을 최소화.
      이 특징 덕분에 많은 메모리 할당/해제 작업낮은 지연 시간 요구 환경에서 G1GC가 효과적입니다.

0개의 댓글

관련 채용 정보