오프닝: 마인크래프트 셰이더 에피소드

최근에 마인크래프트를 팀원분들과 하게 되었다. 올해 새롭게 출시된 엄청나다던 맥북 m4 pro를 구매했기 때문에 셰이더 모드라는 마인크래프트의 그래픽을 좋게 만들어주는 모드를 설치하려고 했다. 위 보이는 사진은 셰이더 모드를 설치하기 위한 인스톨러이다.
근데 이게 웬걸, 찾아보니까 MacOS에서 설치하는 방법이 없었다! 절망감에 휩싸이며 포기하려던 찰나, 셰이더를 설정하는 파일을 봤더니 그 인스톨러의 확장자가 .jar로 끝나는 것을 봤다. 내가 왜 MacOS에서 설치하는 방법이 없었고, 직접 도전해보지 않았냐면 ARM64 버전 인스톨러가 없기 때문에 ‘설마 MacOS는 지원을 안 하는 건가?‘라고 생각한 것이었다.
게임이라는 카테고리에 있어서는 MacOS는 거의 외딴 섬 취급이기 때문에 Mac은 지원을 안 하나보다… 이런 생각이었다. 하지만 마인크래프트가 Java 기반이고, .jar 확장자의 인스톨러를 사용하니까, 그냥 Java 깔려있으면 되는 건가? 싶어서 해봤더니 맙소사. 정말로 그냥 된다. CPU가 어떤 계열이던지 간에 그냥 자바만 있으면 된다.
그때 문득 스치는 자바의 탄생 철학 “Write Once, Run Anywhere” 가 떠올랐고, 자바를 이렇게까지 좋아한 적은 처음이다. 그 동안 자바를 미워했던 게 후회되는 순간이었다.
이 경험에서 자연스럽게 의문이 생겼다. .jar 파일이 대체 뭐길래 CPU 종류에 상관없이 돌아가는 걸까? 왜 어떤 프로그램은 Intel용, Apple Silicon용을 따로 받아야 하는데, 자바 프로그램은 그냥 되는 걸까? 이 질문의 답을 찾아가는 과정이 바로 이 글의 여정이다. 빌드와 컴파일이 뭔지부터 시작해서, 인터프리터, JIT 컴파일, 그리고 CPU 아키텍처까지 이야기해보려고 한다.
1. 컴파일러 vs 인터프리터 기초
빌드(Build)란?
먼저 빌드(Build) 라는 것은 단어의 뜻에서 알 수 있듯이 뭔가를 짓는다는 것이다. 빌드는 소스 코드 파일을 컴퓨터나 휴대폰에서 실행할 수 있는 독립 소프트웨어 가공물로 변환하는 과정이나 그 결과물을 일컫는다.
우리가 소스 코드를 실행시키면 일반적으로 그 소스 코드 자체를 그대로 실행시키는 것이 아니다. 그 코드를 빌드함으로써 생기는 결과물을 실행하게 되는데, 이러한 빌드를 도와주는 것이 컴파일러와 인터프리터다. 컴파일러와 인터프리터에 의해 컴퓨터가 이해할 수 있는 수준으로 바뀌고, 이후 컴퓨터가 빌드된 코드를 실행시키는 것이다. 즉, 프로그래밍에서 빌드라고 하면 실행 가능한 파일로 만드는 과정을 의미한다.
앞서 말한 컴퓨터가 이해할 수 있는 수준은 어셈블리어(Assembly Language) 라고 한다.
In Computer Programming, assembly language often referred to simply as assembly and commonly abbreviated as ASM or asm, is any low-level programming language with a very strong correspondence between the instructions in the language and the architecture’s machine code instructions. - Wikipedia
어셈블리어는 기계어와 1대 1로 대응되는 관계이다. 기계어는 이름만 들어도 알 수 있겠지만 정말 CPU의 언어이다. 실제로 0과 1로 이루어져 있는데, 예를 들면 다음과 같다.
10110000 01100001
이는 x86 계열 CPU의 기계어 명령이고, 이것을 어셈블리어로 옮겨 쓰면 다음과 같다.
mov al, 061h
어셈블리어도 굉장히 복잡한 언어지만, 그래도 기계어를 생각하면 나은 것 같다. 기계어의 문제는, CPU의 언어이기 때문에 CPU가 바뀔 때마다 기계어가 바뀌고, 이러한 기계어와 1대 1로 매칭되는 어셈블리어 또한 바뀐다는 것이다. 프로그래밍 언어가 매번 바뀐다니, 가슴 아픈 일이 아닐 수 없다.
이러한 문제점을 해결하기 위해서 컴파일(Compile) 이라는 방법이 나왔다. 좀 더 인간에게 가까운 수준이고, 좀 더 통일된 언어체계가 필요했다. 따라서 C 언어와 같은 언어로 소스 코드를 작성하고, 이를 컴파일하여 어셈블리어로 빌드하는 방법을 사용하기 시작했다.
컴파일(Compile)
컴파일은 소스코드 전체를 기계어로 번역하는 것이다.
이렇게 한 번에 어셈블리어로 번역되는 언어들은 C, C++, Go 언어들이 있는데, 대표적인 C 언어의 컴파일 과정을 알아보자. 위의 다이어그램처럼 .c 소스가 전처리 과정에 의해서 .i 소스로 바뀌고, 이후 C Compiler가 어셈블리어로 바꾸고, Assembler가 다시 기계어로 바꾸게 된다.
중요한 포인트는 C를 컴퓨터가 실행시키기 위해서 어셈블리어로 바뀌고, 또 기계어로 바뀐다는 점이다. 컴파일 과정의 장단점을 요약하면 다음과 같다.
장점
- 좋은 퍼포먼스: 미리 컴파일된 기계어 코드를 실행하므로 퍼포먼스가 좋은 편이다.
- 에러 검출 용이: 컴파일러는 코드를 컴파일하는 과정에서 에러를 감지할 수 있다.
단점
- 개발 시간 증가: 코드를 작성하고 컴파일하는 데 시간이 걸린다. 코드 수정 후에도 다시 컴파일해야 하기 때문에 개발 과정이 번거로울 수 있다.
- 플랫폼 종속적: 컴파일 언어로 작성된 프로그램은 특정 플랫폼에 종속적이기 때문에 이식성이 떨어진다. (Windows에서 gcc를 통해 C언어를 컴파일하면
a.exe파일, Mac에서는a.out파일이 나온다.) - 큰 용량: 컴파일된 실행 파일의 크기가 크고, 컴파일 과정에서 메모리 사용량을 많이 사용한다.
인터프리트(Interpret)
인터프리트는 소스코드를 한 줄씩 번역하면서 실행하는 것이다.
인터프리터 언어로는 대표적으로 JavaScript나 Python, Ruby 등이 있다. 사실 정확히 꼭 집어서 100% 인터프리터 언어라고 보기 어렵긴 하다. 중간에 컴파일하는 과정이 없는 것은 아니기 때문인데, 이는 JavaScript의 실행 과정을 보면서 설명하겠다.

