Java

JVM 내부 동작 원리 & 자바 컴파일,실행 순서 - 런타임 영역, JIT 컴파일러, 인터프리터

sunggook lee 2021. 1. 5. 19:46

 

출처 http://tcpschool.com/java/java_intro_programming

JVM이란 컴퓨터가 자바 프로그램을 실행할 수 있도록 도와주는 것입니다. OS가 다 달라도 JVM 버전이 OS마다 있기 때문에 JVM은 OS에 의존적이지만 자바 파일은 OS에 의존적이지 않고 JVM에서 실행될 수 있는 것입니다.

 

 

javac hello.java

 

1. 위와 같이 javac로 hello.java를 컴파일하면 자바 컴파일러가 컴파일하여 자바 클래스파일(바이트코드)로 만듭니다.

2. 위 명령어를 실행하면 hello.class라는 클래스파일이 생깁니다.

 

 

Java hello

 

3. 새로 생긴 hello.class를 Java 프로그램 명령어를 통해 실행시킵니다. 이 실행되는 순간 JVM이 동작하는 것이라고 보면 됩니다.

4. 클래스 로더가 클래스(바이트코드)를 JVM내의 Runtime Dara Areas로 올립니다.

 

출처 https://www.holaxprogramming.com/2013/07/16/java-jvm-runtime-data-area/

JVM의 동작 원리를 이해하려면 위 그림에서 나타내는 JVM의 Runtime Data Areas 영역에 대한 이해가 필요합니다.

 

Runtime Data Areas

PC Registers

Program Counter라고도 불리는 이영역은 스레드가 생성 될 때 생기며 어떤 스레드가 어디까지 실행되었는지 어떠한 명령을 실행할건지를 포인터와같이 주소로 저장해놓는 공간입니다.

 

JVM stacks

주로 스택이라고 불리는 이 영역은 스레드가 생성될 때마다 하나씩 생성되며 각 스레드들이 독립적으로 가지고있는 메모리 공간입니다. 또한 메소드가 호출될 때 이 스택에 쌓이며 지역변수 매개변수 임시변수등을 담고 있습니다.

 

String a = new String("jvm");

 

만약 어떠한 메소드 안에서 위처럼 코드를 선언하면 지역변수인 a는 스택에 저장되는 것이고 실제로 생성되는 new String("jvm") 이 스트링 객체는 heap 영역에 저장되는 것입니다. a가 새로운 스트링 객체를 참조하고 있으니 이는 GC의 대상이 되지 않습니다 (밑에서 더 자세히 설명)

 

이처럼 스택은 각 스레드마다 가지고 있는 메모리 공간이기 때문에 heap처럼 Thread-safe 문제를 신경쓰지 않아도 됩니다.

 

Heap

인스턴스가 생성되는 공간입니다. 모든 Object 타입(String, Integer, ArrayList등등)은 heap영역에 생성됩니다.  몇개의 스레드가 존재하든 단 하나의 heap영역이 존재하며 이는 모든 스레드에서 공유하는 공간입니다. Heap영역에 생성된 객체를 가리키는 레퍼런스 변수는 스택에 존재하는 것입니다 (위 예제의 String a 지역변수처럼). 가비지 컬렉션의 대상이 되는 공간은 Heap 영역입니다.

 

Method Areas

대표적으로 static 변수가 저장되는 공간입니다. 또한 클래스에 대한 정보(클래스 이름 클래스 메소드 클래스 변수등)가 저장되는 공간입니다. 이 공간 역시 Heap처럼 모든 스레드가 공유하는 공간입니다.

 

이제 Runtime Data Areas를 간략히 설명했으니 JVM의 동작 순서로 다시 돌아가서

 

4. 클래스 로더가 클래스(바이트코드)를 JVM내의 Runtime Dara Areas로 올립니다. 

5. 클래스 로더가 Runtime Data Areas로 클래스를 올리면 이 올라간 클래스파일(바이트코드)은 Execution Engine에 의해 실행됩니다. 이 과정에서 자바 바이트 코드를 JVM이 컴퓨터가 실행할 수 있는 형태로 변경하는 것입니다. 바이트 코드는 JVM이 이해할 수 있는 언어이고 이를 컴퓨터에서 실행시키기 위해서는 Native code로 변환해야하고 이 과정을 알려면 Execution Engine에 속한 JIT 컴파일러/인터프리터 에 대해 알아야 합니다.

 

인터프리터는 바이트 코드를 한줄한줄 읽으면서 OS가 실행할 수 있도록 기계어로 번역을 합니다. 초기 JVM은 인터프리터방식만 이용하여 한줄 한줄 읽기 때문에 실행속도가 느린 단점이 있었지만 JIT 컴파일러 방식을 통해 속도를 보완했습니다.

 

JIT 컴파일러는 자주 반복되는 코드를 기계어로 변환해서 캐싱을 해놓습니다. 이 캐싱된 기계어는 인터프리터가 해석을 하는 것이 아니라 JIT 컴파일러가 기계어로 변환해논걸 캐쉬에서 꺼내 바로 실행하니 빠른 것입니다. JIT 컴파일러와 인터프리터는 동시에 런타임 영역에서 다른스레드에서 실행됩니다.

 

JIT 컴파일러는 바이트코드를 nativecode로 바꾸기 때문에 실행이 빠르지만 nativecode로 변환하는데 비용이 발생합니다. 이런 변환 비용 때문에 jvm은 모든 코드를 JIT Compiler방식으로 실행하지 않고 인터프리터 방식을 사용하다 자주 사용되는 코드만 캐싱을하는 것입니다. JVM은 내부적으로 어떤 메서드가 얼마나 자주 수행되는지를 확인하고 HotSpot이라고 판단하면 컴파일을 수행해놓습니다.

 

이러한 과정을 통해 바이트코드는 JVM내부에서 컴퓨터가 실행할 수 있는 형태로 변경되어 실행되는 것입니다.

 

 

부록 : 인스턴스 생성시 일어나는 과정

- Heap에 어떤 Class object가 있는지 먼저 확인해야 합니다.

- Heap에 Class Object가 존재하지 않는다면 Class Object를 Heap에 생성 하고 해당 Class에 대한 Data(Class Data)를 Method Area에 저장한다.

- 이 후 JVM은 해당 클래스의 새로운 Instance를 Heap에 생성 하고 Method Area의 Class Data를 가리 킨다.

- 만약 해당 클래스 Object의 새로운 Instance가 생성된다면 Heap과 Method Area에 이미 해당 Class Object와 Class Data 가 생성되어있기 때문에 Heap에 새로운 클래스 Instance만 생성 하면 된다.

출처 mia-dahae.tistory.com/101