프로그래밍 언어/Java

[Java] JVM

highright96 2021. 10. 25.

안녕하세요. 오늘은 전에 간단히 살펴봤던 JVM에 대해 좀 더 자세히 설명드리려고 합니다.

 

자바를 이용하는 개발자라면 누구나 자바 바이트코드가 JRE 위에서 동작한다는 사실을 잘 아실 겁니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java Virtual Machine)입니다.   그럼 JVM이 무엇이고, 어떻게 동작하고, 어떤 구조를 갖고 있을까요? 

JVM은 무엇일까?

C/C++은 컴파일 플랫폼(=운영체제 + CPU 아키텍쳐)과 실행 플랫폼이 다를 경우, 프로그램이 동작하지 않습니다. 대부분의 개발자들은 동일한 플랫폼에서 컴파일을 하고, 실행을 하기 때문에 문제가 없을 것입니다. 그러나 배포할 때에는 문제가 생길 수도 있습니다.  윈도우에서 만든 프로그램이라면 리눅스 서버에서는 이 프로그램이 동작하지 않습니다. 그래서 C/C++ 진영은 크로스 컴파일(Cross Compile)을 해결책으로 내놓았습니다. 

 

하지만, 자바는 크로스 컴파일이 아닌 JVM으로 이 문제를 해결했습니다.  JVM은 컴파일된 자바 바이트 코드를 플랫폼이 인식할 수 있는 기계어로 번역해줍니다. 컴파일된 자바의 바이트코드는 JVM 위에서 동작하기 때문에 어떤 플랫폼이든지 실행될 수 있는 것  입니다. 참고로, JVM은 플랫폼에 의존하기 때문에 윈도우용 JVM, 리눅스용 JVM이 따로 존재합니다.

JVM의 구조

JVM의 구조는 다음 그림과 같습니다.

https://velog.io/@livenow/Java-JVM%EC%9D%B4%EB%9E%80

 

클래스 로더

자바는 동적 로드, 즉 컴파일타임이 아니라 런타임(바이트코드를 실행시킬 때)에 클래스 로드하고 링크하는 특징이 잇습니다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더입니다.

클래스 로더에는 로딩, 링크, 초기화 단계로 나눠져 있는데, 간단히 설명하면 다음과 같습니다.

 

로드

  • 클래스를 파일에서 가져와서 JVM의 메모리에 로드합니다.

링크

  • 검증 : 읽어 들인 클래스가 자바 언어 명세 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사합니다.
  • 준비 : 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스를 나타내는 데이터 구조를 준비합니다.
  • 분석 : 심볼릭 메모리 레퍼런스를 메서드 영역에 있는 실제 레퍼런스로 교체합니다.

초기화

  • 클래스 변수들을 적절한 값으로 초기화합니다. 즉, static 필드들을 설정된 값으로 초기화합니다.

런타임 데이터 영역

런타임 데이터 영역은 JVM이 운영체제 위에서 실행될 때, 할당받는 메모리 영역입니다.  런타임 데이터 영역은 총 5개의 영역으로 나눌 수 있습니다.

그중 메서드 영역, 힙은 모든 스레드가 공유하는 공간이고  PC 레지스터, JVM 스택, 네이티브 메서드 스택은 스레드마다 하나씩 생성됩니다.

 

메서드 영역(Method Area)

메서드 영역은 모든 스레드가 공유하는 영역으로, JVM이 시작될 때 생성됩니다. 클래스 로더가 클래스 파일을 읽어오면, 클래스 정보를 파싱 해 필드, 메서드 정보, static 변수, 메서드의 바이트코드 등을 보관합니다.

메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM에서는 흔히 Permanent Generation(PermGen) (JDK 1.8부터 MetaSpace로 변경)이라고 불립니다. 메서드 영역에 대한 GC도 JVM 벤더의 선택 사항입니다.

 

힙(Heap)

힙은 모든 스레드가 공유하는 영역으로, 프로그램을 실행하면서 생성된 모든 인스턴스 또는 객체를 저장하는 공간입니다. 

PC 레지스터

레지스터는 각 스레드마다 하나씩 존재하며, 스레드가 시작될 때 생성됩니다. 레지스터는 메서드 안에서 바이트코드 몇 번째 줄을 실행하고 있는지와 같은 현재 정보를 갖고 있습니다.

 

JVM 스택

스택은 각 스레드마다 하나씩 존재하며, 스레드가 시작될 때 생성됩니다.  스택은 스택 프레임이라는 구조체로 이루어져 있는데, 새로운 매소드가 호출될 때마다 push, 메서드 실행이 끝나면 pop 동작을 수행합니다.

