JVM 전반에 관하여, 메모리 관리와 GC

2024. 1. 28. 18:33Backend 취업준비/Java

자바 SE(Standard Edition) 의 구현체는 JDK(자바 개발 키트)와 JRE(자바 실행 환경)로 구성된다

  • JDK => 프로그램 개발에 필요한 JVM + 라이브러리API + 개발도구(java.exe, javac.exe) , JDK를 설치하면 JRE도 자동 설치된다.
    • *.java => (javac.exe) =>  *.class => (java.exe) => 기계어 => 실행
  • JRE => 프로그램 실행에 필요한 JVM + 라이브러리API
    • 자바 프로그램을 개발하고자 하는 것이 아닌 이미 개발된 프로그램을 실행만 하려면 설치

JVM

  • 운영체제, 다른 애플리케이션은 컴퓨터의 메모리 위에서 돌아간다. Java프로그램을 실행하면 실행을 위한 메모리를 할당받아 Runtime Data Area 구성.
  • 자바 소스코드를 작성하고, 자바 컴파일러를 통해 컴파일을 하면 .class라는 확장자를 가진 파일이 생성된다. 또한, java 명령으로 해당 클래스 파일을 실행하면, JVM은 클래스 로더를 통해 클래스 파일을 읽어들인다.
    • javac.exe => 자바 컴파일러.*.java 소스파일을 *.class 클래스 파일로 변환
      • 클래스 파일 안에는 클래스 안에 어떤 필드가 몇개 선언되어 있는지, 메서드는 몇개고 이름은 뭔지, 바이트코드 등 클래스에 대한 모든 정보가 들어있다.
    • java.exe => 바이트 코드(JVM이 알아들을 수 있는 명령어 집합)를 기계어로 번역해서 CPU에 일을 시킴. 인터프리터가 해당 역할 수행, 자바 프로그램(클래스 파일)을 실행
  • 이때, 미리 설치된 JVM 은 작성된 프로그램이 운영체제가 다르더라도 동일하게 작동하도록 환경을 제공한다. => 운영체제에 맞춰 컴파일해야하는 다른 언어에 비해 높은 이식성의 장점을 가진다.

 

JVM Architecture

Class Loader

 

  • Java Compiler 를 통해서 .class 확장자를 가진 클래스 파일은 각 디렉터리에 흩어져 있다. 또한, 기본적인 라이브러리의 클래스 파일들은 $JAVA_HOME 내부 경로에 존재한다. 각각의 클래스 파일들을 찾아서 JVM 의 메모리(Runtime Data Area)에 탑재해주는 역할을 하는 것이 바로 ClassLoader 의 역할.
  • ClassLoader 는 클래스 파일을 찾아서 탑재하는 역할뿐만이 아니라 jvm 에 관련된 다른 일들도 같이한다.
  • ClassLoader 는 크게 Loading, Linking, 그리고 Initialization 3가지 역할을 맡는다. 
  • Loading
    • ClassLoader 가 필요한 클래스 파일들을 찾아 탑재한다. 각각의 클래스 파일들이 기본으로 제공받는 클래스 파일인지 혹은 개발자가 정의한 클래스 파일인지와 같은 기준에 의해서 세가지의 수준으로 나뉜다.
      • Bootstrap ClassLoader => 다른 모든 ClassLoader 의 부모가 되는 ClassLoader.
      • rt.jar 를 포함하여, JVM 을 구동시키기 위한 가장 필수적인 라이브러리의 클래스들을 JVM 에 탑재한다. 가장 상위의 ClassLoader 이므로 다른 ClassLoader 와는 다르게 탑재되는 운영체제에 맞게 네이티브 코드로 쓰여있습니다
      • Extensions ClassLodaer, Application ClassLoader
  • Liniking : 로드된 클래스 파일들을 검증하고, 사용할 수 있게 준비하는 과정. Verification , Preparation , 그리고 Resolution 이라는 세 가지 단계로 이루어진다.
  • Initialization :  클래스 파일의 코드를 읽는다. Java 코드에서의 class  interface 의 값들을 지정한 값들로 초기화 및 초기화 메서드를 실행시킨다. 이때, JVM 은 멀티 쓰레딩으로 작동을 하며, 같은 시간에 한 번에 초기화를 하는 경우가 있기 때문에 초기화 단계에서도 동시성을 고려해주어야 한다. Class Loader 를 통한 클래스 탑재 과정이 끝나면 본격적으로 JVM 에서 클래스 파일을 구동시킬 준비가 끝난다.

 

