본문 바로가기

공부방/JAVA

Java의 동작 방식과 JVM의 구조

Java의 동작 방식과 JVM의 구조

 

Java Virtual Machine (JVM)은 Java 언어로 작성된 프로그램을 실행하는 가상 머신입니다. JVM은 Java 프로그램의 실행 환경을 제공하며, 운영 체제와 Java 프로그램 사이에서 중개자 역할을 수행합니다. JVM의 구조와 동작 원리를 이해하는 것은 Java 개발자에게 매우 중요한 요소이기 떄문에, Java의 동작 방식과 JVM의 구조에 대해서 한 번 살펴보도록 하겠습니다.

 

우선 javac라는 컴파일러를 이용하여 모든 .java 파일을 .class 파일로 컴파일 시킵니다. JVM은 바이트코드로 작성된 .class 파일을 이용하여 운영체제에 맞는 네이티브코드로 변환해줍니다. 간략히 살펴보면 아래와 같은 구조입니다.

 

https://dev-sia.tistory.com/4

 

 

https://gngsn.tistory.com/252

 

 

 

 


클래스 로더 시스템

 

JVM은 클래스 파일들을 로드하고, 링크하며, 초기화하는 역할을 수행하는데, 이러한 역할을 담당하는 핵심 구성 요소가 클래스 로더(Class Loader)입니다. 클래스 로더는 JVM의 실행 환경에서 클래스 파일을 로드하고 (바이트 코드를 읽고) 메모리에 올리는 중요한 역할을 수행합니다. 

 

클래스로더가 하는 역할은 크게 세가지로 나눌 수 있다.

  • 로딩: 클래스 파일에서 바이트 코드를 읽어오고 메모리에 올리는 역할을 합니다.
  • 링크: 레퍼런스를 연결하는 과정입니다.
  • 초기화: static 값들 초기화 및 변수에 할당하는 과정입니다.

 

[ Loading ]

클래스가 로딩하는 과정에는 세개의 로더가 동작을 합니다. Application class loader, Platform class loader, Bootstrap class loader, platform class loader는 extention class loader라고 부르기도 하며, Application class loader는 system class loader라고 부르기도 합니다.  클래스로더의 명칭을 아래 표로 정리해 놓도록 하겠습니다. 

 

Bootstrap class loader  
Platform class loader Extention class loader
Application class loader System class loader

 

클래스 로더는 아래와 같이 호출할 수 있으며, Application class loader의 부모 클래스가 Platform class loader이고, platform class loader의 부모 클래스가 Bootstrap class loader인 것을 아래 명령어를 통해 알 수 있습니다.

 

 

위의 커맨드를 실행하면 아래와 같이 실행되고 있는 클래스로더를 아래와 같이 확인할 수 있습니다.

 

 

Bootstrap class loader의 결과가 출력되지 않은 이유는 네이티브로 구현되어 있는 클래스 로더이기 때문에 vm마다 다르고 자바에서 이것을 참조해서 출력할 수가 없기 때문에 null로 나타나는 것입니다.

 

클래스로더는 아래와 같이 Application classloader가 platform class loader를 호출하고 platform class loader가 bootstrap class loader를 호출해서, 클래스를 찾으면 로드하고, 찾지 못하면 platform class laoder가 찾고 platform이 못찾으면 system class loader가 찾는 형태입니다. 클래스를 찾지못하면 ClassNotFoundException이 출력됩니다.

 

 

 

 

[ Linking ]

Verify, Prepare, Reolve(optional) 세 단계로 나눠져 있다.

verify : 로딩된 바이트코드의 유효성을 검증합니다.  (임의로 컴파일된 클래스 파일을 조작하면 에러 나고 종료됩니다.)

Prepare : 선언된 스태틱 필드를 초기화, 필요한 메모리 할당을 합니다.

Resolve : 심볼릭 메모리 레퍼런스를 메소드 영역에 있는 실제 레퍼런스로 교체합니.(optional, 이때 발생할 수도 있고 실제 사용될 때 발생할 수도 있음)

 

 

 

[ Linking - Verifiy ]

