원티드 [프리온보딩 챌린지 BE]

둥냥둥냥둥·2023년 6월 18일
0
post-thumbnail

JVM과 메모리 구조

JVM


JVM(Java Virtual Machine)는 자바 가상 머신

  • 논리적인 개념, 여러 모듈의 결합체
  • Java 앱을 실행하는 주체
  • JVM 때문에 다양한 플랫폼 위에서 동작 가능
  • 대표적인 역할, 기능
    • 클래스 로딩
    • GC 등 메모리 관리
    • 스레드 관리
    • 예외 처리

JVM Architecture

  • Class Loaders
    • 바이트코드 로딩, 검증, 링킹 등 수행

  • Runtime Data Areas
    • 앱 실행을 위해 사용되는 JVM 메모리 영역

  • Execution Engine
    • 메모리 영역에 있는 데이터를 가져와 해당하는 작업 수행

  • JNI (Java Native Interface)
    • JVM과 네이티브 라이브러리 간 이진 호환성을 위한 인터페이스
    • 네이티브 메서드(네이티브 언어 C/C++ 등으로 작성) 호출, 데이
      터 전달과 메모리 관리 등 수행

  • Native Libraries
    • 네이티브 메서드의 구현체를 포함한 플랫폼별 라이브러리

JVM Run-Time Data Areas

  • The pc Register
    • 스레드 별로 생성되며 실행 중인 명령(오프셋)을 저장하는 영역

  • Java Virtual Machine Stacks (Stack Area, Java Stack)
    • 스레드 별로 생성되며 메서드 실행 관련 정보를 저장하는 영역 (프레임 저장)

  • Heap
    • JVM 실행 시 생성되며 모든 객체 인스턴스/배열에 대한 메모리가 할당되는 영역

  • Method Area
    • JVM 실행 시 생성되며 클래스의 구조나 정보를 저장하는 영역

  • Native Method Stacks
    • 스레드 별로 생성되며 네이티브 코드 실행에 관련 정보를 저장하는 영역


JVM Run-Time Data Areas - The pc Register

  • 스레드 생성 시 생성/할당되며 현재 실행 중인 명령의 주소를 저장하는 영역
    • 레지스터는 프로세서 내에서 자료를 보관하는 빠른 기억 장치
    • 저장되는 명령의 주소는 Java 바이트 스트림(바이트코드) 안에 오프셋을 의미
      • 바이트코드 명령 자체(opcode)는 메서드 영역에 저장

  • 각 스레드가 메서드를 실행할 때 실행 메서드에 따라 저장 여부가 결정됨
    • Java 메서드 -> 실행 명령의 주소를 저장 네이티브 메서드-> 저장되지 않음

JVM Run-Time Data Areas - Java Virtual Machine Stacks

  • 스레드 생성 시 생성/할당되며 프레임(Frame)이 저장되는 영역 (Stack Area, Java Stack)
    • JVM의 구현 방식에 따라 크기와 프레임 관리 방법 등이 다를 수 있음

  • 로컬 변수 저장과 메서드 호출/반환 등과 같은 작업 시 사용

  • 스택은 push/pop을 제외하고 직접 조작되지 않기 때문에 프레임은 힙에 할당될 수 있음
    • 메모리는 순차적일 필요가 없음

  • 허용된 것 보다 더 큰 스택이 필요한 경우 StackOverFlowError 발생

  • 스택을 생성하거나 동적으로 크기를 확장할 때 메모리가 부족하면 OutOfMemoryError 발생

Frame

  • JVM stack에 생성(push)되는 메서드 관련 정보 저장 단위 (Stack Frame, Activation Record)
    메서드가 호출될 때 생성(push)되며 종료되면 소멸(pop)됨
  • 메서드의 데이터(매개 변수와 지역 변수 등)와 부분 결과 등을 저장함
    동적 연결, 메서드 값 반환, 예외 전달 등에 사용됨
  • 각 프레임에는 런타임에 필요한 메서드 참조를 위해 런타임 상수 풀 참조를 포함

구성

  • 지역변수 배열 (Local Variables) 매개 변수와 지역 변수 저장
  • 오퍼랜드 스택 (Operand Stacks) 실행 중간 연산 결과 등을 임시로 저장
  • 프레임 데이터 (Frame Data) 반환 주소 등 기타 데이터 저장