Runtime Data Area

 

  • Method Area
    • JVM 의 모든 Thread가 공유하는 영역, JVM 구동 시작 시에 생성이 되며, 종료 시까지 유지되는 공통 영역
    • 클래스에 대한 모든 정보가 저장 된다. 인스턴스 생성을 위한 객체 구조, 생성자, 필드 등이 저장. Runtime Constant Pool  static 변수, 그리고 메소드 데이터와 같은 Class 데이터들도 이곳에서 관리
    • JVM 의 다른 메모리 영역에서 해당 정보에 대한 요청이 오면, 실제 물리 메모리 주소로 변환해서 전달
  • Heap
    • JVM 에서 하나만 생성이 되고, 해당 영역 데이터는 모든 Java Stack 영역에서 참조, Thread 간 공유.
    • 런타임에 생성되는 모든 객체들이 저장(문자열에 대한 정보를 가진 String Pool 뿐만이 아니라 실제 데이터를 가진 인스턴스, 배열 등)
    • Heap 영역이 가득 차게 되면 OutOfMemoryError 를 발생시키게 됩니다. => Garbage Collector의 주요 작동 영역
  • JVM Stack
    •  Thread 별로 따로 할당되는 영역. Thread 별로 메모리를 따로 할당하기 때문에 동시성 문제에서 자유롭다.
    • 메서드를 실행하기 위한 정보들이 저장되는 공간.  Thread 들은 메서드를 호출할 때마다 Frame 이라는 단위를 추가(push)한다. 메소드가 마무리되며 결과를 반환하면 해당 Frame  Stack 으로부터 제거(pop)가 된다. 
    • Frame 은 메서드에 대한 정보를 가지고 있는 Local Variables Array, Operand Stack 그리고 Constant Pool Reference 로 구성이 되어 있다. 
    • Local Variables Array은 메소드의 매개변수, 지역 변수들을 담고 있는 배열. 인스턴스 메서드일 경우 첫번째 인덱스에 현재 인스턴스에 대한 참조를 가지고 있다.
    • Operand Stack 은 메소드 내 연산을 위해서, 바이트 코드 명령문들이 들어있는 공간. JVM은 Stack을 기반으로 연산을 수행하는데 피연산값 혹은 연산의 중간 값들을 저장하기 위한 자료구조.
    • Constant Pool Reference  Constant Pool 참조를 위한 공간.
  • PC Register
    • 현재 실행되고 있는 명령어의 주소를 저장하고 있는 곳
    • 멀티 쓰레드 프로그래밍 환경에서 한 쓰레드에서 작업하다가 다른 쓰레드로 잠시 CPU 점유를 넘겨주고,다시 돌아왔을 때, 이전에 어떤 명령을 수행하고 있었는지 기억하고 있어야, 이전 작업을 다시 이어서 수행할 수 있을 것이다.
  • Native Method Stack
    • C나 C++로 작성된 메서드를 실행할 때 사용되는 Stack
  • JVM Stack, PC Register, Native Method Stack 세 개의 영역은 쓰레드가 생성될 때 마다 같이 생성이 되고, 서로 다른 쓰레드가 침범할 수 없는 영역이다.

 

 

