JVM이란 무엇일까?

Junyoung·2024년 8월 8일

Back

목록 보기
1/4

JAVA 언어와 Spring Boot FrameWork를 사용한지 2년이 다되갈 무렵 문득 내부 원리에 대한 기억이 희미해져간다...

컴파일 원리와 JVM의 내부 저장소와 동작 원리를 다시 한번 학습하고 넘어가자 !

우선 컴파일 원리를 위한 기본 지식으로


.java 원시코드 (java)

.class 바이트코드 (JVM이 이해하는 언어 | OS에 영향 받지 않는다.)

.jar 바이너리코드(기계어) ( CPU가 인식하는 코드이다.)

자바 코드의 3가지 상태를 정리하고 가자


바이트 코드란 → 가상 컴퓨터(VM) 에서 돌아가는 실행 프로그램을 위한 이진 표현법 !

즉 JVM이 인식하는 코드이다.

바이너리 코드 → 이진 코드이나 CPU가 인식하는 코드이다.

CPU 제조 회사마다 해석되는 코드가 조금씩 상이하다.


  1. Java Compiler

.java 파일을 .class 파일로 변환

JDK 설치시 bin에 존재하는 javac.exe이 Compiler이다.

javac test.java | 해당 명령어를 통해서 .java 파일을 .class 파일로 컴파일 한다.

  1. JVM

.class 파일을 Load 한다

JDK bin에 존재하는 java.exe는 JVM을 구동시키는 명령 프로그램

java test | 해당 명령어를 통해서 .class 파일을 실행 시킬 수 있다.

즉 JDK를 설치하면 컴파일러와 JVM이 모두 존재한다

JDK를 설치하면 두개의 exe을 확인할 수 있고 IDE를 활용해 컴파일하는 과정을 해당 kit을 이용하여 진행했던 것이다.


JIT 컴파일러(just-in-time compilation)

동적 번역이라고 한다.

프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일러 이다.


인터프리터 방식

인터프리터는 적절한 시점에 바이트 코드 전체를 바이너리로 바꿔놓기 때문에 이후에 인터프리팅 하지 않는다.

JAVA 언어는 인터프리터 언어이고 JIT를 통해서 동적으로 변경되는 코드를 감지하고 반영한다.


컴파일 과정

  1. 클래스 로더 (.class 파일을 로드한다.)
    • Loading - JVM 내부에 .class 파일들을 로드하고
    • Linking - 링킹을 통해서 파일들을 배치하고 .class 파일을 검증한다.
    • Initialization - 검증된 .class 파일들을 적절한 값으로 초기화한다.
  2. 실행 엔진 (.class 파일을 실행한다.)
    • 인터프리터 - 실행 엔진은 바이트 코드를 명령어 단위로 읽어서 실행
    • JIT 컴파일러 - 인터프리터 방식으로 실행하다 적절한 시점에 인터프리팅을 지속하는 방식이다.
    • 가비지 콜렉터 - 사용되지 않는 인스턴스(Un-Reached)를 찾아 메모리에서 삭제

JVM 메모리 영역

Runtime Data Area (위 사각형 공간)

프로그램을 수행하기 위해 OS에서 할당받은 메모리 공간

Thread

  • PC Register

Thread가 시작될 때 생성되며 생성 될때마다 만들어지는 공간

스레드 별로 하나씩 존재한다.

결국 Java는 하나의 프로세스의 개념이 되는것

  • JVM Stack 영역

프로그램 실행과정에서 임시로 할당되었다가 메소드를 빠져나가면 바로 소멸되는 특성의 데이터를 저장하기 위함

각종 형태의 변수나 임시 데이터, 스레드나 메소드의 정보를 저장

  • Native Method Stack

자바 프로그램이 컴파일되어 생성된 기계어(바이너리 코드)로 작성된 프로그램을 실행 시키는 영역

Method Area (class area , static area)

클래스 정보를 처음 메모리에 올릴때 초기화 되는 대상을 저장해준다

  • Runtime Constant Pool

스태틱 영역에 존재하는 별도의 관리 영역