Frame - Local Variables

  • 이 지역 변수 배열의 길이는 컴파일 타임에 결정되며 관련 메서드, 클래스 등의 정보와 함께 바이트코드로 제공됨
  • 지역 변수는 원시 타입과 reference, returnAddress이 한 곳(slot)에 저장되며 특히 8바이트인 long, double 타입은 연속된 두 곳(slot)에 저장됨
  • 지역 변수 배열의 첫번째 인덱스는 0이며 0부터 array.length-1 까지 유효한 인덱스 범위로 간주
  • JVM에 의해 메서드 호출 시 매개 변수(또는 Args)는 지역 변수 배열에 담겨 전달
  • 인스턴스 메서드 호출 시에 지역 변수 0은 인스턴스 메서드 객체 참조인 this를 전달하는데 사용 그 이후 1부터 모든 매개변수들이 표현됨

Frame - Operand Stacks

  • 일반적인 LIFO 방식의 스택으로 최대 길이는 컴파일 타임에 결정되며 관련된 메서드 코드와 함께 제공되며 최초 생성 시 오퍼랜드 스택은 비어있음
    • 문맥에 따라 다를 수 있지만 일반적으로 오퍼랜드 스택은 프레임의 오퍼랜드 스택 지칭
  • 지역 변수 배열과 다르게 인덱스가 아닌 push/pop을 할 수 있는 명령어에 의해 액세스 가능
  • 저장되는 오퍼랜드의 타입은 JVM에서 제공하는 모든 타입의 값
  • JVM은 지역 변수, 필드의 값을 오퍼랜드 스택으로 로딩하는 명령 제공하며
    다른 명령들을 통해 오퍼랜드 스택에서 값을 가져와 연산, 결과를 다시 저장함
    • 오퍼랜드 스택은 호출할 메서드에 전달할 매개 변수 전달과 결과를 수신할 때도 사용됨
  • 소수의 JVM 명령은 타입에 관계 없이 원시 타입의 값으로 런타임 데이터 영역에서 작동함

Frame - Frame Data

  • 연관된 메서드들의 심볼릭 레퍼런스와 메서드 반환에 필요한 데이터 저장
  • 예외가 발생한 경우 catch 블록 정보를 제공하는 Exception 테이블 참조 포함

JVM 메모리 구조

JVM Run-Time Data Areas - Method Area

  • JVM 실행 시 생성되어 모든 스레드에게 공유되며 클래스 별 구조와 정보를 저장하는 영역
    • 클래스/인터페이스/인스턴스 초기화에 사용되는 스페셜 메서드
    • 런타임 상수 풀, 필드/메서드 데이터
    • 생성자/메서드 코드
  • Hotspot VM 기준으로 Metaspace(PermGen) 영역에 관리됨 (JVM 구현마다 다름)
  • 컴파일된 코드를 저장하는 영역 또는 OS 프로세스의 text 세그먼트와 유사
  • 메서드 영역의 크기는 상황에 따라 고정/확장/축소될 수 있음
  • 메모리 할당 요청을 충족할 수 없으면 OutOfMemoryError 발생
  • 메서드 영역은 논리적으로 힙의 일부, 간단한 구현의 경우 GC/압축/컴팩트를 선택하지 않을 수 있음
    • 메서드 영역의 위치, 컴파일된 코드 관리에 대한 정책을 요구하지 않음

JVM Run-Time Data Areas - Heap

  • 모든 객체 인스턴스, 배열에 대한 메모리가 할당되는 데이터 영역
  • JVM 실행 시 생성되며 모든 JVM 스레드에게 공유되는 영역
  • GC가 처리되는 영역
  • 특정 스토리지 시스템에 종속적이지 않은 구조
  • 계산된 것보다 더 많은 힙 메모리가 필요한 경우 OutOfMemoryError 발생
  • 힙의 크기는 상황에 따라 고정/확장/축소될 수 있음
    • 힙 메모리는 순차적일 필요가 없음

