TIL

  • 자바 코드 실행 과정
  • JVM 구성 요소
  • JVM 메모리 구조
  • JVM이 실행 클래스를 실행하는 방법

 

 

 


 

 

 

자바 코드 실행 과정

1. 자바 프로그램을 실행하면 JVM은 OS로부터 메모리를 할당 받는다.

2. 자바 컴파일러를 통해 *.java 파일을 바이트 코드(*.class)로 컴파일한다.

(*.class 파일은 기계가 바로 수행할 수 있는 상태는 아니고 가상머신이 이해할 수 있는 중간 레벨로 컴파일 된 코드이다.)

3. 컴파일 된 바이트 코드를 JVM의 클래스 로더로 전달한다.

4. 클래스 로더는 동적 로딩을 통해 필요한 클래스들을 로딩, 링크하여 런타임 데이터 영역(JVM의 메모리)에 올린다.

5. 실행 엔진은 JVM 메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행한다.

 

 

 

JVM 구성 요소

1. 클래스 로더

2. 실행 엔진 (인터프리터, JIT 컴파일러, 가비지 콜렉터)

3. 런타임 데이터 영역 (메소드 영역, 힙 영역, PC 레지스터, 스택 영역, 네이티브 메소드 영역)

 

 

 

JVM 메모리 구조

메모리의 구조에 대해 잘 알고 있어야 한다. 메모리는 개발자가 프로그래밍을 할 때, 고려하고 사용할 수 있는 자원의 거의 대부분이기 때문이다. 우선 메모리를 아주 큰 1차원 배열이라고 인지하자. 객체를 생성하면 메모리 공간에서 이 객체가 사용할 영역을 확정 짓는다. 내가 객체를 할당할 때, 이 객체가 어느 영역에 어떻게 선언이 되는지 알게 되면 내가 실제로 쓸 수 있는 자원을 어디까지 사용할 수 있을지 이런 부분들을 판단할 수 있게 된다.

 

 

  • Method area (static) : JVM에서 읽어들인 클래스와 인터페이스에 대한 런타임 상수 풀, 메소드, 변수, static 변수 정보 등이 저장되는 영역이다. 물론 바이트 코드로 저장된다. 모든 스레드가 공유한다.
  • Runtime Constant Pool : 메소드 영역에 포함 되지만 독자적 중요성을 띈다. 클래스와 인터페이스 상수, 메소드와 변수에 대한 모든 레퍼런스를 저장한다. JVM은 런타임 상수 풀을 통해 해당 메소드나 필드의 실제 메모리 상 주소를 찾아 참조한다.
  • Heap area : new 키워드로 생성 된 배열, 인스턴스, 객체가 저장되는 영역이다. GC 이슈는 이 영역에서 일어난다. 모든 스레드가 공유한다.
  • Stack area : 메소드 내에서 사용되는 값들(매개변수, 지역변수, 리턴값 등)이 저장되는 구역이다. 메소드가 호출될 때 하나씩 생성되고 메소드 실행이 완료 되면 하나씩 지워진다. 각 스레드별로 하나씩 생성된다.
  • PC(Program Counter) register : 자바는 현재 작업하는 내용을 CPU에게 연산으로 제공해야 하며, 이를 위한 버퍼 공간으로 PC 레지스터라는 메모리 영역을 만든다. PC 레지스터는 스레드가 시작될 때, 각 스레드별로 하나씩 생성된다. JVM은 멀티 스레드 환경이기 때문에 스레드간 전환이 일어날 때, 각 스레드의 상태를 저장해줄 필요가 있다. JVM은 스택에서 operand를 뽑아 별도의 메모리 공간인 PC 레지스터에 저장하는 방식을 취한다. 만약 스레드가 자바 메소드를 수행하고 있다면 JVM 명령 주소를 PC 레지스터에 저장한다. 그러다 자바가 아닌 다른 언어의 메소드를 수행하고 있다면 undefined 상태가 된다.
  • Native Method Stack : 다른 언어(C, C++ 등)의 메소드 호출을 위해 할당되는 구역으로 언어에 맞게 stack이 형성되는 영역이다.

 

 

 

Heap area

힙 영역은 모든 스레드들이 공유하는 영역이다. 이 영역에는 new 연산자로 생성되는 클래스와 인스턴스 변수, 배열 타입 등 reference type이 저장된다. 또 JVM은 heap 영역에 메모리를 할당하는 instruction(바이트코드로 new, newarray, anewarray, multinewarray)만 존재하고 메모리 해제를 위한 어떤 자바코드나 바이트코드가 존재하지 않는다. java 힙 영역의 메모리 해제는 오직 Garbage Collection을 통해서만 수행된다.

 

 

  • Young Generation : Eden 영역과 Survior 영역. (survior 영역에 0, 1을 명시했다고 해서 어떤 우선순위가 있는 것은 아니다.) 이곳에서 발생하는 GC를 Minor GC라고 한다. Minor GC가 발생할 때마다 살아남은 객체의 age가 증가하는데, 기록된 age가 임계치를 넘은 객체는 old 영역으로 이동한다.
    • Eden 영역 : object가 heap에 최초로 할당되는 장소이다. eden 영역이 꽉 차면 살아남은 객체는 survior 영역 중 하나로 넘어가고 참조가 끊어진 쓰레기 객체면 그냥 eden 영역에 남겨 놓는다. 살아있는 모든 객체가 survior 영역으로 넘어가면 eden 영역을 모두 청소한다.
    • Survior 영역 : eden 영역에서 살아남은 object들이 잠시 머무르는 곳

 

  • Old Generation : young generation에서 일정 age가 될 때까지 살아남은 객체로 오래 살아남아 성숙된 객체가 이 영역에 옮겨진다. old 영역이 가득차면 Major GC(Full GC)가 발생한다. Major GC는 Minor GC보다 훨씬 느리다. 왜냐하면 young 영역을 제외한 모든 살아남은 객체를 검사해야하기 때문이다.

 

  • Perm : 보통 class의 메타 정보, method의 메타 정보, static 변수와 상수 정보들이 저장되는 공간이다. 흔히 메타데이터 저장 영역이라고도 한다. 만약 적재된 클래스의 메타데이터가 더이상 필요없고 새로운 클래스를 위한 perm 영역의 크기가 모자르면 GC는 적재된 클래스를 수집한다. perm 영역의 GC는 Full GC이다. perm 영역은 Java8부터는 Native 영역으로 이동하여 Metaspace 영역으로 변경되었다. (다만, 기존 Perm 영역에 존재하던 static 객체는 heap 영역으로 옮겨져서 GC 의 대상이 최대한 될 수 있도록 하였다.)

 

 

 

