[F-Lab 모각코 챌린지 4일차] JVM

부추·2023년 6월 4일
0

F-Lab 모각코 챌린지

목록 보기
4/66

TIL

  1. 자바 소스코드부터 JVM 구동까지의 전반적인 과정
  2. Runtime Memory, Class Loader, Executor Engine 등의 JVM 내부 구조

1. 자바의 특징

1) 자바는 객체지향의 정신을 언어로 녹여냈다.

객체지향이란, 프로그램을 객체의 상호작용으로 보는 패러다임을 말한다. '객체' 란 현실 세계의 물체를 닮아있으며, 객체 지향에서 객체는 상태값과 메소드를 가진다. 상태값은 말 그대로 특정 객체 인스턴스가 가지는 상태값이며, 메소드는 해당 객체가 할 수 있는 동작, 일을 정의한다.

객체 지향 프로그래밍에서는 이러한 객체를 정의하고, 객체의 상태값과 메소드를 이용해 일련의 기능을 수행하도록 한다. 자바는 객체를 class로 표현한다. 자바의 모든 소스코드는 이 클래스 안에 존재한다.

2) 자바는 JRE가 갖춰진 곳이라면 어디서든 동작한다.

개발자가 작성한 .java 소스코드 파일은 javac 컴파일러에 의해 .class 확장자를 가진 자바 바이트코드로 변환된다. 바이트코드의 타겟은 자바 바이트코드를 실행하는 기계 JVM이다. JRE(JVM + 라이브러리 파일들)가 갖춰졌다면 하나의 자바 바이트코드는 어디서든지 실행될 수 있다. 이는 OS나 아키텍처에 따라 다른 컴파일러를 사용해야하는 다른 컴파일 언어와 비교된다.

3) 자바의 메모리는 GC가 관리한다.

"new" 키워드를 통해 프로세스의 동적 메모리 공간인 힙에 객체 인스턴스를 생성할 수 있다. 프로그램이 동작하다보면 더이상 사용되지 않는 인스턴스가 발생하기 마련인데, 이때 인스턴스의 삭제를 개발자가 직접 하지 않고 자바의 Garbage Collector가 대신 진행한다. C계열 언어의 경우, 개발자가 malloc()을 통해 할당한 메모리는 직접 free() 시켜줘야한다.




2. 자바 실행 과정

개발자가 자바 소스코드를 작성한 뒤 실제로 자바 프로그램이 구동되기까지 어떤 과정이 일어날까?

  1. 개발자가 .java 확장자의 소스파일을 작성한다.
  2. 자바의 컴파일러 javac는 컴파일 타임에 소스파일을 컴파일하며 문법적 오류 등을 잡아낸다. 또한 소스코드가 다른 라이브러리 혹은 클래스를 사용할 경우 classpath를 통해 이를 검색한다.
  3. 컴파일 결과로 .class 확장자를 가진 byte code 파일이 만들어진다. 이는 JVM이 직접 실행할 수 있는 코드이다.
  4. 클래스 로드를 요청받은 클래스 로더가 .class 파일의 클래스들을 JVM의 메모리에 동적으로 로드한다.
  5. 실행 엔진(Executor Engine)이 바이트 코드에 있는 바이트코드를 실행한다. 일반적으로 인터프리팅 방식(바이트 코드를 한 줄씩 실행)을 사용하지만, 더 빠른 동작을 위해 JIT 방식을 사용하기도 한다.

1~3은 컴파일 타임, 4~5는 런타임에 일어나는 일이다. 이제 JVM의 각각의 요소에 대해 더 자세히 살펴보자!




3. Class Loader

클래스 로더는 자바 프로그램에 필요한 클래스 정보를 메모리에 올리는, 말 그대로 클래스를 로드하는 "동적 로드"를 사용한다. 자바 프로그램이 동작하는데 필요한 클래스와 그 정보들을 프로그램의 시작 전 한꺼번에 올리는 것이 아니라, 처음 필요한 상황이 발생할 때 메모리에 올리는 것이다.

특정 클래스 타입을 이용하는 코드를 만났을 때, 클래스 로더는 파일 시스템에서 .class 파일을 찾아 다음의 과정을 거친다.

  1. 로딩 : 클래스 파일을 메모리로 로드한다.
  2. 검증 : 로드한 클래스의 syntax와 구성에 오류가 없는지 검사한다.
  3. 준비 : 클래스가 필요로 하는 메모리를 할당하고 클래스 내부의 데이터를 저장하기 위한 구조를 준비한다.
  4. 분석 : 클래스의 심볼릭 레퍼런스를 실제 스태틱 영역의 레퍼런스 주소로 대체한다.
  5. 초기화 : 클래스의 static 블록을 실행시키며, static 키워드가 붙은 클래스 변수들을 적절한 값으로 초기화한다.