Run-Time Constant Pool

  • 클래스/인터페이스가 로딩될 때 메서드 영역(Method Area)에 할당되는 자료구조
    • 컴파일 시 .class 파일에 생성되는 일반 상수 풀의 런타임 표현

  • 일반 상수 풀의 데이터를 기반으로 생성되며 스태틱 상수와 심볼릭 레퍼런스 등을 포함
    • 상수 뿐 아니라 메서드, 필드 참조까지 여러 종류의 상수가 포함됨

  • 일반 프로그래밍 언어의 심볼 테이블과 유사하지만 그보다 더 넓은 범위에 데이터를 포함함
    • 심볼 테이블은 컴파일러/인터프리터가 프로그램을 분석/처리 시 사용하는 자료구조이며 코드의 식별자/상수/프로시저/함수 등과 관련된 정보를 저장함

  • Method Area 영역에서 허용 가능한 메모리를 초과하면 OutOfMemoryError 발생

Constant Pool

  • 상수 풀 (Constant Pool)
    • Java 바이트코드에 포함되어 있는 모든 상수 값을 저장하는 심볼(룩업) 테이블
      .java 파일이 Java 컴파일러에 의해 컴파일 되어 Java 바이트코드로 변환될 때 생성
    • 클래스명, 필드명, String/Primitive type 리터럴, 심볼릭 레퍼런스 등이 저장됨
    • 컴파일 타임 시점에 알 수 있는 정보들이 저장됨

  • 상수 풀에서 런타임 상수 풀에 새로 생성(이동)되는 데이터
    • 심볼릭 레퍼런스나 String.intern 등 런타임에 달라질 수 있는 데이터
    • 컴파일 타임에 확정되어 런타임에 변경되지 않는 데이터(리터럴 값 등)들을 제외한 데이터