Binary Representation(Java Bytecode)의 유효성을 검사하는 프로세스입니다.

  • Compiler가 유효한지 확인합니다.
  • Class 파일의 포맷 혹은 구조가 올바른지 확인합니다.

 

 

 

[ Linking - Prepare ]

우리가 변수에 지정한 값이 아니라, 기본 값으로 초기화하는 과정입니다.

모든 int 타입 변수에는 0으로 초기화를, 모든 객체에는 null 값으로 초기화, 모든 부울 변수에는 false를 할당합니다.

 

예를 들어 아래와 같은 클래스 내에 아래와 같은 정의가 있었다면,

JVM은 Preparation 단계에서 enabled를 위한 메모리를 할당하고 boolean의 default value인 false를 설정합니다.

 

private static final boolean enabled = true

 

현재 단계에서는 무조건 default 값을 할당하고, "Linking - Initialization" 에서 해당 값을 초기화합니다.

 

 

 

[ Linking - Resolve ]

심볼릭 레퍼런스가 구체적인 값을 가리키도록 동적으로 결정하는 프로세스입니다.

심볼릭 레퍼런스란, 컴파일시에 각각의 메모리 주소를 직접 가리키는 대신 특정 문자열로 대체되어 있습니다. 이 문자열은 javap를 통해서 확인할 수 있는데, 이 문자열 Linking - Resolve 단계에서 실제 메모리 주소 값으로 대체됩니다.

 

 

 

[ Linking - Initilization ]

이 단계에서는 모든 정적 및 인스턴스 변수에 실제 값들이 할당됩니다. 모든 static 변수와 인스턴스 변수에 실제 값이 할당됩니다. 

 

 

 

 

 


메모리 ( Runtime Data Area )

JVM은 Java 프로그램의 실행을 담당하며, 이를 위해 Runtime Data Area라고 불리는 메모리 영역을 제공합니다.

Runtime Data Area는 JVM이 프로그램을 실행하는 동안 데이터와 코드를 저장하고 관리하는 중요한 구성 요소입니다.

메모리는 복잡해 보일 수 있는데 크게 다섯가지 영역으로 나눠져 있습니다. 각각의 영역을 살펴보도록 하겠습니다.

 

https://gngsn.tistory.com/252

 

 

 

[ 메모리 (Runtime Data Area) - Method Area ]

메소드 영역은 클래스 로더에 의해 로드된 클래스 파일의 바이트 코드와 메소드 정보, 상수 풀(Constant Pool) 등을 저장하는 영역입니다. 이 영역은 JVM이 시작될 때 생성되며, 모든 스레드에서 공유되는 데이터를 저장합니다.

  • 메소드 영역에는 클래스 수준의 정보 (풀패키지 경로를 포함한 클래스 이름, 부모 클래스 이름, 메소드, 변수) 저장.
  • 공유 자원이다. (다른 영역에서도 참조할 수 있는 정보라는 뜻)

 

 

 

[ 메모리 (Runtime Data Area) - Heap Area ]

힙(Heap)은 동적으로 생성된 객체와 배열을 저장하는 영역으로, 프로그램에서 사용하는 모든 객체가 여기에 할당됩니다. 힙은 JVM의 가비지 컬렉션(Garbage Collection)에 의해 관리되며, 객체의 생성과 소멸을 관리하여 메모리를 효율적으로 사용합니다.

  • 힙 영역에는 객체를 저장. 공유 자원이다.

 

 

 

[ 메모리 (Runtime Data Area) - stack ]

스택(Stack)은 각 스레드마다 생성되는 영역으로, 메소드 호출과 관련된 정보를 저장합니다. 스택은 메소드 호출 시마다 스택 프레임(Stack Frame)을 생성하여 호출된 메소드의 로컬 변수, 매개변수, 임시 데이터 등을 저장합니다. 스택은 메소드 호출이 완료되면 해당 프레임을 제거하여 메모리를 반환합니다.

  • 스택 영역에는 쓰레드 마다 런타임 스택을 만들고, 그 안에 메소드 호출을 스택 프레임이라 부르는 블럭으로 쌓는다. 쓰레드 종료하면 런타임 스택도 사라진다. (에러 메세지 로그가 스택으로 나오는데 이때 보여지는 것이 스택에서 나오는 것이다.)

 

 

 