Execution Engine

실행 엔진은 <클래스 로더>를 통해 <런타임 데이터 영역>에 배치된 바이트 코드를 명령어 단위로 읽어서 실행한다. 자바의 바이트 코드(*.class)는 기계가 바로 수행할 수 있는 상태는 아니고 가상머신이 이해할 수 있는 중간 레벨로 컴파일 된 코드이다. 실행 엔진은 이와 같은 바이트 코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 상태로 변경해준다. 실행 엔진은 인터프리터와 JIT 컴파일러 2가지 방식을 혼합하여 바이트 코드를 실행한다.

 

  • Interpreter : 코드를 한 줄씩 읽고 실행한다. 한 줄씩 실행하기 때문에 비교적 느리다. 한 메소드가 여러번 호출될 땐 매번 해석하고 수행하기 때문에 속도가 느려진다.

 

  • JIT Compiler : 실행 엔진은 바이트 코드를 실행하기 위해 먼저 인터프리터를 사용하지만 반복되는 코드를 발견하면 JIT 컴파일러를 사용한다. JIT 컴파일러는 전체 바이트 코드를 컴파일하고 native code 기계어로 번역한다. 기계어는 반복되는 메소드 호출에 직접 사용되어 시스템의 성능을 향상시킨다. 
     
    • JIT 컴파일러 구성요소
      1. Intermediate Code Generator : 중간 코드 생성
      2. Code Optimizer : 중간코드 최적화
      3. Target Code Generator : 중간코드를 기계어로 변환
      4. Profiler : 핫스팟(반복적으로 실행되는 코드) 찾기

  • GC는 따로 포스팅

 

 

 

Native method Interface (Java Native Interface)

  • 하드웨어와 상호작용하거나 Java의 메모리 관리 및 성능 제약을 극복해야 하는 경우 자바가 아닌 네이티브 코드(예: C,C++)를 사용해야될 수 있음
  • 자바는 JNI를 통해 네이티브 코드 실행을 지원
  • C, C++ 등 다른 프로그래밍 언어에 대한 패키지를 지원하는 다리 역할
  • Native Keyword를 사용한 메소드 호출
  • System.loadLibrary()

 

 

 

Native Method Libraries

  • C, C++, 어셈블리와 같은 다른 프로그래밍 언어로 작성된 라이브러리
  • .dll 또는 .so 파일 형식으로 제공
  • JNI를 통해 로드

 

 

 

JVM은 실행클래스를 어떻게 실행할까?

public class example {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;

        Example ex = new Example();
        int sum = ex.add(a, b);
        System.out.println(sum);
    }

    public int add(int a, int b) {
        int sum = a+b;
        return sum;
    }
}

JVM이 Example.class를 실행하는 절차를 알아보자.

  1. 해당 클래스를 현재 디렉토리에서 찾는다.
  2. 클래스를 찾으면 클래스 내부에 있는 static 키워드가 있는 메소드를 JVM 메모리 영역 중 method area의 static zone에 로딩한다.
  3. static zone에서 main() 메소드를 실행한다. main() 메소드가 호출되면 main() 메소드의 호출정보가 stack area에 들어간다.(push) 이 프레임 안에 main() 메소드의 매개변수, 지역변수, 리턴값 등이 저장된다.
  4. new 연산자로 Example 객체를 생성하면 heap area에 객체가 생성된다. Example 객체 안에 있는 add() 메소드가 생성되는데 사실 method area의 non-static zone에 add() 메소드의 바이트 코드가 생성되는 것이고 heap area에 있는 Example 객체의 add() 메소드는 저 바이트코드의 위치를 가리키고 있는 것이다.
  5. ex 변수 안에 heap area에 생성된 Example 객체 주소가 저장된다.
  6. ex.add() 메소드 호출에 의해 add() 메소드가 stack area에 생성된다. 이때 main() 메소드를 가리키던 PC(program counter)가 add() 메소드를 가리키면서 제어권이 이동한다.
  7. 메소드 내 코드가 다 실행되면 stack area에서 사라진다.
  8. PC가 다시 main() 메소드를 가리키게 된다.
  9. main() 메소드 내 코드도 다 실행되면 stack area에서 사라진다.
  10. stack area가 비어있으면 프로그램이 종료된 것이다.

📚 참고
Java TPC (생각하고, 표현하고, 코딩하고)
Java Garbage Collection Basics
[JAVA] JVM 구조 및 자바 프로그램 실행 과정