Symbolic Reference

  • Java 바이트코드에서 클래스/인터페이스/필드 등 참조하는 다른 요소를 표현 방식
  • JVM 구현에 따라 심볼릭 레퍼런스가 Lazy가 아닌 Eager로 확인/연결될 수 있음
    • 이때 발생하는 오류는 직간접적으로 참조(사용)하는 지점에서 발생해야 함
  • 클래스가 로딩 후 링킹(Resolution)되는 시점에서 심볼릭 레퍼런스가 실제 주소값으로 대체 됨

  • 일반적인 예시 (간접적인 표현)
    • java.lang.String 클래스의 private final byte[] value 필드
    • Ljava/lang/String;.value:[C
    • L -> 참조 타입(클래스/인터페이스)
    • ; -> 클래스와 필드의 구분자 (일반적으로 생략함)
    • .value -> 필드명
    • : -> 참조
    • [ -> 배열 표현 (2차원이라면 [[)
    • C -> char 타입

  • java.lang.String 클래스의 public char charAt(int index) 메서드
    • Ljava/lang/String;.charAt:(I)C
      • (I) -> 메서드의 파라미터 타입
      • C -> 메서드의 반환 타입

JVM Run-Time Data Areas - Native Method Stacks

  • 스레드 생성 시 생성/할당되며 네이티브 메서드 데이터가 저장되는 영역
    • 네이티브 메서드는 Java 외에 언어로 작성된 메서드 (일반적으로 C/C++)
    • 네이티브 로딩 상태나 사용 여부에 따라 JVM이 해당 영역을 제공하지 않을 수 있음
    • C와 같은 언어로 구성된 JVM 명령셋을 위한 인터프리터 구현에 사용될 수 있음

  • 네이티브 메서드 스택의 크기는 상황에 따라 고정되거나 확장, 축소될 수 있음
    • 네이티브 메서드 스택 크기가 고정되어 있다면 생성될 때 크기를 독립적으로 선택할 수 있음

  • 허용된 것 보다 더 큰 스택이 필요한 경우 StackOverFlowError 발생
  • 동적 확장 시도 중에 사용 가능한 메모리가 부족하면 OutOfMemoryError 발생

JVM Run-Time Data Areas - 생성 시점 정리

  • JVM 실행 시
    • Heap
    • Method Area

  • 스레드 실행 시
    • pc register
    • Java Virtual Machine Stacks
    • Native Method Stacks (필요한 경우)

  • 클래스/인터페이스 생성 시
    • Run-Time constant Pool (Method Area에 저장)

  • 메서드 호출 시
    • Frame (Java Virtual Machine Stacks에 저장)

Special Methods

  • Instance Initialization Methods (인스턴스 초기화 메서드)
    • Constructor, Instance Initializer Blocks (바이트 수준에선 동일)
    • 조건
      - 클래스 안에 정의
      - 이라는 이름을 가진 스페셜 메서드
      - 반환 타입은 void

  • Class Initialization Methods (클래스 초기화 메서드)
    • static Initializer Blocks
    • 조건
      - 이라는 이름을 가진 스페셜 메서드
      - 반환 타입은 void
      - 클래스 파일의 버전이 51.0 이상인 경우 해당 인자를 사용하지 않고 ACC_STATIC 플래그가 설정된 메서드

  • Signature Polymorphic Methods
    • 런타임에 메서드 시그니처가 결정되는 메서드
    • 대표적인 예시로 JDK 7부터 도입된 java.lang.invoke.MethodHandle 클래스의 invoke, invokeExact 메서드
    • 조건
      • java.lang.invoke.MethodHandle 또는 java.lang.invoke.VarHandle 클래스에서 선언
      • Object[] 타입의 단일 공식 파라미터
      • ACC_VARARGS, ACC_NATIVE 플래그 설정

JVM Run-Time Data Areas


Memory Type

  • Heap memory
    • JVM 힙 영역, 모든 객체를 저장 메모리 설정 가능(java -Xms4096M -Xmx6144M com.example.Class) 부족한 경우 OutOfMemoryError 발생

  • Stack memory
    • JVM stacks(지역 변수나 메서드 정보 등) 데이터 저장 (GC 영역 아님) JVM이 관리하는 영역이며 JVM에 의해 고정된 크기를 가짐 부족한 경우 StackOverflowError 발생

  • Native memory
    • 힙 외부에 할당되는 영역으로 오프힙(off-heap) 메모리라고도 표현함 (GC 영역 아님) 외부 영역에 존재하기 때문에 데이터 입출력 시 직렬화 수행이 필요하고 성능은 버퍼, 직렬화 프로세스, 디스크 공간 등 환경에 따라 달라짐 JVM stacks, 내부 자료 구조, 메모리 매핑 파일 등 저장

  • Direct memory (Direct Buffer Memory)
    • 힙 외부에 할당되지만 JVM 프로세스에 의해 사용되는 네이티브 메모리 영역 (GC 영역 아님)
      대표적으로 Java NIO에서 사용되는 영역(JNI 등)
      -XX:MaxDirectMemorySize=1024M 처럼 메모리 설정 가능

Stack Memory & Heap Memory

class Person {
    int id;
    String name;

    public Person(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

public class PersonBuilder {
    private static Person buildPerson(int id, String name) {
        return new Person(id, name);
    }

    public static void main(String[] args) {
        int id = 23;
        String name = "John";
        Person person = null;
        person = buildPerson(id, name);
    }
}

  1. main 메서드 호출 시 이를 위한 스택 메모리가 할당되며 프레임(main)이 생성
    • id 변수의 int 값은 직접 저장됨
    • name 참조 변수는 힙의 스트링 풀을 참조
    • person 변수 생성 후 null 참조
  2. main 메서드가 buildPerson 메서드를 호출하면서 프레임(buildPerson)이 생성됨
    • 파라미터로 넘어온 id의 값과 name의 참조가 저장됨
  3. 연이어 Person 클래스의 인스턴스 생성을 위해 생성자가 호출되며 프레임(Person)이 생성됨
    • 파라미터로 넘어온 id의 값과 name의 참조 그리고 자신을 가리키는 this가 저장됨
    • 생성되는 Person 객체는 Heap에 저장
  4. Person 인스턴스가 생성된 후 Person 프레임이 소멸되며 Person 참조 주소를 반환
  5. 마찬가지로 buildPerson 프레임이 소멸되며 Person 참조 주소를 반환
  6. main 메서드의 person 변수에 Person 참조 주소가 바인딩되고 main 프레임이 소멸되며 메서드 종료

Stack Memory & Heap Memory

  • Stack Memory (스레드 실행과 static memory 할당을 위해 사용)
    • 메서드 실행/완료에 따라 메모리 크기가 변경됨
    • 스택 내부 지역 변수는 메서드가 실행 동안에만 존재
    • 이 영역이 가득 차면 StackOverFlowError 발생
    • 힙 메모리 보다 상대적으로 빠름
    • 스레드 별로 할당 받기 때문에 스레드 세이프함

  • Heap (객체 생성과 dynamic memory 할당을 위해 사용)
    • Young Generation, Old Generation 같은 메모리 엑세스 기술이 활용됨
    • 이 영역이 가득차면 java.lang.OutOfMemoryError 발생
    • 스택 메모리보다 상대적으로 느림
    • 스택 메모리와 다르게 해당 영역은 GC에 의해 메모리가 비워짐
    • JVM 앱 전체에서 공유되는 영역이기 때문에 스레드 세이프하지 않음 (별도의 동기화 처리 필요)

Stack Memory & Heap Memory

  • Applicaiton
  • 스택은 스레드에 의해 한 번에 하나씩 부분적으로 사용됨
  • 힙은 앱 전체에서 런타임 동안 사용됨

  • Size
    • 스택 크기는 OS에 의존적이며 보통 힙보다 작음
    • 힙 크기는 제한 없음

  • Storage
    • 스택은 힙에서 생성된 객체의 변수와 참조만 저장
    • 힙은 새로 생성된 모든 객체를 저장

  • Order
    • 스택은 LIFO(Last-in First-out) 기준으로 사용/액세스
    • 힙은 Old/Young Generation 등 다소 복잡한 관리 방법을 통해 사용/액세스

  • Life
    • 스택은 현재 메서드가 실행되는 동안만 존재
    • 힙은 앱이 실행되는 동안 존재

  • Efficiency
    • 스택은 힙에 비해서 할당 속도가 매우 빠름
    • 힙은 스택보다 할당 속도가 느림

  • Allocation/Deallocation
    • 스택은 메서드 호출 시 자동으로 할당되며 반환될 때 해제됨
    • 힙은 새 객체가 생성되면 할당되며 더이상 참조(사용)되지 않을 때 GC에 의해 해제됨

Native Memory

  • 네이티브 메모리는 OS 레벨에서 직접 관리되는 메모리 영역
    JVM 메모리 외에 추가 할당이 가능하며 이 경우 JVM의 최대 메모리 설정보다 더 많은 메모리를 사용하게 됨

  • 네이티브 메모리 영역
    • Metaspace (Permanent Generation)
      로딩된 클래스의 메타데이터를 보관하는 영역 (힙 외부의 별도 영역)
      따라서 힙이 아닌 메타스페이스 메모리 설정 필요(-XX:MetaspaceSize, -XX:MaxMetaspaceSize)

    • Threads
      스레드가 사용하는 JVM stack, 일반적으로 약 1MB(-Xss 튜닝 플래그로 설정 가능)
      다른 영역과 달리 스레드 수 제한이 없다면 실질적으로 스택에 할당된 메모리는 제한이 없음
      작업을 수행하는 스레드 외에 GC처럼 별도의 내부작업을 위한 스레드도 필요

    • Code Cache
      JIT 컴파일러에 의해 생성된 네이티브 코드를 저장하는 영역(non-heap 영역)
      메모리 설정 가능(-XX:InitialCodeCacheSize , -XX:ReservedCodeCacheSize)

    • GC (Garbage Collection)
      GC 알고리즘과 함께 제공되며, 이 작업을 위해 일부 오프-힙 데이터 구조가 필요 (더 많은 네이티브 메모리를 필요로 함)

    • Symbols
      대표적으로 String 상수 풀(String 인터닝을 통해 비효율적인 메모리 사용을 줄임)이 있고 이를 네이티브 고정된 크기의 해시테이블에 저장
      메모리 설정 가능(-XX:StringTableSize), 런타임 상수 풀 또한 마찬가지로 여기에 해당함

    • Native Byte Buffers
      개발자가 직접 접근할 수 있는 네이티브 메모리 영역
      JNI 및 NIO 등에서 ByteBuffers를 통한 malloc 호출로 사용 가능
출처
https://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html
https://www.geeksforgeeks.org/java-virtual-machine-jvm-stack-area/
https://www.baeldung.com/java-stack-heap
https://www.baeldung.com/java-stack-heap
profile
Java 먹자

0개의 댓글