[java] 자바 실행 과정 deep dive

2021. 8. 24. 18:49[ 백엔드 개발 ]/[ Java,Kotlin ]

자바는 OS와 독립적으로 실행시킬 수 있다. 그 이유는 JVM이 OS에 의존적이기 때문인데, 즉, OS마다 다른 JVM이 존재하기 때문이다. C/C++는 링커, 로더를 포함한 운영체제 바로 위에서 직접적으로 실행되므로, 빠르고 가볍다. 하지만 개발에 있어서 그만큼 메모리 회수 등 개발자가 신경써야 할 부분이 많다. 또한 C/C++ 등의 전통적인 언어는 컴파일 플랫폼(CPU 아키텍처 16bit, 64bit 등 및 OS)에 따라 자료형형의 크기가 변한다. 그래서 윈도우에서 컴파일한 C/C++파일은 리눅스에서 안 돌갈 수도 있다.

 

JVM은 이러한 문제를 근본적으로 해결한다. 자바 소스코드(.java)가 자바 컴파일러(javac.exe)를 거치고 나면, 자바 바이트코드(.class)를 생성하는데, 이 자바 바이트코드는 JVM이 설치된 플랫폼이라면 어떤 플랫폼이든 상관없이 잘 동작한다. 즉, JVM은 OS, 컴퓨터 아키텍처 등을 포함한 플랫폼과는 의존적이지만 자바 소스코드(더 면밀히 말하면 바이트코드)와는 의존성을 근본적으로 없애서 자바와 컴퓨터시스템 간의 의존성을 없앤다. 또한 JVM은 메모리 공간을 효율적으로 관리해 주는 가비지 컬렉터와 같은 편리한 기능을 제공함으로써 개발자가 보다 논리적인 개발에 집중할 수 있도록 도와준다.

 

cf. WORA(Write Once, Run Anywhere) - Sun Microsystems(초기 자바 만든 회사)

자바 개발자가 작성한 자바코드는 어떤 플랫폼이든 다시 컴파일할 필요 없이 실행시킬 수 있다. 하지만, 실행시키려면 그 플랫폼에 맞는 JVM을 설치해야 한다.

 

cf. 크로스 컴파일(Cross Compile) : 타겟 플랫폼에 맞춰 컴파일하는 것. 예를 들어, 윈도우에서 리눅스를 타겟으로 맞춰 컴파일할 수 있게 해 주는 것.

 

 

왜 JVM이 사용되나?

  • 재컴파일(Recompilation)을 방지하기 위함이다.
  •  다양한 OS에서 프로그램을 구동시키기 위해 응용프로그래머가 기기마다 다르게 프로그램을 만들고 컴파일해야 한다면 비효율적일 것이다.
  •  JVM이 없다면 하나의 기능을 수행하는 프로그램에 대해 OS마다 재컴파일되어야 한다. 즉, 근본적인 기능은 똑같은데 하드웨어 아키텍처, OS가 다르다는 이유로 다른 기계어로 바꿔주는 컴파일러를 사용해야 한다.
  •  JVM은 이러한 문제를 해결하기 위해 등장했다. 컴파일한 파일(.class)과 하드웨어 사이에 JVM이라는 가상 머신을 두어서, 응용프로그래머는 해당 기기의 OS에 맞는 JVM을 설치하고 항상 같은 자바 코드를 작성할 수 있도록 한다.

 

자바 실행 과정

https://hoonmaro.tistory.com/19

자바 실행 과정은 다음과 같다.

 

1) 작성한 자바 파일은 .java 확장자를 갖는다. 자바 프로그램 실행시, JVM은 OS로부터 메모리를 할당 받는다.

 

2) JDK의 자바 컴파일러(javac.exe)가 자바 소스파일(.java)을 컴파일한다. 이때 생성되는 파일은 자바와 기계어 사이의 중간 언어인 자바 바이트코드(.class)로 아직 컴퓨터 기계(CPU)가 읽을 수 없는 파일이며, .class 파일은 JVM(SW)이 읽는 코드다.

 

바이트코드의 존재는 자바 언어의 가장 큰 특징이라고 할 수 있는데, 특정 하드웨어(OS포함)가 아닌 가상 컴퓨터(JVM)에서 돌아가는 실행 프로그램을 위한 이진 표현법이다. 바이트코드의 존재 이유는 하드웨어와 프로그래밍 언어의 의존성을 제거하기 위함에 있다. 즉, 컴퓨터 아키텍처마다 다르게 처리해 줘야 하는 부분을 JVM이라는 소프트웨어에 의해 처리되도록 구성한 것이 바이트코드이고 소프트웨어에 의해 처리되기 때문에 기계어보다 추상적이다. 이로써 자바 개발자는 플랫폼(OS)에 종속적이지 않은 개발을 할 수 있으며 JVM이 설치된 컴퓨터라면 항상 똑같은 바이트코드를 어디에서든지 실행시킬 수 있다.

 

자바 바이트코드는 Opcode와 피연산자로 이루어진다.

- 바이트코드 생성 명령어 : javac {fileName}.java

- 바이트코드의 명령어(Opcode)가 1 byte여서 바이트코드라는 이름을 갖는다. 


3) 컴파일된 바이트코드(.class)는 JVM의 클래스 로더에게 전달된다.

 

4) 클래스 로더는 런타임 중에 '동적로딩(Dynamic Loading)'을 통해 필요한 클래스들을 로딩 및 링크하여 JVM의 메모리(JVM이 운영체제로부터 할당받는 메모리 영역)에 올린다.

- 명령어 : java {fileName}.class

 