메모리 관리와 GC(Garbage Collector)

 

  • JVM 상에서 동적으로 할당된 메모리 영역 중 더 이상 사용되지 않는 영역을 탐지하여 해제하는 기능
    • Stack : 정적으로 할당한 메모리 영역, 원시 타입의 데이터가 값과 함꼐 할당, Heap영역에 생성된 Object타입의 데이터의 참조 값 할당. (frame이 pop되면서 삭제된다)
    • Heap : 동적으로 할당한 메모리 영역, 모든 Object 타입의 데이터가 할당, Heap영역의 Object를 가리키는 참조변수가 Stack에 할당. ( frame이 pop되면서 삭제되고, 더이상 참조되지 않는 객체일 경우 GC가 삭제)
  • 참조되고 있는지에 대한 개념을 reachability 라고 하고, 유효한 참조를 reachable , 참조되지 않으면 unreachable 이라고 한다. 그리고 GC unreachable 한 객체들을 garbage 라고 인식하게 된다.
  • Garbage Collection 과정 정리 (1, 2 를 Mark과정, 3을 Sweep과정이라 칭하여 Mark and Sweep 알고리즘 이라 한다)
    1. Gabage Collector가 Stack의 모든 변수를 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
    2. Reachable Object가 참조하고 있는 객체도 찾아서 마킹한다.
    3. 마킹되지 않은 객체를 Heap에서 제거한다.
  • JVM 에서 자동으로 동작하기 때문에 Java 는 특별한 경우가 아니면 메모리 관리를 개발자가 직접 해줄 필요가 없다.

 

stop-the-world

  • JVM 은 GC 를 통해 JVM 에서의 여유 메모리를 확보 한다. 이 때문에 GC 를 자주 실행시키면, 여유 메모리를 최대한 확보하여 성능이 좋아지리라 추측할 수도 있다. 하지만 빈번한 GC 는 프로그램의 성능을 저하 한다.
  • GC 가 일어나면 GC 를 담당하는 쓰레드를 제외한 모든 쓰레드들은 작동이 일시적으로 정지되며, 이를 Stop-The-World 현상이라고 한다.
  • 모든 쓰레드가 정지되기 때문에 더 이상 작업이 실행되지 않고, 성능이 저하된다. 그래서 적절한 빈도의 GC 가 실행되도록 하여, Stop-The-World 시간을 줄여 쓰레드가 정지되는 시간을 줄이는 것이 중요하다.

 

위의 특징들로 생기는 장단점

  • 장점
    • 메모리 누수가 발생되지 않음
    • 휴먼 에러 발생 가능성 낮춤 (해제된 메모리에 업근시도 or 메모리 이중 해제)
  • 단점
    • 성능저하 (어떤 메모리를 해제해야 할지 검사하고 삭제하는 이 과정 또한 결국 CPU자원과 메모리를 필요로 한다)
    • 개발자가 언제 메모리가 해제되는지 모른다. (JVM은 GF를 실행시키기 위해 잠시 어플리케이션 실행을 멈춘다. 실시간성이 매우 강조된다면, 이런 특징이 적합하지 않을 수 있다.)

 

Heap영역의 구조

  • Heap은 young generation, old generation으로 나뉜다.
    • young generation : 새로운 객체들이 할당
    • old generation : young generation에서 오랫동안 살아남은 객체들이 존재

 

  • generation이 둘로 나뉘는 이유
    • 할당된 객체는 오랫동안 참조되지 않는 것이 대부분이다. 즉, 금방 garbage상태가 된다.
    • 반대로 오래된 객체에서 젊은 객체로의 참조는 거의 없다.
    • 따라서 Heap이 하나라면, 오래된 객체까지 스캔하는 것(major gc)이 비효율 적이다.
    • 해당 이유로 두개의 구역으로 나누고, 할당되지 않은 객체들을 주기적으로 스캔하는 것(minor gc)이 훨씬 효율적이다.

 

출처

(도서) 이것이 자바다

https://www.linkedin.com/pulse/jvm-architecture-how-internally-work-ali-as-ad

https://tecoble.techcourse.co.kr/post/2021-07-12-jvm-jre-jdk/

https://tecoble.techcourse.co.kr/post/2021-07-15-jvm-classloader/

https://tecoble.techcourse.co.kr/post/2021-08-09-jvm-memory/

https://tecoble.techcourse.co.kr/post/2021-08-30-jvm-gc/

https://www.youtube.com/watch?v=GU254H0N93Y

https://www.youtube.com/watch?v=vZRmCbl871I

https://www.youtube.com/watch?v=M49_H5FjJ3U