[Java] JVM / 자바 프로그램의 실행흐름

- 9 mins

Description:

자바 프로그램이 JVM에서 어떻게 실행되는지, 어떤식으로 메모리를 사용하는 지 알아보자.


Java 프로그래밍을 하기위한 구성요소

Java 프로그래밍을 하기위해선 JDK(Java Development Kit) 가 필요하다. JDK는 자바 프로그램 구동을 위한 요소(JRE)와 자바 프로그램 개발을 위한 요소(javac 및 각종 개발 관련 library)을 포함한 패키지를 지칭한다.

자바 프로그램 실행은 JRE만 있어도 된다.

Screenshot

  1. JDK (Java Development Kit)
    • JRE와 개발용 라이브러리들을 포함하고 있다.
  2. JRE (Java Runtime Environment)
    • JVM이 자바 프로그램을 실행하기위해 필요한 라이브러리와 파일들을 갖고있다.
  3. JVM (Java Virtual Machine)
    • 자바가상머신
    • JAVA 프로그램용 운영체제(OS 역할)
    • .java(소스코드)로부터 컴파일된 .class(자바 바이너리파일)을 실행한다.
    • 자바 바이너리 파일을 플랫폼에 관계없이 독립적으로 실행 가능하도록 JVM이 각 OS 위에서 동작한다.
    • JVM은 각 플랫폼에 의존한다.(Window, Mac, Linux, etc)

JVM의 구조

Screenshot

Class Loader

자바프로그램을 실행하면 .java파일은 컴파일러를 통해 .class(자바 바이트코드)파일로 변환된다. Class Loader는 해당 파일을 Runtime(실행)하는 시점에 Runtime Data Areas에 로딩시킨다. 즉, 컴파일하는 시점이 아니라 클래스를 처음으로 참조하는 시점(클래스의 인스턴스를 만들 때) Class Loader를 통해 메모리(Runtime Data Areas)에 로드한다.

이 때 모든 Class는 참조되는 순간 동적 으로 load와 link가 이루어진다. 이 방식을 Dynamic Loading이라고 한다.

Runtime Data Areas

런타임 데이터 영역은 JVM이 OS 위에서 실행될 때 할당받는 메모리 영역이다.

Alt Text
Runtime Data Areas
Thread가 시작될 때마다 생성

- PC Register

- JVM Stack

- Native Method Stack

모든 Thread가 공유

- Heap

- Method Area >> Runtime Constant Pool

- PC Register

현재 수행중인 JVM 명령의 주소가 저장되어있다. CPU의 Register와는 다름, 연산을 위해 필요한 피연산자를 임시로 저장하기 위한 용도로 사용한다.

- JVM Stack

Stack Frame을 저장하는 스택. JVM은 오직 Stack Frame을 push하고 pop하는 작업만 함. 예외 발생 시 printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현

- Native Method Stack

자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.

- Heap

인스턴스(객체)를 저장하는 공간. GC(Garbage Collection)의 대상이다. 성능 이슈를 일으키는 공간

- Method Area

JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 Runtime Constant Pool, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 이 영역에 있는 클래스 정보로 Heap 영역에 객체를 생성한다. 즉, 매우 중요한 영역!

메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.

Execution Engine

Execution EngineClass Loader를 통해 JVM 내의 Runtime Data Areas에 배치된 바이트코드를 실행한다. Execution Engine은 자바 바이트코드를 명령어 단위로 읽어서 실행한다

그런데 자바 바이트코드는 기계가 바로 수행할 수 있는 언어보다는 비교적 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 다음 두 가지가 있다.

JIT 컴파일러 가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

GC

Garbage Collection은 메모리 관리를 위한 방법 중의 하나로, 프로그램이 동적으로 할당했던 메모리 영역 중에서 필요없게 된 영역[Heap]을 해제하는 기능. (new 연산자를 이용한 객체)

Java GC는 객체가 garbage인지 판단하기 위해 reachability 개념을 사용하여 객체의 유효한 참조가 있다면 ‘reachable’ 로 없으면 ‘unreachable’ 로 구별하고, unreachable 객체를 Garbage로 간주하여 GC를 실행한다. 한 객체가 다른 객체를 참조하며, 다른 객체는 또다른 객체를 참조할 경우에는 유효한 최초의 참조가 무엇인기 파악해야 되는데, 이를 객체 참조의 root set이라고 한다.

