개발/JAVA

JVM 메모리 구조

낭곰 2022. 6. 24. 10:26
반응형
반응형

 

 

JVM(Java Virtual Machine)

JVM은 자바 가상머신의 약자로, 자바 소스코드(.java)로부터 만들어지는 자바 바이너리 파일(.class)을 실행할 수있습니다. 또한 JVM은 플랫폼에 의존적입니다.(리눅스의 JVM과 윈도우의 JVM은 서로 다름) 단, 컴파일된 바이너리 파일은 어떤 JVM에서도 동작 시킬 수 있습니다.

 

 


 

JVM의 역할

  • 바이너리 코드를읽습니다.
  • 바이너리 코드를 검증합니다.
  • 바이너리 코드를 실행합니다.
  • 실행환경(Runtime Enviroment)의 규격을 제공합니다.(필요한 lib및 기타파일)

 

아래는 Java 프로그램의 컴파일 및 실행 단계에 대한 그림 입니다.

 

 

컴파일 과정

 

 

Java 프로그램 실행 과정

 

 


 

JVM 메모리 구조

JVM의 구조는크게 보면 아래와 같이이루어져 있습니다.

  1. 클래스 로더(Class Loader)
  2. 실행엔진(Execution Engine)
      - 인터프리터(Interpreter)
      - JIT 컴파일러(Just-In-Time)
      - 가비지콜렉터(Garbage collector)
  3. 런타임 데이터 영역(Runtime Data Area)

JVM 구조

 

클래스 로더(Class Loader)

JVM 내로 클래스 파일을 로드하고(동적 로딩 수행), 링크를 통해 배치하는 작업을 수행하는 모듈입니다.

런타임시 JVM이운영체제로부터 할당받은 메모리 영역인 런타임 데이터 영역(Runtime Data Area)로 적재하는역할을 수행합니다.

즉, 클래스를 처음으로 참조할 때, 해당 클래스를 로드하고 링크하는 역할을 합니다.

Class Loader 계층구조

  • Bootstrap ClassLoader
    3가지중최상위 클래스로 %JAVA_HOME%/jre/lib 에 담긴 Java 라이브러리를 로딩합니다.
  • Extention ClassLoader
    %JAVA_HOME%/jre/lib/ext 폴더에 담긴 Java의 확장 클래스 파일을 로딩합니다.
  • Application ClassLoader
    -classpath(또는 -cp) 폴더에 있는 클래스를 로딩합니다.
    개발자가 직접 작성한 코드를 로딩합니다.

클래스 로더 세부 동작

  • 로드 : 바이트 코드 파일을 가져와서 JVM 메모리에 적재
  • 검증 : 자바언어 및 JVM 명세에 명시된 대로 구성되어 있는지 검사
  • 준비 : 클래스가 필요로 하는 메모리 할당(필드, 메서드, 인터페이스 등)
  • 분석 : 클래스의 Constant Pool 내에 저장된 모든 Symbolic Reference를 Direct Reference로 변경
  • 초기화 : 클래스 변수를 적절한 값으로 초기화(static)

 

실행엔진(Execuion Engine)