[ 메모리 (Runtime Data Area) - PC Register]

PC 레지스터(Program Counter Register)는 각 스레드마다 현재 실행 중인 명령어의 주소를 저장하는 영역입니다. PC 레지스터는 JVM이 다음에 실행할 명령어를 가리키며, 명령어의 실행 위치를 제어합니다.

  • PC(Program Counter) 레지스터: 쓰레드 마다 쓰레드 내 현재 실행할 instruction의 위치를 가리키는 포인터가 생성된다.

 

 

 

[ 메모리 (Runtime Data Area) - Native Method Stack ]

  • 쓰레드 마다 생기는데 네이티브 메소드 호출할 때 사용하는 별도의 메소드 스택이다.
  • Native Method는 다른 프로그래밍 언어로 구현된 자바 메소드입니다. 가령 C 나 C++ 등으로 작성될 수 있습니다. 
  • 해당 메모리 영역은 이러한 native method들의 정보를 담는 역할을 합니다. 
    • 네이티브 메소드 라이브러리를 쓰려면 JNI를 통해야하는데 JNI를 사용하는 메소드 스택은 네이티브 메소드 스택에 저장됩니다.
    • JNI가 구현된 것을 네이티브 메서드 라이브러리라고 보면 된다.
    • 라이브러리는 항상 JNI를 통해야 한다.
    • 네이티브 메서드라고 함은 메소드에 native라고 붙어있고 구현을 java가 아닌 c나 c++로 구현한 것이다.
    • https://javapapers.com/core-java/java-jvm-run-time-data-areas/#Program_Counter_PC_Register

 

 

 

힙영역과 메소드 영역은 모드 쓰레드에서 공용으로 사용 가능하며,
Stack,  pc register,  Native method stack 은 특정 쓰레드에 국한된 정보입니다.

 

 

 

[ 메모리 (Runtime Data Area) - Execution engine ]

 

  • 인터프리터: 바이트 코드를 한줄 씩 실행.
    • 바이트코드는 인터프리터가 이해할 수 있습니다. 한줄 한줄 실행하면서 네이티브 코드로 바꿔줍니다.
    • 한줄 마다 바이트 코드를 네이티브 코드로 컴파일 해서 실행하는 것입니다.
    • 똑같은 코드가 여러개 나와도 매번 네이티브 코드로 바꾸는 것이 비효율 적이기 때문에 반복적인 코드가 나오면 JIT 컴파일러로 보냅니다.
    • JIT 컴파일러는 자바 코드를 바이트 코드로 바구는 것이 아닌 바이트 코드를 네이티브 코드로 바꿔주는 것이며 반복된 코드를 찾아 미리 네이티브 코드로 바꿔주고, 인터프리터가 해당 라인에 걸리면 JIT에서 바꿔놓은 네이티브 코드를 사용하여 프로그램 효율을 높여주는 것입니다.
  • JIT 컴파일러: 인터프리터 효율을 높이기 위해, 인터프리터가 반복되는 코드를 발견하면 JIT 컴파일러로 반복되는 코드를 모두 네이티브 코드로 바꿔둡니다. 그 다음부터 인터프리터는 네이티브 코드로 컴파일된 코드를 바로 사용합니다.
  • GC(Garbage Collector): 더이상 참조되지 않는 객체를 모아서 정리합니다.
    • GC 개념은 이해를 해야되는 것은 당연하며 경우에 따라 옵션을 변경해야 할 때도 있습니다. (customizing)
    • 프로젝트에 사용하는 GC를 알아야 하며, 어플리케이션 실행시 사용할 GC를 선택해야 할 때도 있습니다.
    • 가장 크게 GC를 둘로 나누면 throughput 위주의 GC 가 있고, stop the world를 줄이는 GC가 있습니다.
    • 서버 운영중에 굉장히 많은 객체를 사용하고 response time이 중요하다 -> stop the world를 최소화 할 수 있는 gc사용하는 것이 맞습니다.

 

 

 

[ JNI (Java Native Interface) ]

 

 

 

 

[ Native Method Library ]

  • JNI가 구현된 것을 의미합니다
  • 일반적으로 C 또는 C++로 작성 되어 있습니다.