각 스택 프레임은 지역 변수 배열(local variables array), 피연산자(operand stack)프레임 데이터(frame data)를 갖습니다. 여기서 프레임 데이터는 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀(runtime constant pool), 이전 스택 프레임에 대한 정보, 현재 메서드가 속한 클래스/객체에 대한 참조 등을 말합니다.

스택 프레임에 들어있는 정보들은 컴파일 시점에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 고정됩니다.

 

네이티브 메서드 스택

네이티브 메서드 스택는 자바 바이트코드가 아닌 다른 언어(C/C++)로 작성된 네이티브 코드를 위한 스택입니다. 성능 향상을 목적으로 작성되었습니다.

바이트코드 분석

앞에서 런타임 데이터 영역에 대해 자세히 알아봤습니다. 그럼 스레드들이 해당 영역을 어떻게 활용하여, 바이트코드를 실행시키는지 살펴보도록 하겠습니다.

 

실제 코드

바이트코드

Code:
  0: ldc2_w        #2                  // double 10.0d
  3: dstore_1
  4: ldc2_w        #2                  // double 10.0d
  7: dstore_3
  8: ldc2_w        #2                  // double 10.0d
  11: dstore        5
  13: dload_3
  14: dload         5
  16: ldc2_w        #4                  // double 100.0d
  19: dmul
  20: dadd
  21: dstore_1
  22: return

 

0 : ldc2_w  #2

상수 풀 2번 인덱스에 저장된 10.0을 operand stack에 push 합니다.

 

3 : dstore_1

operand stack에 저장된 10.0을 pop 하여 지역 변수 배열 1번 인덱스에 저장합니다.

4 : ldc2_w  #2

상수 풀 2번 인덱스에 저장된 10.0을 operand stack에 push 합니다.

 

7 : dstore_3

operand stack에 저장된 10.0을 pop 하여 지역 변수 배열 3번 인덱스에 저장합니다.

8 : ldc2_w  #2

상수 풀 2번 인덱스에 저장된 10.0을 operand stack에 push 합니다.

 

11 : dstore_5

operand stack에 저장된 10.0을 pop 하여 지역 변수 배열 5번 인덱스에 저장합니다.

13 : dload_3

지역 변수 배열 3번 인덱스에 저장된 10.0을 operand stack에 push 합니다.

15 : dload_5

지역 변수 배열 5번 인덱스에 저장된 10.0을 operand stack에 push 합니다.

16 : ldc2_w   #4

상수 풀 4번 인덱스에 저장된 100.0을 operand stack에 push 합니다.

19 : dmul

operand stack에 저장된 100.0과 10.0을 pop 해 곱하기 연산을 한 뒤 다시 push 합니다.

20 : dadd

operand stack에 저장된 1000.0과 10.0을 pop 해 더하기 연산을 한 뒤 다시 push 합니다.

21 : dstore_1

operand stack에 저장된 1010.0을 pop 해 지역 변수 배열 1번 인덱스에 저장합니다.

22 : return

메서드를 종료합니다.

 

간단한 연산을 하는 자바 코드를 컴파일하고, 컴파일된 바이트코드를 스레드가 JVM 스택 영역을 어떻게 활용하면서 실행시키는지 알아봤습니다.  지역 변수 배열, 피연산자(operand) 스택, 상수 풀 간에 많은 데이터 전달이 있었고, 그 전달은 load, store, ldc 등의 명령어로 이루어지는 것도 확인할 수 있었습니다.

 

위 그림에서는 보여주지 않지만, 실제 동작에서는 각각의 클래스 인스턴스들이 힙에 할당되고, 클래스 정보가 메서드 영역에 저장되어 활용될 것입니다.

마무리

오늘은 JVM의 클래스 로더, 런타임 데이터 영역에 대해 알아봤습니다. 이번 기회에 잊고 있었던 JVM의 내부 구조를 다시 한번 정리할 수 있었고, 사실 메모리 별 역할이 모호했는데 바이트코드를 직접 분석해보니 이해할 수 있었습니다.

 

다음 글에서는 이번에 설명하지 못한 실행 엔진(인터프리터, JIT 컴파일러, GC)에 대해 알아보려 합니다. 읽어주셔서 감사합니다.

참고

'프로그래밍 언어 > Java' 카테고리의 다른 글

[Java] ClassLoader  (0) 2021.11.02
[Java] Garbage Collection  (0) 2021.10.31
자바(Java) 버전별 특징  (0) 2021.10.19
[스터디 할래] 6주차 - 상속  (0) 2021.05.26
[스터디 할래] 5주차 - 클래스  (0) 2021.05.26

댓글