5) JVM의 Execution Engine은 JVM 메모리에 올라온 바이트코드들을 명령어 단위로 하나씩 가져와서 기계어로 변환시킨다. 변환된 기계어는 JVM의 메모리구조(Runtime Data Area)의 각 영역에 배치되며 GC(Execution Engine에 포함)와 함께 실행된다. 

 

 

cf. 컴파일 언어와 인터프리터 언어

1) 컴파일언어:

- 프로그래밍 언어가 바로 기계어로 변환되어 기계(CPU)에 전달되어 처리되는 언어

 

2) 인터프리터언어:

- 작성된 프로그램이 실행 중에 동적으로 기계어로 변환되어 머신에 전달되고 실행됨

- 명령어를 하나씩 실행시키는 속도는 빠르지만 전체적인 코드를 싱행시키는 속도는 한번에 컴파일 후 실행시키는 컴파일러보다 느릴 수 있음.

- 한줄씩 읽고 실행하기 때문에 별도 실행파일이 생성되지 않음

 

 

[자바에서의 실행 방식: JIT(Just In Time) 방식]

https://blog.siner.io/2021/12/25/java-compile/

 

1) javac가 .java 파일을 컴파일하여 class 파일(중간언어, 기계언어 아님, JVM입장에선 기계어)을 생성한다.

    - 이 과정은 컴파일 방식이기 때문에 모든 java 파일이 실행전에 모두 class로 한 번에 전환된다.

2) 생성된 class파일을 프로그램 실행 시에 인터프리터 방식이 적용된다.

    - 이 과정은 JVM의 클래스 로더가 JVM 메모리로 class 파일을 올리고

    - JVM의 Excution Engine이 JVM 메모리로 올라온 class 파일을 기계어로 변환한 뒤 CPU에서 처리되는 방식이다.

    - 이미 기계어로 변환된 class 파일이 있다면(캐싱되어 있다면) 캐싱된 데이터를 활용한다.

 

 

* 결국 Java는 컴파일 방식과 인터프리터 방식을 모두 활용하는 언어라고 볼 수 있다. (이를 JIT 방식이라고 함)

 

 


Class Loader

JVM의 클래스 로더는 컴파일타임이 아니라 런타임에 클래스(.class)를 처음으로 참조할 때 해당 클래스를 JVM이 할당받은 메모리에 로드하고 링크하는 역할을 한다. 또한 초기에는 static 값에 대해 생성/초기화하는 역할도 수행한다. 클래스 로더는 아래 그림과 같이 계층 구조를 갖는다. 또한 자식 클래스 로더는 부모 클래스 로더에게 역할을 위임하며, 부모 클래스 로더가 해당 클래스를 찾는데 실패시, 자식 클래스 로더에게 요청을 넘긴다.

 

https://engkimbs.tistory.com/606

 

예를들어, 특정 클래스를 로딩하는 과정을 살펴보면 먼저 가장 아래 단계인 User-defined 클래스 로더에서 해당 클래스를 찾고 없을 경우, 그 윗 단계로 올라가서 찾는다. 주의할 것은 클래스 로딩은 동적으로 이루어진다는 것이다. 즉, 프로세스 실행 중에 특정 클래스가 참조되면 클래스를 파일(.class)로 부터 로딩해서 JVM의 메소드 영역으로 올리는데, 이 과정이 필요시에 따라 동적으로 발생한다는 것이다. 따라서 이러한 특징을 활용하여 서버 재시작 없이 프로그램을 동적으로 변경하는 Hot Deploy 개념으로도 활용할 수 있다.

 

cf. 클래스 로딩의 구체적인 예

public class A extends B {
}

A 클래스를 로딩하는 시점에서 B 클래스가 로딩되어 있지 않다면 A 클래스를 로드할 수 없다. 따라서 B 클래스가 로딩되어 있는지 먼저 확인하는데 이는 부모 클래스 로더에서 위임되는 방식으로 진행된다. 이때 부모 클래스 로더가 모두 B 클래스를 가지고 있지 않을 때, 현재 클래스 로더가 B 클래스를 로드할 수 있는 상황(B.class 파일을 가지고 있음)이라면 현재 클래스 로더가 직접 B 클래스를 로드한다.

 

웹 애플리케이션 서버(WAS)와 같은 프레임워크는 웹 애플리케이션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게 하기 위해 사용자 정의 클래스 로더를 사용한다.(최하위에 위치) 즉, 클래스 로더의 위임 모델을 통해 WAS에서 java.lang.ClassLoader를 구현하는 클래스로더를 추가로 둔다. 이와 같은 WAS의 클래스 로더 구조는 WAS 벤더사마다 조금씩 다른 형태의 계층 구조를 사용하고 있다. WAS는 Servlet Specification(서블릿 사양)을 구현하기 때문에 서블릿 명세사항을 확인하면 찾아볼 수 있다.

 

이를 통해 container를 구성하는 다양한 클래스와, container에서 구동되는 Web Application들이 다양한 클래스에 접근할 수 있도록한다. 이러한 매커니즘은 Servlet Specification에서 정의한 기능들을 WAS에서 제공되게 하기 위해 사용된다. 

 

 

https://dzone.com/articles/jvm-architecture-explained

 

 

Reference

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

- https://tomcat.apache.org/tomcat-10.0-doc/class-loader-howto.html

- https://dzone.com/articles/jvm-architecture-explained

- https://www.sciencedirect.com/topics/computer-science/execution-engine

https://jh-labs.tistory.com/28

- https://ko.wikipedia.org/wiki/JIT_%EC%BB%B4%ED%8C%8C%EC%9D%BC