클래스 로더를 통해 JVM 내의 런타임 데이터 영역(Runtime Data Area)에 배치된 바이트 코드들을 명령어 단위로 읽어서 실행합니다. 최초 JVM이나왔을 당시에는 인터프리터 방식이었기 때문에 속도가 느리다는 단점이 있었지만 JIT 컴파일러 방식을 통해 이 점을 보완하였습니다. JIT는 바이트 코드를 어셈블리어 같은 네이티브 코드로 바꿈으로써 실행이 빠르지만 역시 변환하는데 비용이 발생하였습니다. 이같은이유로  JVM은 모든 코드를 JIT 컴파일러 방식으로 실행하지 않고, 인터프리터 방식을 사용하다가 일정한 기준이 넘어가면 JIT컴파일러 방식으로 실행합니다.

  • 인터프리터(Interpreter)
    실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행합니다. 하지만 한 줄씩 수행하기 때문에 느리다는 단점이 있습니다.
  • JIT(Just-In-Time compiler)
    인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일 하여 기계어로 변경하고, 이후에는 더 이상 인터프리팅 하지 않고 기계어로 직접 실행합니다.
  • 가비지 콜렉터(Garbage Collector)
    GC는 힙 메모리 영역에 생성된 객체들 중에서 참조되지 않은 객체들을 탐색후 제거하는 역할을 수행합니다. 이떄, GC가 언제 객체를 제거하는지는 정확히 알 수 없습니다.
    또 다른 특징으로는 GC가 수행되는 동안 GC를 수행하는 스레드가 아닌 다른 모든 스레드는 일시정지 됩니다.
    특히 Full GC가 일어나서 수 초간 모든 스레드가 정지한다면 장애로 이어지는 치명적인 문제가 생길 수 있습니다.

    - GC는 Minor GC와 Major GC로 나뉜다.

      Minor GC : New 영역에서 일어나는 GC
      1. 최초에 객체가 생성되면 Eden 영역에 생성된다.
      2. Eden 영역에 객체가 가득차게 되면 첫 번째 GC가 일어난다.
      3. survivor1 영역에 Eden 영역의 메모리를 그대로 복사된다. 그래고 survivor1 영역을 제외한 다른 영역의 객체를 제거한다.
      4. Eden 영역도 가득차고 survivor1 영역도 가득차게 된다면, Eden 영역에 생성된 객체와 survivor1 영역에 생성된 객체 중에 참조되고 있는 객체가 있는지 검사한다.
      5. 참조되고 있지 않은 객체는 내버려두고 참조되고 있는 객체만 survivor2 영역에 복사한다.
      6. survivor2 영역을 제외한 다른 영역의 객체들을 제거한다.
      7. 위 과정중에 일정 횟수 이상 참조되고 있는 객체들을 survivor2에서Old 영역으로 이동시킨다.
        -> 위 과정을 계속 반복, survivor2 영역까지 꽉 차기 전에 계속해서 Old로 비움

    Major GC(Full GC) : Old 영역에서 일어나는 GC 
    1. Old 영역에 있는 모든 객체들을 검사하며 참조되고 있는지 확인한다.
    2. 참조되지 않은 객체들을 모아 한 번에 제거한다.
      -> Minor GC보다 시간이 훨씬 많이 걸리고 실행중에 GC를 제외한 모든 스레드가 중지된다.

    *Major GC(Full GC)가 일어나면, 
    Old 영역에 있는 참조가 없는 객체들을 표시하고 그 해당 객체들을 모두 제거하게 된다.
    그러면서 Heap 메모리 영역에 중간중간 구멍(제거되고 빈 메모리 공간)이 생기는데 이 부분을 없애기 위해 재구성을 하게 된다.(디스크 조각 모음처럼 조각난 메모리를 정리함)
    따라서 메모리를 옮기고 있는데 다른 스레드가 메모리를 사용해 버리면 안되기 때문에 모든 스레드가 정지하게 되는 것이다.

 

 

 

런타임 데이터 영역(Runtime Data Area)

JVM의 메모리 영역으로 JAVA 어플리케이션을 실행할 때 사용되는 데이터들을 적재하는 영역으로 이 영역은 크게 Method Area, Heap, Stack, PC Register, Native Method Stack으로 나눌 수 있습니다.

 

Runtime Data Area

 

Method(=Class=Static) 영역

모든 스레드가 공유하는메모리 영역입니다. 메소드 영역은 클래스, 인터페이스, 메서드, 필드, static 변수 등의 바이트코드를 보관합니다.

  • 가장먼저 데이터가저장됨
  • 클래스 로더에 의해 로드된 클래스, 메서드 정보와 클래스 변수 정보를 저장
  • 클래스 변수 남발 시 메모리공간이 부족할 수 있음(Java7까지는 부족할 수 있었으나 Java8부터 개선됨)
  • 프로그램 시작부터 종료될 떄까지 메모리에 적재
  • 명시적 null 선언 시 GC가 청소
  • 모든 스레드가 공유함

 

Heap 영역

모든 스레드가 공유하며 new 키워드로 생성된 객체와 배열이 생성되는 영역입니다. 또한, 메서드 영역에 로드된 클래스만 생성이가능하고, GC가 참조되지 않는 메모리를 확인하고 제거하는 영역입니다.

  • 런타임 시 결정되는 참조 자료형이 저장됨
    - 런타임 시 결정됨에 따라 동작 중의 문제(범위 초가 참조 등)가 발생할 코드임에도 문법의 문제는 아니기에 컴파일 시 에러를출력하지 않음
  • new 연산자를 통해 생성된 객체(인스턴스)가 저장되는공간
    - 즉 인스턴스 변수도 여기에저장됨
  • 객체가 더이상쓰이지않거나 명시적 null 선언시 GC가 청소
  • 모든 스레드가 공유함

