저우즈밍(周志明) 저, "JVM 밑바닥까지 파헤치기"를 기반으로 작성한 글입니다.
실행 환경은 Windows + Ubuntu 22.04.05 LTS 기반으로 진행합니다.
1. 소스 코드 구하기
📌 OpenJDK 17
위 링크에서 zip file을 클릭하여, 전체를 압축한 파일을 다운 받는다.
zip 파일 크기는 164MB고, 압축을 풀면 약 582MB의 파일을 받을 수 있다.
만약 Windows 사용자라면, 해당 파일을 가상 환경으로 옮겨야 한다.
cp -r mnt/c/Users/{사용자 이름}/{설치된 경로}/openjdk-17+35_src ./home/{사용자 이름}/
2. 컴파일 환경 구축하기
📌 GCC 설치
💡 맥북이 있다면 반드시 사용하길 권장합니다. 맥북 놔두고 윈도우 환경으로 하면 여러모로 귀찮습니다.
맥OS 환경이면 10.13(High Sierra) 이상에서, 최신 버전의 XCode와 XCode용 명령 줄 도구(Command Line Tools)를 애플 개발자 웹 사이트에서 받으면 된다.
리눅스 환경이면 GCC 5.0 이상 혹은 Clang 3.5 이상을 준비해야 하는데, 우리는 GCC로 진행해볼 예정이다.
sudo apt-get install build-essential -y
위 명령어만으로 여러 언어를 컴파일하기 위한 다양한 개발자 도구를 설치할 수 있다.
gcc -version
g++ -version
위 커맨드로 버전이 잘 나오면 정상 설치된 것이다.
👇 만약 설치가 안 됐다면
sudo apt update
내 경우엔 apt를 업데이트 안 했다가 발생한 문제였다.
📌 서브 파티 라이브러리와 빌드 도구 설치
sudo apt-get install libfontconfig1-dev -y
sudo apt-get install libfreetype6-dev -y
sudo apt-get install libcups2-dev -y
sudo apt-get install libx11-dev libxext-dev libxrender-dev libxrandr-dev libxtst-dev libxt-dev
sudo apt-get install libasound2-dev
sudo apt-get install libffi-dev
sudo apt-get install autoconf
- Fontconfig: 폰트 설정 라이브러리
- FreeType: 폰트 렌더링 라이브러리
- CUPS
- X11: X 윈도 시스템
- ALSA
- libffi: 포터블 외부 함수 인터페이스(foreign function interface) 라이브러리
- Autoconf: M4 매크로 확장 패키지
👇 내 경우 추가로 설치해줬어야 했던 것
sudo apt-get install unzip
sudo apt install cmake
sudo apt install gdb # 디버거
만약, 설치되어 있지 않다면 확인을 해보자.
참고로 cmake의 버전을 확인했을 때, 3.30 보다 낮으면 나중에 에러가 발생한다.
이 경우 다음과 같은 방법으로 최신 버전을 받을 수 있다.
# 1. 필요한 패키지 설치
sudo apt install wget
# 2. Kitware의 apt 저장소 키 다운로드 및 추가
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null
# 3. 저장소 추가
echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ jammy main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null
# 4. 패키지 목록 업데이트 및 CMake 설치
sudo apt update
sudo apt install cmake
대강 설명을 첨부하자면
- 웹에서 파일을 받기 위한 wget 패키지 설치
- Kitware(CMake 만든 회사) 공식 패키지 저장소 인증키 다운받고, gpq로 키 변환하여 시스템 등록
- Kitware의 Ubuntu 패키지 저장소를 시스템 소스 목록에 추가하고, Ubuntu 22.04 코드명(jammy) 등록
- 새로 추가한 저장소를 포함하여 패키지 목록 업데이트
📌 Boot JDK
💡 Boot JDK란?
- 옛날 옛적에는 JDK를 C, C++로 작성했으나, 최근엔 Java로 많이 바뀌었다.
- 그래서 자바 코드를 컴파일하기 위한 JDK를 의미한다.
- 부트 JDK는 직전 메이저 버전이 원칙이다.
- JDK17을 컴파일하려면 JDK16을 필요로 한다.
- 같은 메이저 버전 또한 가능하다.
아쉽게도 JDK 16은 LTS 버전이 아니라서, apt 저장소에 들어 있지 않다.
따라서 OpenJDK 17로 진행한다.
sudo apt-get install openjdk-17-jdk
3. 컴파일하기
📌 OpenJDK가 제공하는 컴파일 매개변수
가장 유용한 몇 가지만 정리
- --with-debug-level=<레벨>
- 컴파일 레벨 지정
- release, fastdebug, slowdebug 가능 (default는 release)
- 최적화가 적을 수록 디버깅 정보를 많이 제공
- --enable-debug
- --with-debug-level=fastdebug와 동일
- --with-native-debug-symbols=<방식>
- 디버그 심벌 정보의 컴파일 방식 지정
- none, internal, external, zipped 가능
- --with-version-string=<string>
- 컴파일된 jdk 버전 지정 (java -version으로 출력되는 값)
- --with-jvm-variants=<변형>[,<변형>...]
- 핫스팟 가상 머신을 특별한 모드(변형)로 컴파일
- server, client, minimal, core, zero, custom 가능
- --with-jvm-features=<기능>[,<기능>...]
- custom 가상 머신용 기능 목록 지정
- --with-target-bits=<비트>
- 가상 머신 컴파일을 32 bit, 64 bit 지정
- --with-<라이브러리>=<경로>
- 의존하는 패키지 경로 지정
- 여러 버전의 부트 JDK나 의존 패키지가 설치되어 있을 때 사용
- boot-jdk, freetype, cups, x, alsa, libffi, jtreg, libjpeg, giflib, libpng, lcms, zlib 가능
- --with-extra-<플래그 종류>=<플래그들>
- C, C++, 자바 컴파일에 필요한 추가 컴파일 매개 변수 지정
- <플래그 종류>: cflags(C), cxxflags(C++), ldflags(Java)
- --with-conf-name=<이름>
- 컴파일 설정 이름 지정
- 기본적으로 설정 이름은 {OS, ISA, 디버깅 레벨} 등의 정보로 자동 생성
📌 configure
처음에 받은 jdk 17 디렉토리 안에는 configure라는 파일이 존재한다.
JDK 빌드를 위한 환경 설정을 담당하는데, 이 설정은 위의 매개변수로 지정할 수 있다.
- 시스템 환경 검사
- 빌드 설정 구성
- 빌드 환경 준비
- 의존성 확인과 설정
- 결과를 담을 디렉토리 구조 생성
bash configure [options]
기본적인 명령어 구성은 위와 같다.
만약, 핫스팟 가상 머신을 FastDebug 버전으로 서버용으로만 컴파일하고 싶다면, 다음과 같이 설정하면 된다.
bash configure --enable-debug --with-jvm-variants=server
성공하면 위와 같이 설정 요약 정보를 출력한다.
👇 Ubuntu on Windows 환경에서 Target CPU mismatch 에러가 발생한다면
처음에 configure를 설정하면 위와 같이 에러가 발생했다.
로그를 보아하니, 개발자 도구들을 끌어올 때 ubuntu 가상 환경 내의 라이브러리가 아니라 /mnt/ 경로를 통해 Windows 환경에 설치된 라이브러리들을 끌고 오기 때문인 것 같았다.
Ubuntu에서 Windows의 toolchain을 사용하려고 시도하니, 빌드 시스템이 타겟 CPU 아키텍처와 컴파일러(CL - Microsoft C/C++ 컴파일러)의 아키텍처 사이에 불일치로 인한 에러가 발생한 것.
이럴 땐, 사용할 도구를 명시적으로 지정하는 옵션을 추가하면 된다.
bash configure --enable-debug \
--with-jvm-variants=server \
--with-boot-jdk=/usr/lib/jvm/java-17-openjdk-amd64 \
--with-toolchain-type=gcc \
--build=x86_64-linux-gnu \
--host=x86_64-linux-gnu \
--target=x86_64-linux-gnu
📌 make
make
위 명령어를 실행하면 JDK를 본격적으로 빌드하기 시작하는데, 시간이 좀 걸리니 15분 짜리 youtube 영상 한 편 보고 오면 된다.
실행이 끝나면 위 경로에 다양한 파일들이 생성된다.
- buildtools/: 컴파일 과정에서 쓰이는 도구를 생성하고 저장
- hotspot/: 핫스팟 가상 머신에 의해 컴파일되는 중간 파일 저장
- images/: `make *-image` 명령으로 만들어지는 이미지 저장
- jdk/: 컴파일 완료된 jdk 저장
- support/: 컴파일 도중 생성되는 중간 파일들 저장
- test-results/: 컴파일 후 자동화 테스트 결과 저장
- configure-support/: configure 명령이 사용하는 임시 파일 저장
- make-support/: make 명령이 사용하는 임시 파일 저장
- test-support/: test 명령이 사용하는 임시 파일 저장
💡 다시 컴파일하거나 설정을 변경하려할 때
만약 위 디렉토리들이 생성된 이후, 재컴파일 혹은 설정을 변경하려고 할 때는
반드시 clean 이후 dist-clean 명령을 순차적으로 실행해 기존 설정들을 정리해야 한다.
그렇지 않으면 기존 설정이 새로운 설정에 영향을 줄 수 있다.
📌 make images
make images
make가 소스 코드를 컴파일하여 바이너리 파일들을 생성하고, 개별 컴포넌트들(클래스 파일, 네이티브 라이브러리 등)을 빌드하면
make images로 컴파일된 모든 컴포넌트들을 모아서 실제 사용 가능한 JDK 이미지를 생성한다.
- JDK 표준 디렉토리 구조 생성 (bin, lib, include 등)
- 실행 가능한 형태로 패키징
- 필요한 설정 파일들과 함께 완전한 JDK 배포 이미지 생성
여기서 images란 product-images의 줄임말로, JDK 이미지 전체를 컴파일하는 target을 의미한다.
물론 여기도 여러 target이 존재한다.
- hotspot: 핫스팟 가상 머신
- hotspot-<variant>: 지정한 특정 모드의 핫스팟 가상 머신들만
- docs-image: JDK 문서 이미지 생성
- test-image: JDK 테스트 이미지 생성
- all-images: product, docs, test 타겟을 차례로 호출
- bootcycle-images: JDK를 두 번 컴파일하여, 첫 번째 컴파일 결과를 두 번째 컴파일의 boot jdk로 사용
- clean: make 명령으로 생성된 임시 파일 삭제
- dist-clean: make, configure 명령으로 생성된 임시 파일 삭제
참고로 이 디렉토리 파일들을 JAVA_HOME 경로에 복사하면, 완전한 JDK로 이용할 수 있다.
위 과정이 모두 끝났다면, 위 경로에서 java version을 확인할 수 있다.
4. 디버깅
📌 IDE 설치
학생 계정으로 프로 라이센스가 있어서, CLion을 IDE로 사용했다. (책에서도 이거 씀)
그런데 그냥 CMake 실행할 수 있는 IDE라면, 뭐가 됐든 딱히 상관 없는 듯하다.
위와 같이 프로젝트를 생성하면 CMakeLists를 자동으로 생성해준다.
👇 프로젝트 생성 후 cmake에 실패한다면
처음에 이런 에러가 발생했는데, IDE에서 WSL 파일 시스템의 프로젝트를 Windows 환경에서 직접 빌드하려고 시도했기 때문에 발생한다.
그래서 다음과 같이 빌드 환경을 수정해주어야 한다.
Toolchains 설정할 때, 자동으로 도구 선택이 안 되면 Ubuntu 환경에서 cmake, gdb 등이 제대로 설치되지 않았기 때문.
📌 실행 환경 설정
CMake가 성공했다면 Run/Debug Configurations 창에 이미 애플리케이션이 등록되어 있다.
여기서 아래 내용을 변경해주자.
- Executable 경로를 <프로젝트 파일>/build/<설정 이름>/jdk/bin/java 로 수정
- Program 매개변수에 "-version" 추가
- Before launch 항목에서 "build" 제거
📌 break point
우리가 작성한 자바 코드를 실행해 디버깅을 하려면, 가상 머신에서 실행되는 특정 자바 코드를 추적해야 한다.
그런데 어디서부터 시작해야할까?
이는 매우 알기 어렵다.
왜냐하면, 핫스팟이 OS 위에서 bite code를 실행할 때 template interpreter를 사용하기 때문이다.
JIT compiler와 똑같이 최종 실행 코드를 Runtime에 생성하기 때문에, 소스 코드에서 break point를 직접 설정할 수가 없다.
핫스팟은 개발자가 인터프리터를 디버깅할 수 있도록 다음 매개 변수를 제공한다.
-XX:+TraceBytecodes -XX:StopInterpreterAt=<n>
- 일련변호 <n>에 해당하는 바이트코드 명령을 만다면, 프로그램 실행에 끼어들어 break point를 추가한다.
- 인터프리터 코드를 디버깅하려면 java 명령에 위 매개변수를 추가하면 된다.
(우린 이렇게 하는 대신, IDE 기능을 사용할 것이다.)
🤯 뭔 소리야
위 내용이 잘 이해가 안 가서 한참을 읽었는데, 순서대로 정리해보자.
일반적인 자바 프로그램 실행은 다음과 같은 순서로 이루어진다.
자바 소스코드(.java) → 바이트코드(.class) → JVM에서 실행
그런데 핫스팟 JVM은 실행 시점(runtime)에 실제 실행할 코드를 만든다.
즉, JIT 컴파일러처럼 동적으로 코드를 생성하는 것이다.
보통 디버깅할 때는 소스코드에 직접 break point를 찍는데,
template interpreter가 runtime에 코드를 생성하니까 미리 break point를 설정하는 게 어려워지는 것이다.
이는 핫스팟이 위의 디버깅 옵션을 제공함으로써,
JVM이 실시간으로 코드 만들어가는 상황에 "n번째 동작에서 잠시 멈춰"라고 명령하는 것과 같다.
📌 디버깅 해보기
위 설정을 모두 완료했다면, 핫스팟 프로젝트를 수정, 컴파일, 디버깅할 수 있다.
핫스팟 가상 머신 진입점인 JavaMain()에 break point를 설정하고, 디버그 모드로 프로젝트를 실행해보자.
그러면 우리가 설정했던 "-version" 매개변수가 올바르게 입력되는 것을 확인할 수 있다.