상수 자료형을 저장하여 참조하고 중복을 막는 역할

객체를 저장하는 가상메모리 공간.

new 연산자로 생성되는 객체와 배열을 저장한다.

이때 위의 Method Area에 정의된 class만 객체로 생성이 가능하다

  • Metaspace (Permanent Generation)

영구적인 세대

생성된 객체들의 정보의 주소값이 저장된 공간

클래스 로더에 의해서 저장된(Method Area)에 있는 class, Method 등 Meta 정보(래퍼런스)를 저장해두는 공간이다.

포인터, 래퍼런스를 사용하는 방법은 Reflection 기법이라고 한다.

  • New/Young

젊은 영역

추후 가비지 콜렉터에 의해 사라지는 영역이다.

new 연산자로 인해서 생성되는 객체들을 저장해놓고 사용한다.

New/Young 영역에서 발생하는 GC를 “Minor GC” 라고 한다.

  • Old

추후 가비지 콜렉터에 의해 사라지는 영역이나, 생명주기가 긴 “오래된 객체“를 가지고 있다.

젊은 영역과 같이 생성되는 객체들을 저장해둔다.

Old 영역에서 발생하는 GC를 “Major GC” 라고 한다.


JDK

Java Development Kit

JRE 를 포함하고 있는 Kit로

JAVA를 사용하기 위한 모든 기능을 갖춘 SDK이다.

JRE

Java Runtime Enviroment

JVM + 자바 클래스 라이브러리로 구성 되어있으며,

컴파일된 Java 프로그램을 실행하는데 필요한 패키지이다.


저장소에 대해서 학습하다 보니 문득 클래스 변수의 생명주기와 변수의 동시성 관리에 대해서 궁금해졌다.

그럼 예제를 통해서 직접 테스트 해보자

public class SharedObjectExample {
    private int counter = 0; // 객체가 생성될 때 초기화

    public void increment() {
        counter++;
    }

    public int getCounter() {
        return counter;
    }

    public static void main(String[] args) {
        SharedObjectExample obj = new SharedObjectExample();

        // 스레드 1: counter를 증가시키는 작업
        Thread incrementThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                obj.increment();
            }
        });

        // 스레드 2: counter를 0으로 초기화
        Thread resetThread = new Thread(() -> {
            obj.counter = 0;
        });

        // 실행
        incrementThread.start();
        resetThread.start();

        try {
            incrementThread.join();
            resetThread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 최종 카운터 값 출력
        System.out.println("Final counter value: " + obj.getCounter());
    }
}

두개의 스레드를 순차적으로 실행시켰고

1번 스레드의 결과는 5

2번 스레드의 결과는 0이다.

이처럼 자바는 하나의 객체에 변수를 공유 자원으로 사용한다.

이런 자원을 동시에 접근할때 발생하는 문제가, 동시성 문제이다...

여기서 두가지 의문이 생긴다

  1. 그러면 for문에서 공통적으로 사용하는 for(int i=0...)
    변수 i도 공유하나?

  2. 그러면 static 변수를 왜 사용하나?

1번의 대답은 No 이다. class 레벨의 변수들만 객체와 함께 살아있고 메소드의 변수들은 로컬 변수로 스레드에서 항당받은 공간에 존재하기에 스레드간 이동이 불가하기 때문이다.

2번의 대답은 Spring을 사용하기에 보통 컨텍스트에 존재하는 객체에 접근하기에 static 처럼 사용했지만, 해당 객체의 변수의 주도권은 객체이다, 하지만 static 변수는 시스템 단계로 주체 또한 시스템이기에 다른 부분이다...


  • 결론
    언제나 근본은 중요하다. 항상 에러와 문제는 다 근본적인 부분에서 발생하기 때문이다 JVM에 대해서 공부하는 좋은 기회였고 동시성에 대한 궁금증이 커졌다.
    다음 포스팅에서는 동시성에 대해서 예제를 만들고 Test해보며 학습해볼 예정이다.

참조
[JAVA] JVM이란? 개념 및 구조 (JDK, JRE, JIT, 가비지 콜렉터...)

profile
라곰

0개의 댓글