Heap 영역에 있는 객체들에 대한 참조의 종류는 4가지이다.

  1. Heap 내의 다른 객체에 의한 참조
  2. JVM Stack, Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  3. Native Stack, JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  4. Method Area 의 static 변수에 의한 참조

이들 중 Heap 내의 다른 객체에 의한 참조를 제외한 나머지 3개(JVM Stack, Native Stack, Method Area)가 root set으로, reachability를 판가름하는 기준이 된다. 즉 root set으로부터 시작한 객체들은 reachable이며, root set과 무관한 객체들이 unreachable 객체로 GC의 대상이 된다.


자바프로그램의 실행흐름

Screenshot JVM의 메모리 구조는 한번에 이해하기 너무 어렵기 때문에, 이해하기 쉽도록 추상화하여 코드실행영역(Runtime Engine을 추상화)과 T 메모리 영역(Runtime Data Areas를 추상화) 이라 지칭하겠다.

T 메모리구조는 “스프링 입문을 위한 자바 객체지향의 원리와 이해”의 저자 김종민님이 설명을 위해 추상화시킨 용어

코드 실행 영역(Runtime Engine)은 자바 개발자들이 알 필요는 없다.(초보 기준) 데이터 저장 영역(Runtime Data Areas)을 알아야한다.

T 메모리 구조

Screenshot

실행흐름

public class Start4 {
	public static void main(String[] args) {
		int k = 5;
		int m;

		m = square(k);
	}

	private static int square(int k) {
		int result;

		k = 25;

		result = k;

		return result;
	}
}
  1. JRE가 자바프로그램을 실행시킬 때 맨처음으로 main()를 찾는다.
  2. 있으면 Class Loader가 목적파일(.class)을 실행시킴
  3. static 영역에 java.lang 패키지, import한 패키지들을 위치시킨다. 프로그램에 있는 모든 클래스필드 ,메소드가 올라감
  4. stack영역에 main()Stack Frame이 위치하고 변수 영역에 인자를 위치시킨다.
    • 지역변수의 경우 선언이 아닌 초기화 될 때 위치된다.
    • 클래스 선언 {}을 제외하고 메소드의 {}, if의 {}이 생길때 마다 Stack Frame이 생긴다.
  5. 메소드를 실행
  6. ”}”괄호를 만나게 되면 Stack Frame이 사라진다.
  7. JRE는 JVM을 종료시키고 위치했던 메모리들이 모두 없어진다.

Screenshot

두 개의 Stack Frame 사이는 서로 접근할 수 없다. 단 스택프레임안에 내부에 스택프레임이 생성된다면 내부에서 외부 스택프레임으로 접근할 수 있다.(메소드 안에 또 if 블록이 있을 경우)

tip : 멤버변수는 초기화 해주는데 왜 지역변수는 초기화 해주지 않을까? 멤버변수는 어디서 먼저 쓸지 모르고 지역변수는 해당지역에서 쓰기때문에 거기서 초기화해서 쓰라는 것이다.

메모리상 변수의 위치

public Tester {
  static int classInt = 10;
  int instanceInt;

  Tester() {
    this.instanceInt = 20;
  }

  int getLocalInt() {
    int localInt = 30;
    return localInt;
  }
}

public Main {
  public static void main(String[] args) {
    Tester newTester = Tester();

    System.out.println(Tester.classInt); // 10
    System.out.println(newTester.instanceInt); // 20
    System.out.println(newTester.getLocalInt()); // 30
  }
}

멀티스레드와 멀티프로세스

멀티스레드

Screenshot

멀티스레드는 stack 영역(JVM stack)을 스레드 갯수만큼 분할

멀티프로세스

Screenshot

멀티프로세스는 데이터 저장영역(Runtime Data Areas)을 프로세스 갯수만큼 분할


참고

Sehun Kim

Sehun Kim

하다보니 되더라구요.

comments powered by Disqus
rss facebook twitter github youtube mail spotify lastfm instagram linkedin google google-plus pinterest medium vimeo stackoverflow reddit quora quora