자바스크립트(JavaScript) 란? 간단하게 객체 기반의 스크립트 프로그래밍 언어이며, 웹 브라우저 내에서 사용되는 프로그래밍 언어라고 보면 된다. 자바스크립트를 실행시키는 것을 도와주는 것을 V8 엔진이라고 부른다.
V8 엔진은 구글이 주도하여 C++로 작성된 고성능의 자바스크립트 및 웹 엔진이다. Google Chrome에 내장되어 있으며, HTML&CSS와 함께 웹에서 실행하는 것이 아닌, 자바스크립트 자체만을 실행시키게 도와주는 Node.js 또한 V8 엔진을 통해서 자바스크립트를 실행하고 있다.

C 언어의 컴파일 과정과 마찬가지로 개별적인 과정(Parser, Syntax Tree 등)이 동작하는 원리는 복잡하다. 간단하게 전체적인 과정을 보면 자바스크립트는 자바스크립트의 바이트코드로 먼저 컴파일된 후, 인터프리터를 통해서 한 줄씩 번역하면서 실행된다. 앞서 100% 인터프리터 언어라고 부르기 어려운 이유는 컴파일하는 과정이 포함되어 있기 때문인데, 여기서는 인터프리터가 바이트코드를 기계어로 번역해 주면서 바로바로 실행해 주게 된다.
장점
- 빠른 개발: 소스 코드를 직접 실행하기 때문에 코드를 빠르게 수정하고 테스트할 수 있다.
- 좋은 이식성: 일반적으로 플랫폼에 독립적이다. (단, 플랫폼에 맞는 인터프리터를 설치해야 한다.)
- 동적 타입 시스템: 대부분의 인터프리트 언어는 동적 타입 시스템을 사용하기 때문에 유연하고 간편하게 코드를 작성할 수 있다.
단점
- 낮은 성능: 일반적으로 컴파일러 언어에 비해 퍼포먼스가 떨어진다.
- 에러 발견 지연: 인터프리터는 컴파일 과정이 없기 때문에, 에러를 런타임에서 확인하게 된다.
하이브리드(Hybrid): Java의 접근 방식
하이브리드 타입은 컴파일 방식과 인터프리트 방식을 합친 방법이다.
대표적인 하이브리드 언어는 Java가 있다. Java가 여러 개의 CPU에서 같은 코드가 실행되기 위해서는 C/C++의 프로그램의 실행 구조와는 다른 방식이 필요하다. C/C++가 특정 CPU의 기계어 코드를 직접 생성하면, 이 기계어 코드가 메모리에 적재되어 바로 실행된다. 따라서 C/C++는 CPU가 달라지면 컴파일러가 달라져야 한다.
그러나 Java는 같은 코드를 사용하여 다른 CPU에서 실행되도록 하기 위해 직접 CPU의 기계어 코드를 생성해서는 안 된다. 그 대신 자바는 바이트코드(Java bytecode) 라는 것을 생성하고, 이것을 자바 가상 머신(JVM, Java Virtual Machine) 이 해석하여 실행하는 구조다. JVM이 인터프리터가 되어 코드 해석 방식을 실행함으로써, 같은 바이트코드를 가지고 여러 가지의 CPU에서 실행이 가능해진다.
자바 소스코드는 *.java의 확장자를 가지게 된다. 이는 당연하게도 그 자체로 CPU가 인식할 수 없기 때문에 빌드 과정을 통해서 소스 코드를 실행시킬 수 있는 상태로 만들어야 한다. JDK(Java Developer Kit) 를 설치하면, JDK 안에 있는 javac.exe가 .java 확장자 파일을 .class 확장자 파일로 컴파일해주게 된다. 이렇게 생긴 .class 확장자 파일은 자바의 바이트 코드가 된다.
즉, 동일한 .class 바이트코드가 JVM이 설치된 어떤 플랫폼에서든 실행될 수 있다.
이렇게 생긴 .class 파일은 JVM이 인식할 수 있게 되는데, 이러한 구조를 가지고 있기 때문에 자바 언어를 하이브리드 언어라고 부르며, 앞서 컴파일 언어가 가졌던 플랫폼의 종속적이라는 단점을 극복할 수 있게 되었다. 이것이 바로 앞서 마인크래프트 셰이더의 .jar 파일이 ARM Mac에서도 그냥 실행된 이유이다.
2. 심화: JIT 컴파일과 인터프리터/컴파일러 스펙트럼
기초편에서 컴파일러와 인터프리터, 그리고 하이브리드 방식까지 살펴보았다. 하지만 현실의 프로그래밍 언어들은 이분법적으로 “이건 컴파일러, 이건 인터프리터"라고 단정 짓기 어렵다. 인터프리터와 컴파일러를 스펙트럼으로서 표현하는 것이 현실을 더 잘 반영한다.
바이너리 파일의 실행 과정
일반론적으로는 빌드된 결과물이 바이너리 파일이면 “이것은 컴파일러 언어다!“라고는 한다. C언어나 Go언어가 그렇다. 하지만 바이너리 파일 속 기계어가 CPU 내부에서 디코딩되고 실행되는 과정까지도 인터프리팅이라고 볼 수 있다. 왜냐하면, CPU 아키텍처에 따라서 같은 기계어라도 내부적으로 해석하는 방식이 다르기 때문이다.
인터프리터는 소스 코드를 하나하나씩 읽어서 실행한다. 컴파일러는 말 그대로 실행 가능한 바이너리 파일을 만들어낼 뿐, 실행하지 않는다. 바이너리 파일이 실제로 실행되는 과정을 살펴보면, 이 과정을 인터프리팅이라고 볼 수 있지 않을까?
바이너리 파일이 실행되는 과정을 알아보자.
1. 로더(Loader)의 역할
- OS가 바이너리 파일을 메모리에 적재
- ELF(Linux), PE(Windows), Mach-O(macOS) 등의 실행 파일 포맷 해석
- 메모리 주소 재배치(relocation), 동적 링킹 수행
2. CPU 레벨에서의 해석
- 바이너리 명령어들이 CPU의 명령어 디코더를 거쳐 마이크로 연산으로 분해
- 예를 들어,
ADD EAX, EBX같은 x86 명령어는 바이너리01 D8(2바이트)가 된다. 이를 명령어 디코더가 해석해서 ALU에서 실제 덧셈을 수행
3. 마이크로아키텍처 레벨
- 복잡한 CISC 명령어(x86)는 내부적으로 여러 개의 RISC 스타일 마이크로 연산으로 변환
- Intel이나 AMD CPU 내부에서 실시간으로 일어나는 변환
바이너리 파일이 기계어로 변환되는 과정에서 여러 단계의 “해석” 과정이 있다는 걸 알 수 있다. 이 관점에서 보면 C 컴파일러가 만든 바이너리도 결국 OS 로더와 CPU에 의해 “인터프리팅” 된다고 생각할 수 있다.
JIT(Just-In-Time) 컴파일
Python도 사실 내부적으로는 바이트코드로 컴파일된다. 또한 JIT(Just-In-Time) 컴파일 같은 기술도 있다. .py 확장자로 끝나는 소스 코드를 python이 아니라 pypy라는 런타임으로 실행하면 JIT 방식을 통해서 부분적으로 컴파일을 실행해 주며, 속도가 훨씬 빨라진다.
https://www.ulrich-scheller.de/a-pypy-runtime-for-aws-lambda/
백준에서 Python으로 돌리면 시간초과가 나는 소스 코드를 PyPy로 돌리면 가끔씩 통과되는 경우가 있다. Node.js 또한 V8 엔진을 통해서 JIT 방식을 사용한다. 처음에는 인터프리팅하다가 자주 쓰이는 코드는 JIT 컴파일하는 것이다.
인터프리터와 컴파일러 스펙트럼
프로그래밍 언어의 인터프리터/컴파일러 스펙트럼
컴파일러에 가깝게 딱 붙어있는 언어들도 CPU 레벨의 기계어로 전환되는 과정을 생각하면 약간의 인터프리팅 영역이 있다. 그리고 Python과 같은 언어들도 내부적으로 바이트코드로 변환하는 부분이 있지만, 다른 언어들에 비해서 크지 않다. 따라서 인터프리터에 가깝게 딱 붙어있는 언어들도 마찬가지로 약간의 컴파일 영역이 있다고 볼 수 있다.
왼쪽부터 왜 그 위치에 있는지 대표적인 언어들로 설명하면 아래와 같다.
- Python:
.py확장자를 가진 파이썬 소스 코드를python3으로 실행하면 소스 코드를 그대로 실행해 준다. - JavaScript:
.js확장자를 가진 자바스크립트 소스 코드를node로 실행하면 소스 코드를 그대로 실행하지만, 처음부터 적극적인 JIT 컴파일 전략과 코드 실행 즉시 최적화 등 JIT가 크게 발전했기 때문에 컴파일의 영역이 파이썬보다 크다. - Java/C#: Java와 C# 모두 플랫폼 독립적인 중간 표현(자바 바이트코드, IL)등으로 바뀌며, JVM 및 CLR이라는 머신 위에서 인터프리팅 된다.
- C/C++: OS에서 바로 실행이 가능한 바이너리 파일 수준으로 컴파일되며, 나머지는 OS에 따라서 해석되어 기계어를 제어한다.
그렇다면 PyPy는 어느 위치 정도에 있을까? 위에 따르면 아마 Python과 JavaScript 사이에 있을 것이다. 적극적인 JIT를 사용하면서 점점 오른쪽으로 가게 되며, 성능도 좋아진다. 반대로 왼쪽에 있는 인터프리터 언어일수록 쓰기 편하며, 빠르게 개발할 수 있다. node를 통해서 띄운 서버를 --watch 옵션을 걸어서 소스 코드가 바뀔 때마다 띄울 수 있게 만들면, 순식간에 바뀐 코드로 테스트를 할 수 있다.
프로그래밍 언어의 퍼포먼스를 비교한 자료도 참고해보자.
각 프로그래밍 언어별 실행 속도 비교 — 컴파일 언어일수록 빠른 경향을 보인다
동일 알고리즘을 각 언어로 구현했을 때의 실행 시간 비교
3. ARM vs x86 아키텍처
앞에서 컴파일러가 만든 바이너리가 CPU에 따라 다르다는 이야기를 했다. 그렇다면 CPU 아키텍처는 구체적으로 어떻게 다를까?
소프트웨어를 깔면서, 그리고 Docker 이미지를 빌드하면서 자꾸 ARM과 AMD를 설정해줘야 하는 부분이 있었고, 가끔씩 뭐가 뭔지 까먹는다. 둘이 비슷하기도 하고, 실수해서 잘못 입력하기도 한다.
CPU 회사들의 제품들 아키텍처는 다음과 같이 정리할 수 있다.
포인트는, Intel과 AMD는 AMD(x86) 계열이며, Apple은 ARM 계열이라는 것이다. 따라서 Intel과 AMD의 CPU 언어 차이는 서울과 부산 방언 수준의 차이가 있지만, AMD와 ARM의 차이는 거의 외국어라서 아예 호환이 안 된다.
CPU마다 기계어가 다르다. 그래서 어떤 코드라도, 기계어로 변환할 때 아키텍처에 따라서 다르다. Intel과 AMD는 역사적으로도 같이 발전해 왔기 때문에 AMD 계열로 묶이며, x86_64/amd64 이렇게 한 번에 묶어서 표현하는 경우도 있다.
AMD(x86)의 발자취
https://www.youtube.com/watch?v=TqOCC65HkCQ
AMD(Advanced Micro Devices) 는 1969년 설립된 회사이며, ARM과 비교하면 먼저 등장했다. 1970년대부터 Intel의 세컨드 소스 공급 업체였고, 1990년대부터 독자적인 프로세서를 개발하여 2003년에 AMD64(x86-64) 아키텍처를 발표했다.
AMD는 큰 특징으로는 CISC 방식을 사용한다는 점이다. CISC는 Complex Instruction Set Computer라는 뜻이다.
- 복잡하고 다양한 명령어 집합
- 하나의 명령어로 여러 작업을 동시에 처리 가능
- 복잡한 연산에 유리하지만 전력 소모가 큼
ARM의 여정
ARM은 1990년 영국에서 시작된 회사로, 처음에는 Acorn RISC Machine의 줄임말이었다. ARM의 독특한 점은 직접 칩을 제조하지 않고 설계만 라이선스 해주는 회사였다는 점이다. 2010년대에는 스마트폰 시장을 장악하기 시작했고, 2020년대에는 데스크톱/노트북에 진출하여 큰 인기를 끌고 있다. 애플 실리콘이 이에 해당한다.
ARM은 RISC 방식을 사용한다. RISC는 Reduced Instruction Set Computer라는 뜻이다.
- 단순하고 효율적인 명령어 집합
- 각 명령어가 한 번에 하나의 작업만 수행
- 하드웨어가 단순해 전력 효율성이 높음
레지스터 구조 비교
x86-64
- 16개의 범용 레지스터 (RAX, RBX, RCX 등)
- 상대적으로 적지만 복잡한 명령어로 보완
ARM64
- 31개의 범용 레지스터 (X0-X30)
- 더 많은 레지스터로 메모리 접근 횟수 감소
- 효율적인 데이터 처리 가능
https://www.tothenew.com/blog/x86-or-arm64-making-sense-of-the-architectural-variations/
일반적으로 ARM이 전력 효율성 측면에서 승리했으며, 당연히 전력 효율성이 중요한 모바일 시장에서부터 점점 점유율을 넓혀가고 있다. 맥북 M1의 등장, 그리고 계속해서 나오는 M2, M3, M4까지 맥미니와 함께 데스크톱과 노트북 시장을 장악해가고 있다.
하지만, 모든 기술은 일장일단이다. 전력을 많이 사용하기는 하지만 고성능이 필요한 분야(게이밍, 고성능 컴퓨팅 등)에서는 여전히 x86 아키텍처가 유리하긴 하다.
현실적인 이유도 있다. 1970년대부터 먼저 CPU 시장에 진출한 AMD(x86)가 온갖 역풍을 다 맞았을 것이다. 16비트 -> 32비트 -> 64비트 확장되면서 기술적인 복잡성이 누적되었고, 1978년의 8086부터 시작해서 하위 호환성을 계속 유지하고 있기 때문에, 40년 전 설계 결정들이 아직까지 발목을 잡고 있다. 그에 비해 ARM은 초신성이며, 깨끗한 설계로 시작했고 모바일 시대의 요구사항에 부합하며, 전력 효율성이 점점 중요해지는 시대가 왔기 때문이다.
실제 사용에서의 차이점
인스톨러 플랫폼
맥 사용을 하다 보면 프로그램을 다운로드할 때 다양한 선택지를 볼 수 있다. Intel Mac(x86 or AMD), Apple Silicon(M chip or ARM64) 이 중 어떤 것을 설치할 것이냐는 선택지이다.
chromedriver 다운로드 페이지
위와 같이 다양한 버전으로 크롬드라이버 인스톨러가 나뉜다. 이는 당연하게도, CPU에 따라서 완전히 다른 기계어를 사용하기 때문이다. 따라서 항상 자신의 플랫폼에 맞는 인스톨러를 선택해야 한다.
로제타(Rosetta)와 Docker 멀티플랫폼 빌드
에뮬레이션
Docker의 유구한 역사를 돌이켜보면, 하이퍼바이저를 개선하기 위해서 호스트 OS 위에 가상 OS를 다운로드하곤 했다. 하지만 Docker가 나오면서 Docker Engine 위에서는 어떤 도커 이미지도 잘 돌아가도록 운영체제에 종속적인 문제가 사라졌지만, CPU에 종속적인 것은 피할 수 없었다. 따라서 Docker를 빌드할 때 --platform 파라미터를 통해서 AMD나 ARM 아키텍처를 지정할 수 있다.
특히, ARM을 사용하는 경우에는 로제타라는 에뮬레이터를 통해서 x86_64/amd64 아키텍처를 사용할 수 있도록 할 수 있다. 물론 에뮬레이팅의 오버헤드로 인해 성능은 떨어지겠지만, 나쁘지 않다.
마치며
마인크래프트 셰이더 모드의 .jar 파일 하나에서 시작된 호기심이 꽤 긴 여정으로 이어졌다. 정리하면 다음과 같다.
- 빌드와 컴파일: 소스 코드를 컴퓨터가 이해할 수 있는 형태로 변환하는 과정이다. 컴파일러는 한 번에 전체를, 인터프리터는 한 줄씩 번역한다.
- JIT 컴파일과 스펙트럼: 현실의 언어들은 컴파일러/인터프리터로 이분법적으로 나눌 수 없다. JIT 같은 기술로 인터프리터 언어도 점점 컴파일 영역을 넓혀가고 있다.
- ARM vs x86: CPU 아키텍처에 따라 기계어가 완전히 다르다. ARM(RISC)은 전력 효율, x86(CISC)은 고성능에 유리하며, Java의 “Write Once, Run Anywhere"는 이 차이를 JVM으로 극복한 것이다.
결국 이 모든 이야기의 공통점은 추상화이다. 기계어의 차이를 컴파일러와 가상 머신이 숨겨주고, CPU 아키텍처의 차이를 JVM과 같은 런타임이 극복해준다. 기술이 발전할수록 이러한 추상화 계층은 더 정교해지고, 개발자는 더 높은 수준에서 문제를 해결할 수 있게 된다.
개인적으로 프로그래밍 언어를 컴파일러/인터프리터로 이분법적으로 나누는 것보다는, 스펙트럼 관점에서 바라보는 것이 현실을 더 잘 반영한다고 생각한다. 마인크래프트 셰이더 설치 경험에서 느꼈던 Java의 매력도 결국 이 스펙트럼 상에서 적절한 위치를 차지하고 있기 때문이다. 바이트코드로 컴파일되어 성능을 확보하면서도, JVM 위에서 플랫폼 독립성을 유지하는 것 말이다.
References
- 위키피디아: 어셈블리어, C, V8, 소프트웨어 빌드, 자바
- Stranger’s LAB: 프로그래밍 언어와 빌드 과정
- Evans Library: V8 엔진은 어떻게 내 코드를 실행하는 걸까?
- 알짜배기 프로그래머: 알기 쉽게 정리한 JAVA의 컴파일 과정 및 JVM 메모리 구조, JVM GC
- attractivechaos: Programming Language Benchmark