Heap

  1. Permanent Generation
    생성된 객체들의 정보의 주소값이 저장된 공간입니다. Class Loader에 의해 load 되는 Class, Method 등에 대한 Meta 정보가 저장되는 영역이고 JVM에 의해 사용됩니다. Reflection을 사용하여 동적으로 클래스가 로딩되는 경우에 사용됩니다. 내부적으로는 Reflection 기능을 자주 사용하는 Spring Framework를 이용할 경우 이 영역에 대한 고려가 필요합니다.
  2. New/Young 영역
    - Eden : 객체들이 최초로 생성되는 공간
    - Servivor0/1 : Eden에서 참조되는 객체들이 저장되는 공간
  3. Old 영역
    New Area에서 일정 시간 참조되고 있는, 살아남은 객체들이 저장되는 공간으로, Eden 영역에 객체가 가득차게되면 첫번쨰 GC(minor GC)가 발생합니다. Eden 영역에 있는 값들을 servivor1 영역에 복사하고 이 영역을 제외한 나머지 영역의 객체를 삭제합니다.

 

※Heap 영역? Method 영역?

 

위 그림은 일반적으로 JVM의 heap 구조를 설명할 때 사용하는 그림입니다. 그런데 permanent 영역은 보통 heap 취급을 하지 않지만 이렇게 heap 영역에 "포함된" 상태입니다. 하지만 일반적으로 permanent 영역은 구분해서 설명합니다. 그리고 Method 영역은 permanent 영역에 속해 있어 heap 영역이 아니라고 보통이약합니다. 따라서, heap 영역에 포함되어 있기는 하나 heap 영역과는 구분해서 간주함으로 인해 오라클 문서에서 logical하게는 heap 의 한 부분이라고 설명합니다.

 

 

※Heap의 permanent Generation 영역의 변화

  • Java7 JVM
<----- Java Heap -----------------> <--- Native Memory --->
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Permanent | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+
                        <--------->
                       Permanent Heap
S0: Survivor 0
S1: Survivor 1
  • Java8 JVM
<----- Java Heap -----> <--------- Native Memory --------->
+------+----+----+-----+-----------+--------+--------------+
| Eden | S0 | S1 | Old | Metaspace | C Heap | Thread Stack |
+------+----+----+-----+-----------+--------+--------------+

Permanent 영역은 JVM에 의해 크기가 제한된 영역으로 Java7까지 유지되었습니다. 따라서 영역 제한으로 인해 OOM(Out Of Memory) 문제가 있었습니다. 대신 Java8 부터는 Permanent Generation을 제거하고 Metaspace로 대체하였고 heap이 아니라 JVM에 의해메모리가 제한되지 않는 Native Memory 영역으로 전환하여 OS에 의해 메모리 할당 공간이 자동으로 조절되므로 이론상 아키텍쳐가 지원하는 메모리 크기까지 확장할 수 있게 되었습니다.

따라서, 애매하게 heap에 걸쳐있던 permanent 영역이 non-heap이라고 구분하던 과거와는 달리 명확하게 Method area는 heap 영역이아니라고 정의할 수 있게 되었습니다.

변경 이유는 ArrayList와 같은 레퍼런스 타입으 동적 배열 객체를 static으로 생성하면 레퍼런스를 Permanent 영역에 저장하는데 해당 객체 배열에 객체원소를 추가하면 그대로 static object의 레퍼런스가 Permanent 영역에 쌓일 뿐만아니라 string pool도 permanent영역에 저장하느라 OOM 에러가 발생하는 이슈가 잦았었다고 합니다.

 

※Permanent에서 Metaspace로 변경됨에 따른 변화

오라클 문서에는 The proposed implementation will allocate class meta-data in native memory and move interned Strings and class statics to the Java heap. Hotspot will explicitly allocate and free the native memory for the class meta-data. 라고 나와 있습니다.

해석하자면 class meta -data는 native memory로 이동된 Metaspace에 저장하고 permanent에 저장했던 interned strings와 static 변수는 heap 영역으로 보낸다는 의미가 됩니다. 이는 곧 Java8 부터는 satic 변수를 heap 영역에서 관리함은 GC의 대상이 될 수 있음을 의미합니다.

