지나공 : 지식을 나누는 공간

Java 안에서의 Interpreter와 Compiler 본문

Tech Interview

Java 안에서의 Interpreter와 Compiler

해리리_ 2023. 2. 26. 17:01

들어가기


직렬화 객체에 빈 생성자를 넣지 않았다가 Feign으로 외부 API 호출 시 에러가 났다. 그래서 직렬화 객체는 왜 빈 생성자가 필요한지 알아보려했다. 그러다보니 Reflection을 알아야 했고, 그러다보니 Java의 클래스가 로드되는 과정을 알아야 했으며, 그걸 보다 보니 Java의 소스 코드가 실행되는 과정을 알아야 했다. 예전에는 단순히 컴파일러와 인터프리터를 비교하고, 자바는 이 둘 다가 같이 쓰인다는 것까지만 이해했는데, 그보다 더 많은 내용이 있었다.

 

본래 알고 싶었던 빈 생성자와 reflection에 대해서는 다음으로 넘긴다ㅠ 나 분명 reflection 공부하려 했는데ㅠㅠㅠ 항상 공부하다보면 판도라의 상자 마냥 .. 모르는 게 많아서 끝도 없이 다른 주제로 가지가 쳐진다...^_^... 내 주말 내놔.....

 

용어 정리


인터프리터와 컴파일러를 보기 전에 용어를 정리해봤다. 일단 읽지 말고 넘어갔다가 필요할 때 읽는 것을 추천!

  • Source code : 인간이 읽을 수 있는 코드
  • High-level code : 자바, C 같은 프로그래밍 언어
  • Assembly code : low level 언어의 일종인 어셈블리어로 작성된 코드
  • Object code : 컴파일 과정의 결과물로, machine code의 형태이기도 하고 byte code의 형태이기도 하다.
  • Machine code : 기계어로 된 코드
  • Byte code : JVM 같은 인터프리터에 의해 실행될 수 있는 intermediate 중간 형태의 코드
  • Executable file : 링크 과정을 하고 난 결과물. CPU에 의해 바로 실행될 수 있는 상태의 machine code.
  • Library file : 일부 코드는 재사용 가능성과 같은 여러가지 이유로 library file로 컴파일이 되었다가 executable file에서 사용된다.

 

Interpreter와 Compiler


Interpreter는 direct하게 soruce code를 실행하는 것이다.

  • 실행하기 전 machine code로 변환하여 별도 파일로 작성하는 컴파일 과정 없이 소스 코드를 번역한 뒤 실행까지 같이한다.
  • 학부 때 line-by-line으로 실행한다고 배웠다. 일단 프로그램을 실행부터 시키고, run time 시점에 각 코드를 한 줄씩 변환해서 메모리에 로드한 뒤 실행한다. 변환과 실행을 같이 하니까 속도가 느리다.
  • 어찌 보면 컴파일러를 내장하고 있다.

Interpreter

Compiler는 소스 코드를  다른 언어로 변환하고 그걸 Object Code. 목적 파일에 써놓았다가 run time 시에 실행 시킨다.

  • 전체 소스 코드를 한번에 컴파일하기 때문에 컴파일하는 속도는 오래 걸리지만 이미 여기서 기계어로 다 변환해서 실행용 파일을 로드해 놓기 때문에 실행 속도는 빠르다.

Compiler

 

Java의 JIT Compiler와 Interpreter, Compiler


자바에는 세 가지가 모두 있다.

 

자바는 본래 인터프리터만 사용했었는데, 인터프리터의 경우 명령어를 하나씩 실행하는 방식이라서 각각의 명령어 단위로 보면 실행 속도가 빠르지만 전체로 볼 때는 라인별 수행이다보니 중복 코드를 또 인터프리팅하기도 해서 느리다는 문제가 있었다. 그래서 Compiler와 JVM 내 JIT Compiler, Interpreter를 두어서 java는 적절히 위 단점을 보완했다.

 

그렇다면 런타임에 들어왔을 땐 이미 컴파일이 다 됐으니 실행만 하면 되는 거 같은데,
이미 번역이 다 된 상태에서 Java Interpreter / JIT Compiler가 하는 역할은 뭘까?

라는 의문이 들었다.

 

그전에 JVM의 구조와 Java의 동작을 살펴보자.

JVM 구조

 

Java 동작 과정


1) a1.java 안에 f1(), f2()가 호출되고 있고, 각 메소드는 a2.java와 a3.java 안에 정의되어 있다고 가정하자.

2)

2) 컴파일러가 세 가지 파일을 모두 컴파일해서 각 파일 별로 .class 파일을 만들어 낸다. 각 .class 파일들은 Byte Code로 이루어져 있다. C에서는 RAM에 로드하기 전에 compiler 이외에 linker까지 거쳐서 linking이 이루어진 exe 파일을 RAM에 로드하는데, JAVA에서는 바이트 코드로 이뤄진 .class 상태로 RAM에 로드한다.

3)

3) JVM은 RAM에 상주하고 있고, 프로그램을 실행하면서 run time에 들어가면 JVM의 Class Loader를 통해서 .class 파일을 RAM으로 로드한다. bype 코드는 보안 사항에 대해 검증된다.

4)