위 과정을 거쳐 초기화된 클래스는 JVM 런타임 메모리의 static 영역에 적재된다.


# 계층 구조

클래스 로더는 계층 구조를 가진다.

  • 부트스트랩 클래스 로더 : JVM이 구동될 때 가장 먼저 동작하는 클래스 로더로, JVM 부팅을 위한 최초의 클래스와 최소한의 자바 클래스(Object 등)들을 로드한다.
  • 확장 클래스 로더 : 자바의 기본 API를 제외한 확장 클래스들을 로드한다.
  • 시스템 클래스 로더 : 위 클래스 로더가 JRE 자체에서 필요한 클래스들을 로드한다면, 시스템 클래스 로더는 사용자가 작성한 어플리케이션 클래스를 로드한다.
  • 사용자 정의 클래스 로더 : 사용자가 직접 작성한 클래스로더이다.
    클래스 로더마다 다른 네임스페이스를 가지며, 같은 클래스 이름을 가지고 있더라도 클래스로더가 다르면 다른 클래스로 취급된다. 클래스로더가 요청을 받으면, 하위 클래스로더부터 시작해 위로 거슬러 올라가 요청받은 클래스가 존재하는지 확인한다.(위임 방식) 최상위 클래스로더까지 갔음에도 클래스가 보이지 않으면 그때 앞서 설명한 로딩~초기화 과정을 거친다.



4. Runtime Data Area

자바 프로세스가 시작하면 JVM은 OS에게 메모리를 할당받는다. 여기에 JVM이 사용하는 메모리 영역이 존재한다.

  • Static(Method) 영역 : JVM에서 동작하는 모든 스레드가 공유하는 영역이다. 클래스 로더가 로드한 클래스들이 적재된다. 클래스 내부의 static 변수, 메소드의 바이트코드 등이 존재한다.
  • Runtime Constant Pool : 레퍼런스를 담고있는 테이블. 클래스의 메소드나 변수를 참조해야할 때 실제 메모리 주소를 매칭해주는 중요한 역할을 담당한다.
  • Heap 영역 : new 키워드를 통해 생성된 클래스 인스턴스가 존재하는 영역이다. GC의 대상이 되며, 자바의 메모리 관련 성능에서 가장 크게 이슈가 되는 부분이다.
  • Stack 영역 : 자바에서 실행되는 스레드들의 영역. 각 스레드는 고유의 PC(Program Counter)와 스택을 가지고 일련의 동작을 수행한다. 실행 엔진은 각 스레드의 스택 프레임을 push/pop하며 자바 메소드를 수행한다.




5. Executor Engine

일반적인 컴파일링 언어는 소스코드가 컴파일러에 의해 object 파일로 변환되고, 이를 하드웨어가 직접 구동하는 방식을 따른다. 자바도 javac가 있는데 비슷한 거 아닌가? 싶을 수도 있는데, JVM은 실제 하드웨어가 아니라 "가상 머신"임을 상기해야한다. javac가 컴파일한 결과물은 JVM이 실행할 수 있는 파일이지, 실제 컴퓨터가 실행할 수 있는 파일은 아니다. 결국 실제로 명령어를 수행하는 것은 JVM이 설치된 하드웨어이다.

JVM이 실제 하드웨어를 이용해 자바 바이트코드를 실행하는 방식엔 크게 두 가지가 있다.

  1. 인터프리팅 방식 : .class 파일의 바이트코드들을 한 줄 씩 해석하여 실행하는 방식이다. 이는 직관적이지만 코드 줄 단위로 명령이 수행되기 때문에 느리다는 단점이 있다.
  2. JIT 컴파일링 방식 : 인터프리팅 방식으로 동작하다, 메소드 코드 등 반복적으로 사용하는 바이트 코드들을 JIT 컴파일러를 이용해 한 번에 네이티브 코드(실제 하드웨어가 이용)로 컴파일링하는 방식이다.

JIT(Just-In-Time) 컴파일러를 이용하면 실제 하드웨어가 이용하는 코드를 통해 기기를 동작시키고, 캐시를 이용할 수 있어 인터프리팅 방식보다 빠르게 프로그램을 돌릴 수 있다.

하지만 JIT의 컴파일링 시간 자체도 오래 걸리므로 내부적으로 바이트코드가 얼마나 자주 사용되는지 체크하여 일정 정도를 넘을 때만 컴파일링하는 것이 효율적이다. 한두 번만 사용되는 코드라면 인터프리팅 방식이 더 빠르기 때문이다.



REFERENCE

https://d2.naver.com/helloworld/1230

https://yozm.wishket.com/magazine/detail/1979/

https://steady-coding.tistory.com/593

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글