PermGen에 속한 Method Area가 클래스 변수를 저장한다고 알고 있다면 이해하기 쉽지 않을것 같습니다. static 변수는 클래스 변수로 명시적 null 선언이 되지 않으면 GC 되어서는안되는 변수입니다. Method Area가 클래스(static) 변수를 저장한다고 이해하는 시점에서 오해가 발생하게 되는데 Method Area는 class의 meta-data를 저장할 뿐 실질적인 객체와 데이터는 Method Area 바깥의 PermGen에 저장됨을 알아야합니다.

class meta-data가 metaspace로 이동하고 기존에 perment 영역에 저장되던 static object는 heap 영역에 저장되도록 변경되었다고 셜명하는데 이는 reference는 여전히 metaspace에서 관리됨을 의미하기에 참조를 잃은 static object는 GC의 대상이 될 수 있으나, reference가 살아있다면 GC의 대상이 되지 않음을 의미합니다.

따라서, metaspace는 여전히 static object에 대한 reference를 보관하며 애매하게 heap에 걸쳐지지 않고 non-heap(native memory)로 이관되며 static변수(primitive type, interned string)는 heap 영역으로 옮겨짐에 따라 GC의 대상이 될 수 있게끔 조치한 것입니다.

 

클래스 변수 및 객체의 저장위치와 클래스 메타 정보의 위치가 Method 영역이 속한 ParmGen으로부터 Heap과 메모리로 서로 분리되었다는 점을 의미합니다. 

 

 

Stack 영역

메서드 호출 시 마다 각가의 스택 프레임(그 메서드만을 위한 공간)을 생성합니다. 그래고 메서드 안에서 사용되는 갓들을 저장하고, 호출된 메서드의 매개변수, 지역변수, 리턴 값 및 연산시 일어나는 값들을 임시로 저장합니다. 마지막으로, 메서드 수행이 끝나면 프레임 별로 삭제합니다.

  • 컴파일 시 결정되는 기본 자료형(&참조 변수)이 저장됨
     - 컴파일 시 결정됨에 따라 자료형의 범위를 초과한 값할당 등의 코드가 컴파일 단계에서 검출됨
    - 참조 변수는 기본 자료형을 Wrapper Class로 boxing한 변수(Integer, Byte등)
  • 메서드 호출 시 메모리에 FILO(First In Last Out)로 삽입
  • 메서드 종료 시메모리에서 LIFO(Last In First Out)로 제거
  • 메서드가 호출될 때 마다 각각의 스택 프레임이 생성됨
    - 각 스택 프레임은하나의 메서드에 대한 정보를 저장
  • {} 혹은 메서드가 종료될 때 삭제됨
    - 메서드 종료 시 프레임 별로 삭제
  • 각 스레드 별로 생성

stack

 

PC Register

스레드가 시작될 때 생성되며, 생성될 때마다 생성되는 공간으로 스레드마다 하나씩 존재합니다. 스레드가 어떤 부분을 무슨 명령으로 실행해야 할 지에 대한 기록을 하는 부분으로, 현재 수행중인 JVM 명령의 주소를 갖습니다.

  • JVM이 수행할 명령어의 주소를 저장하는 공간
    - OS의 PC 레지스터와 유사한 역할이나 CPU와는 별개로 JVM이관리
  • 스레드가 시작될 때마다 생성됨
  • 각 스레드 별로 생성

 

Native Method Stack

자바 외 언어로 작성된 네이티브 코드를 위한 메모리 영역입니다.

  • 바이트 코드가 아닌 기계어로 작성된 코드를 실행하는 공간
  • 다른언어(c/c++)로 작성된 코드를 수행하기 위함
  • Jaa Native Inerface를 통해 바이트 코드로 변환됨
  • Java Code를 수행하다 JNI 호출 시 Java Stack에서 Native Stack으로 동적 연결(Dynamic Linking)을 통해 확장됨
    - 따라서 나뉘어졌다고는 하나 stack에서 연결할 수 있음
  • JNI(Java Native Interface) 호출 시 생성
  • 각 스레드 별로 생성

 

 

 

 

참고 

https://steady-coding.tistory.com/305

https://jeong-pro.tistory.com/148

https://jwjwj.tistory.com/23

https://velog.io/@dev_isaac/JVM

https://8iggy.tistory.com/229

반응형