4) 실행엔진이 byte code (.class)를 Native machine code로 변환한다. VM 구조를 보면 알겠지만, 자바는 JIT Compiler를 사용한다. JIT란 Just In Time, 즉 동적 번역을 의미하고, 프로그램을 실제 실행하는 시점에 기계어로 번역하는 컴파일 기법이다. Object Code인 byte code 상태로 만들어서 RAM에 로드한 뒤에 프로그램 실행 시점에 진짜 machine code로 번역하는 것이다. 

 

여기서!! 일단 compile time에 이뤄지는 과정을 알겠고, Run time 시 JVM 내에서 Interpreter와 JIT Compiler가 하는 일을 자세히 알아보자.

 

참고용 : C 동작 과정 펼치기

더보기
1)

1) main 함수는 a1.c에 있고, 그 내부에서 호출하는 f1(), f2()는 각각 a2.c 와 a3.c에 정의되어 있다.

2)

2) 위 파일들은 모두 컴파일러에게 제공되고, 컴파일러는 각 파일들을 machine code를 포함하는 각각의 Object Code로 변환한다.

3)

3) 그 다음 Linker를 통해 이 모든 object file들을 단일의 .exe 파일로 통합 생성한다.

4)

4) 프로그램이 실행되는 동안 loader가 .exe 파일을 RAM에 로드한다.

JVM에 로드된 이후 과정


1) 이 상태에서 시작

1)

자바 컴파일러에 의해 byte code가 생성됐고, class loader에 의해 RAM에 상주한 JVM의 메모리 상으로 byte code가 로드됐다.

 

 

2) 인터프리터가 byte code를 한줄씩 machine code로 변환 후 실행

2)

JVM 내 Interpreter는 byte code를 line-by-line으로 읽는다. 첫 줄을 읽고 machine code로 만든 다음 이걸 실행하고, 둘째 줄을 읽고 machine code로 만든 다음 또다시 이걸 실행한다.

  • 다른 언어에서는 컴파일 과정 없이 source code가 직접 interpreted 되는데, Java에서의 Interpreter는 source code를 다루지 않고 컴파일된 코드나 중간코드(intermediate code)를 다룬다. 이게 차이임!

 

3) 중복된 코드가 있는 상태

3)

JUST-IN-TIME Compiler 가 이제 등장한다. 위에서 세 줄의 코드가 중복된 상태다. 아래 코드가 intermediate 코드라고 해보자.

여기서 프로그램이 실행되면 interpreter는 또다시 line by line으로 byte code를 읽고, 컴퓨터는 해당 코드가 번역되는 즉시 바로 실행한다.

 

4)

4)

여기서 주목할 점은, 컴퓨터는 인터프리터가 코드를 번역할 때까지 항상 기다려야 한다는 점이다. 이 시간을 줄이기 위해 Just In Time Compiler를 작동시킬 수 있다. 컴파일된 코드나 intermediated code가 JIT 컴파일러를 거치는데, 이때 JIT 컴파일러가 중복된 4,5,6번째 코드를 마크한다. JIT 컴파일러는 내부적으로 메소드가 호출될 때마다 호출 횟수를 카운트하고, 그 횟수가 임계치를 초과하면 이를 캐싱해서 미리 해석본을 만든다.

 

그래서 파일이 interpreted 될 중복된 코드라면 만들어둔 해석본으로 JIT 컴파일러가 컴파일을 하고, 인터프리터는 해석 과정을 거치지 않고 컴퓨터가 바로 이 기계어를 실행하게 한다. 이를 통해 같은 코드를 다시 번역하는 과정이 없어진다. 

 

JIT 컴파일러는, 해석된 프로그램이 실행되는 동안 가장 자주 사용되는 코드를 결정하고 이를 기계어 코드로 번역한다. 즉 프로그램 실행 도중에 동적으로 컴파일하기 때문에 이름이 Just In Time 컴파일러다.

 

 

끝나지 않는 나의 의문들...


  • 인터프리터를 거치기 전에 JIT Compiler를 거친다고 했다. 그럼 JIT 컴파일러를 거쳐서 여러 번 호출되는 코드(Hot Spot)라고 판단되면 JIT가 컴파일해버리고 캐싱하는건가? Interpreter는 이 코드는 해석하지 않는건가?
  • Interpreter도 언어를 다른 수준 언어로 바꾸니까 컴파일해서 어떤 목적 파일을 만들어놓지 않을 뿐이지, 컴파일하는 과정은 다 내포하고 있는거 아닌가? Intermediate code와 compiled code의 차이를 잘 모르겠다....
더보기

C는

  • 컴파일러에 의해 소스코드 → 어셈블리어
  • 어셈블러에 의해 어셈블리어 파일 → Object File
  • linker에 의해 Object FIle - > machine code 됨.

Java는

  • 컴파일러에 의해 소스코드 → byte code (Object Code)
  • 인터프리터에 의해 byte code(Object Code) → machine code(Object Code)
  • 근데 이제 해당 해석을 미리 JIT 컴파일러가 캐싱을 함. 
  • 중복된 코드에 대해서 첫번째 번역을 인터프리터가 해놓으면 JIT가 카운팅을 하는거야 , 아니면
  • 인터프리터 가기 전에 먼저 JIT를 거치니까 JIT가 중복 카운트 후 중복되는 애가 맞으면 자기가 컴파일을 해버리는거야???

 

728x90
Comments