---
title: 'JVM 歷史、編譯 JDK'
disqus: kyleAlien
---
JVM 歷史、編譯 JDK
===
## Overview of Content
如有引用參考請詳註出處,感謝 :cat:
Java 不僅僅是開發語言,它也是一系列計算機應用、規範組成的 **體系**,該技術體系提供了完整的軟體開發、跨平台佈署...等等的環境(像是包括嵌入式、移動裝置、服務器)
[TOC]
## Java 技術體系
運行在 JVM 上的語言除了 Java 之外,還有 Kotlin, Clojure, JRuby, Groovy... 等等,這些都算是 Java 技術體系中的一員;一般來講 Java 技術體系包括
* Java 程序設計語言
* Java Library API
* 各平台的 JVM 實現
* Class 文件格式
* 其他開源第三方 Library
### 照組成劃分
* 根據 Java 各個組成部分的功能,**JDK(`Java Development Kit`)是 Java 程序開發的最小環境**
* Java 程序設計語言
* Java Library API
* 各平台的 JVM 實現
:::info
* **JRE (`Java Runtime Environment`)是指**?
是指 Java Library API 中的 **`Java SE`、`JVM` 這兩部份的組成**,只要有這兩個元素就可以運行 Java 程序
> 
:::
### 照服務領域劃分 - JCP/JSR
* 根據 Java 的技術服務領域,又可分為 4 個主要產品;而這些 API 都是業界共同制定的標準,業界代表可參與 **JCP** (`Java Community Process`)、審核、投票,決定平台元件、特性、應用程式介面... 等等
> 而訂製出來的標準會以 **JSR**(`Java Specification Requests`)作為正式標準規範文件,不同的技術解決方案都會給予一個編號
>
> 在 JSR 規範的表準之下,各廠商都可以實現自己的成品!(相當於每個廠商都有不同的實作)
* **Java Card**
支持 Java 小程序(`Applets`),它運行在 **小內存設備上的平台**
> 智能卡、支付卡、門禁卡...等等
* **Java ME**(`Micro Edition`)
支持 Java 程序運行在 **移動端上的平台**,它的特點是它對 Java API 進行精簡,並加入移動端的特性
> 運行在小型嵌入設備上
:::info
* JDK 6 之前稱為 J2ME
:::
:::warning
* 然而 Android 平台上並不使用 Java ME,**Android 平台使用 JSE**
:::
* **Java SE**(`Standard Edition`)
支持 **桌面等級應用**,它提供完整的 Java 核心 API
:::info
* JDK 6 之前稱為 J2SE
:::
* **Java EE**(`Enterprise Edition`)
支持 **多層級架構的企業應用**,其特色是除了標準 API 之外,還有大量的擴充 API
> ERP, MIS, CRM... 等等應用
:::success
* JDK 6 之前稱為 J2EE
* JavaEE 6 使用的規範是 [**JSR 316**](https://jcp.org/en/jsr/detail?id=316);而 Servelet 3.0 規範則是 [**JSR 315**](https://jcp.org/en/jsr/detail?id=315),Servelet 在 Java EE 中,主要是在接收客戶端的請求
:::
## Java 歷史簡介
這裡做個 Java 歷史的簡介
### Sun 時代
* 最一開始 Java 是 **`Sun` 公司的產品**,它的重點事蹟如下
* 原始名稱為 Oak,後來才改名為 Java
* 開發理念為 **Write Once, Run Anywhere**
* Sun 時代 版本差異
| JDK 版本 | 說明 | 技術重點補充 |
| - | - | - |
| 1.1 | **有 Java 中最基礎的技術支持**:Jar 文件格式、JDBC、JavaBeans、RMI... 等等 | InnerClass, Reflection 的出現 |
| 1.2 | JDK 重要里程碑,**依照服務將其分為 J2SE, J2EE, J2ME... 等等** | EJB, Java Plugin, Java IDL, Swing, Collection.. (**JVM 中新增 JIT 技術,並出現 `Classic VM`, `HotSpot VM`, `Exact VM`**)|
| 1.3 | **HotSpot VM 的出現**,並預設為 Java 默認虛擬機 | Java 2D API, Java Sound API |
| 1.4 | 開始穩定成熟 | 正則表達式、異常鏈、NIO、異常類、XML 解析... 等等 |
| 5 | 全面 **改由 Java x 發布** | 自動裝箱、泛型、動態註解、枚舉...等等;改進虛擬機中的內存模型(JMM);併發包 |
| 6 | **初步的動態語言支持** | JVM 改進了鎖、同步、GC、類加載...等等 |
| | **Java 開源**,並建立 OpenJDK 組織對源碼進行管理 | - |
| 7 | 由於經濟問題,導致發布延期(最終被 Sun 收購) | Lambda(**函數式編成**), Jigsaw(虛擬機層面的模塊化), COIN... |
:::info
* **到 JDK7 為止,Oracle 公司收購了 Sun 公司**
:::
### Oracle 時代
* Oracle 公司收購後極具商業化風格
* 宣佈 JDK7 維護到 2022 年,在這期間 JDK7 的所有功能都可以在 Mac OS X、Window 上運行
* Oracle 接手後的版本差異
| JDK 版本 | 說明 | 技術重點補充 |
| - | - | - |
| 7 | G1 收集器 | 延後 Lambda, Coin ... 等等項目 |
| 8 | 完成 Lambda, 新的 時間、日期 API, Nashorn JavaScript 引擎支持 | 移除 JVM 中的永久代 |
| 9 | Oracle 堅持 **發展 VM 模塊化技術 Jigsaw** | 增強 JS Shell, JLink, JHSDB... 等等,並統整了 HotSpot 各個模塊的日誌系統,支持 Http2 客戶端 API |
| | 使用敏捷開發 | 在此之後的版本都以 Java x.y 為準 (y 為月份),Java 1.x 變成別名 |
| | JDK 更新週期改為 6 個月一次,並且只有 LTS 版本才會長期支援 | - |
| 10 | 主要是 **內部重構** | 統一源倉庫、GC 界面、JIT 界面 |
| | Google 被告使用 Java API 侵權(並不關虛擬機,針對使用 Java 語言) | 出現 **==Graal 編譯器==** |
| 11(LTS) | **ZGC 革命性出現** | 把 JDK 11 之前的特性開放給 OpenJDK 組織 |
| | 發布分為兩個版本 JDK:商業收費版本(`OracleJDK`)、免費版(`OpenJDK`) | 免費板只支持半年的更新 |
| 12 | Switch 表達式、Java 微測套件(`JMH`)... 等等功能 | - |
:::success
* **Oracle JDK 開始收費了嗎**?
其實不然,這是給使用者自行選擇,在 JDK 11 之後,可以選擇付費(長期支援),或是免費(你要一直更新)
:::
### JDK 開分支策略
* 由於 Sun 公司被 Oracle 公司接管 Java 時是在 Java 6 時期(Java 7 未發布),因此他們改變了管理專案的分支策略
1. 剝離原本要發布 JDK 7 的功能,對外公佈穩定 JDK6 分支(並且 6 可不斷更新)
``` mermaid
graph TD;
JDK6_Main-->JDK7_Dev;
JDK6_Main-->JDK6u1;
JDK6u1-->JDK6u4;
JDK6u4-->JDK6u10;
JDK6u10-->JDK6u26;
```
2. JDK7 經過測試、功能穩定後,發布 JDK7,並同時在 JDK7 發布版上建立 JD8 為下一代做準備
``` mermaid
graph TD;
JDK6-->JDK7_Main;
JDK7_Main-->JDK8_Dev;
JDK7_Main-->JDK7u1;
JDK7u1-->JDK7u4;
JDK7u4-->JDK7u10;
JDK7u10-->JDK7u26;
```
## 編譯 JDK
Java 類中有許多底層方法(Native),了解這些方法的細節實做,可以對於調適 VM 有不小的幫助
### 下載 Java 源碼
* **在 Sun 的後期(`JDK6` 時建立 OpenJDK 組織)將 Java 開源**,而之後使用 OpenJDK 源碼衍生出的多個發行板(`Azul Zulu`、`Oracle JDK`、`Oracle Open JDK`、`AdoptOpenJDK`、`AmazonCorretto`... 等等)
**OpenJDK 中的源碼倉庫,只包含了標準 Java SE 的代碼**,如果須需要額外模塊則須另外下載
:::warning
* 也就是說每個發行板的 OpenJDK 有的功能不盡相同,只能保證一定有 `Java SE` 的實現代碼
:::
:::info
* **在 Java 11 中,OracleJDK 與 OpenJDK 的實現已經非常接近相同**
:::
* 藉此我們可以使用 [**OpenJDK**](https://openjdk.org/) 的版本進行研究,下載源碼方式如下
> **以下載 JDK 12 為例**
1. **透過 Mercurial 代碼管理工具** 下載 JDK 源碼
```shell=
sudo apt install mercurial
hg clone http://hg.openjdk.java.net/jdk/jdk
cd jdk
# 查看指定 tag 並切換指定版本
hg log | grep tag
# 切換到 JDK12+1
hg update -r jdk-12+1
# 確認
hg summart
```
> 
2. **透過 [**github**](https://github.com/openjdk/jdk) 下載 JDK 源碼**
```shell=
git clone https://github.com/openjdk/jdk.git
git tag | grep jdk-12
# 切換到 JDK12+33
git checkout jdk-12+33
# 確認
git branch
```
> 
:::info
* 個人習慣使用 Git、Github,所以採用這個方案
:::
3. 手動下載 [**JDK 壓縮包**](https://hg.openjdk.org/jdk/jdk12):點擊左邊 Browse 再點擊壓縮包(`zip`, `gz`... 等等)
> 
### 編譯 JDK - 環境需求
* **環境需求**:
* **OS 平台**:建議在 Linux or MacOS 上進行編譯,可以少走一些坑
> 目前我使用 Ubuntu 20
* **磁碟空間**:建議準備個 10 G 以上的空間,因為在 **編譯期間會產生不少中間文件** (IR File)
* **路徑**:建議是使用純英文的空間路徑
### 編譯 JDK - 工具需求 Bootstrap JDK
* 編譯 JDK 除了建議環境之外,還需要一些 **系統工具**
* **NativeCode 編譯工具**:也就是 C、C++ 編譯工具,這邊可以使用 `GCC` or `CLang` 編譯工具
:::warning
* `GCC` 版本最低為 4.8
* `CLang` 版本最低為 3.2
:::
```shell=
sudo apt install -y build-essential
gcc --version
```
> 
* **第三方工具**:OpenJDK 還依賴一些第三方編譯工具,可依照需求做安裝
| 工具名 | 庫名 | 安裝包 |
| - | - | - |
| FreeType | The FreeType Project | `libfreetype6-dev` |
| CUPS | Common UNIX Printing System | `libcups2-dev` |
| X11 | X Window System | `libx11-dev`、`libxext-dev`、`libxrender-dev`、`libxtst-dev`、`libxt-dev`|
| ALSA | Advanced Linux Sound Architecture | `libasound2-dev` |
| libffi | Portable Foreign Function Interface Library | `libffi-dev` |
| autoconf | Exetensible PAckage of M4 Macros | `autoconf` |
```shell=
sudo apt install -y libfreetype6-dev \
libcups2-dev \
libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev \
libasound2-dev \
libffi-dev \
autoconf \
build-essential \
zip unzip \
git \
libfontconfig1-dev
```
* **Bootstrap JDK**:
編譯版號為為 N 的 JDK 時,需要有 JDK(N - 1) 版的工具,這種 N-1 版本的 JDK 稱為 Bootstrap JDK;
> 假設現在要編譯 OpenJDK 12,那我們就要先安裝 OpenJDK 11
```shell=
sudo apt install -y openjdk-11-jdk
```
### 編譯 JDK & 參數設置
* JDK 編譯會透過一個 `autoconf` 工具來協助,讓使用者免去關注各個硬體平台差異的 Options 設定(所以請記得安裝該工具)
在編譯 JDK 前我們可以先查看 configure 可以做哪些設定,透過**設定 configure 也會影響到最終 Make 指令編譯出來的結果**
```shell=
# 查看完整可設定的 Options
bash configure --help
```
> 
下表為常用的幾個設定值
| Options | 功能說明 | 補充 |
| - | - | - |
| `--with-debug-level=<level>` | 設定調適等級 | `release`(默認)、`fastdebug`、`slowdebug`... |
| `--enable-debug` | 等於 `fastdebug` 調適等級 | - |
| `--with-native-debug-symbols=<method>` | 指定要調適的符號信息 | `non`、`internal`、`external`、`zipped` |
| `--with-version-string=<String>` | 設置編譯 JDK 的版號 | 這會影響最終編譯出 JDK 的版本訊息(`java --version`) |
| `with-jvm-variants=<variants>` | 編譯特定模式的 HotSpot JVM | `server`、`client`、`core`、`monimal`、`zero`、`custom` |
| `--with-target-bits=<bits>` | 指名要編譯出 32, 64 位元的 JVM | 64 位元的機台上可以交叉編譯出 32 bit 的 JVM |
| `--with-<lib>=<path>` | 用於指明依賴包的具體路徑 | - |
| `--with-extra-<flagtype>=<flags>` | 該設定會作用在 C, C++, Java 代碼編譯時期 | 像是 `cflags`, `cxxflags`, `ldflags`... |
| `--with-conf-name=<name>` | 指定編譯配置名稱 | - |
* **編譯 JDK 12**
1. **設定參數**:
```shell=
bash configure --enable-debug --with-jvm-variants=server
```
:::warning
* **configure 設定失敗?**
當 configure 設定失敗時,它的返回碼就會是非 0(0 才是成功),這時你就需要按照提示去安裝、設置某些必要工具或參數
> 
:::
設置成功後會看到 configure 摘要訊息
> 
2. **執行 make 指令開始編譯**
:::warning
* 如果你執行多次 make 那會產生多個檔案,這時可以用 `make clean` 清理
:::
```shell=
make jdk
```
:::danger
* **JDK 12 編譯失敗**? 出現 `BUILD_TOOLS_LANGTOOLS.vardeps`
這是因為 **JDK12 只支援 4.3 版本以下的 make**,所以我們要找到 `./make/common/MakeBase.gmk` 檔案,並找到 `DependOnVariableHelper` 關鍵字,手動修改程如下
```shell=
DependOnVariableHelper = \
$(strip \
$(eval $1_filename := $(call DependOnVariableFileName, $1, $2)) \
$(if $(wildcard $($1_filename)), $(eval include $($1_filename))) \
$(if $(call equals, $(strip $($1)), $(strip $($1_old))),,\
$(call MakeDir, $(dir $($1_filename))) \
$(if $(findstring $(LOG_LEVEL), trace), \
$(info NewVariable $1: >$(strip $($1))<) \
$(info OldVariable $1: >$(strip $($1_old))<)) \
$(call WriteFile, $1_old:=$(call DoubleDollar,$(call EscapeHash,$($1))), \
$($1_filename))) \
$($1_filename) \
)
```
:::
### 編譯 JDK 建議 - Docker
* 編譯 JDK 最常碰到的是環境問題,每個版本的 JDK 都有偏好環境、處理工具的版本... 等等需求;而我們可以透過以下方法解決
1. 依照 JDK 編譯需求,去改變當前系統的工具、環境(不太推薦)
* 修改 JDK 源碼去符合當前環境
* 下載所需的工具並自己編譯程式安裝到當前平台上
:::info
* 但老實說以上方法確實可以,但卻相當麻煩又不好處理!工具之間的版本相互依賴、過時工具... 等等問題非常不好處理
:::
2. **創建符合編譯 JDK 的環境**(推薦,可以使用 Docker)
* 這個最直覺的就是創建虛擬 OS 平台,再透過該平台來編譯 JDK
:::success
* 這是個很好的辦法,我們 **可以在 Docker 上建立編譯 JDK 所需的環境**
像是 JDK 12 就可以在 `Ubuntu:18.04` 編譯
:::
* 接下來 **使用 Docker 創建一個環境來編譯 JDK 12**
> 安裝請參考官方網站,基礎使用可參考另外一篇 [**Docker 安裝、映像檔使用**](https://hackmd.io/tARjfVCSQTy-aG4PtPo2eQ?view#Docker-%E5%AE%89%E8%A3%9D%E3%80%81%E6%98%A0%E5%83%8F%E6%AA%94%E4%BD%BF%E7%94%A8)
1. **下載 OS 檔案**:
使用 `pull` 下載 `Ubuntu 18.04`
```shell=
# 搜尋對應的版本
sudo docker search ubuntu:18.04
# 下載 `Ubuntu 18.04`
sudo docker pull ubuntu:18.04
# 查看所有 images
sudo docker images
```
> 
2. **建立、運行 OS,並指定本地檔的映射**:
:::info
* 下載請參考上面小節(目前已經下載好 JDK12)
:::
```shell=
# 創建 docker 資料夾
mkdir ~/docker/
# 將原本下載好的 docker 移動到 ~/docker/ 資料夾下
mv ~/Documents/github/Learning/JDK/downloadJDK ~/docker/
# 運行 ubuntu 並將 `~/docker` 資料夾映射到 ubuntu:18.04 中的 `/opt` 資料夾
sudo docker run -it -v ~/docker:/opt/ ubuntu:18.04 /bin/bash
```
3. **下載基礎工具**
```shell=
sudo apt install -y libfreetype6-dev \
libcups2-dev \
libx11-dev libxext-dev libxrender-dev libxtst-dev libxt-dev \
libasound2-dev \
libffi-dev \
autoconf \
build-essential \
zip unzip \
git \
libfontconfig1-dev
sudo apt install -y openjdk-11-jdk
```
4. **改變 Docker Container 中 Ubuntu OS 的設置**:
使其可以運行 JDK12 的編譯
```shell=
# 安裝 gcc 4.8 版本
update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 100
# 安裝 g++ 4.8 版本
update-alternatives --install /usr/bin/gcc gcc /usr/bin/g++-4.8 100
```
> 
5. **設置 JDK config**
```shell=
bash configure --enable-debug --with-jvm-variants=server --disable-warnings-as-errors
```
> 
6. **編譯 JDK 12,並驗證編譯結果**
```shell=
# 譯 JDK 12
make
```
> 
:::warning
* 如果還編譯失敗,可以考慮先執行 `make clean`,並再次執行 `make`
:::
驗證編譯結果確認是 JDK 12
```shell=
./build/linux-x86_64-normal-server-fastdebug/jdk/bin/java --version
```
> 
7. **編譯 JDK image**:
```shell=
make images
ls -laF ./build/linux-x86_64-normal-server-fastdebug/images/
```
> 
## 其他
### 編譯 JDK 目標
* make 除了上述之外(無指定),還可以指定不同目標產生不同作用
| make 目標 | 功能 |
| - | - |
| `docs-image` | 產生 JDK 的文檔 image |
| `test-image` | 產生 JDK 的測試 image |
| `bootcycle-image` | 編譯兩次 JDK,第二次使用第一次的編譯結果作為 Bootstrap JDK |
| `all-image` | 產生所有 image(相當於 `product`, `docs`, `test`)三個編譯目標 |
| `hotspot` | 只編譯 Hotspot 虛擬機 |
| `clean` | 清理 make 指令產生的臨時文件! |
| `dist-clean` | 清理 make、configure 指令產生的文件(相當於回歸到最初的檔案) |
### 編譯 JDK - 產生目錄
* 編譯完 JDK 後會產生以下常見目錄,它們功能如下
| make 結果 | 功能 |
| - | - |
| `buildtools/` | 用於生成、存放編譯過程中使用到的工具 |
| `hotspot/` | HotSpot 虛擬機產生的中間文件 |
| `images/` | 使用 `make *-image` 相關指令產生的 image 檔案 |
| `jdk/` | 編譯後產生的 JDK 文件 |
| `support/` | 存放編譯時期產生的中間文件 |
| `test-results/` | 存放編譯後的自動化測試結果 |
| `configure-support/`、`male-support/`、`test-support/` | 存放執行 `configure`、`make`、`test` 的臨時文件 |
> 
### 重用 Docker 環境
* 在前面範例中,我們有使用 Docker 建立對應符合固有版本 JDK 的 OS 環境,為了將來方便重用,我們可以把這個特定的 OS 儲存
```shell=
sudo docker ps -a
# 提交指定 OS (儲存)
sudo docker commit -m "Ubuntu 18.04 for JDK" 49acf ubuntu:18.04_JDK
# 查看建立好的 image 檔案
sudo docker images
```
> 
### CLion 調適 JDK
* 以下示範使用 CLion 來調適 JDK(當然你也可以傳統一點使用 GDB 命令、VIM 調適)
1. **開啟指定 JDK 專案**
> 
2. 這裡先選擇 **自動產生 `CMakeList.txt`**
> 
但預設的 `CMakeList.txt` 是不能使用的
> 
:::success
* 正確的 `CMakeList.txt` 可以參考 [**ojdkbuild 設置**](https://github.com/ojdkbuild/ojdkbuild),下載專案並複製其中的 `CMakeList.txt` 檔案
:::
3. 接著我們要自己 **建立一個 `CMake Application`**
> 
3. **設定啟動的 `Executable`、`target`、`Program argument`... 等等資訊**
* 設定 `Executable` 為上面編譯好的 Java 程式
> 
* 設定 `Program argments` 如下
```shell=
--version -XX:+TraceBytecodes -XX:StopInterpreterAt=<n>
```
:::info
* `-XX:+TraceBytecodes -XX:StopInterpreterAt=<n>`
該設置值是因為 Hotspot 在主流的操作系統上是採用 **模板解釋器執行字節碼的**(與即時編譯器相同),最終 **執行的匯編代碼都是運行時產生的**
> 藉此無法透過下定斷點來暫停程式,所以需要設置此值
:::
4. 移除 `Before launch` 中的 `Build`
> 
### CLion 調適 JDK - compile-commands
* 在 jdk 編譯完成(完成 `make`、`make images`)後,使用 CLion 開啟 JDK 調適還有另一種方法([**參考**](https://blog.jetbrains.com/clion/2020/03/openjdk-with-clion/))
1. 執行以下指令 **產生 `compile_commands.json` 檔案**
```shell=
make compile-commands
```
:::danger
* **並不是每一版本的 JDK 都有這個指令**,我目前是切換到 **jdk-12+33** TAG 上
:::
2. **使用 CLion 開啟 `compile_commands.json` 檔案**
> 
3. 執行 `Tools` -> `Compilation Database` -> `Change Project Root` 功能,**切換專案根目錄**
> 切換到原 `jdk` 目錄
4. **建立 `Custom Build Targets`**:**設置 Build、Clean 工具**
> 
* **Build 工具設置**
| 欄位 | 設定 |
| - | - |
| name | `linux_x86_64-server-fastdebug_make` |
| Program | `make` |
| Arguments | `CONF=linux-x86_64-server-fastdebug make` |
| Working directory(jdk 位置) | `/home/alien/docker/downloadJDK/jdk_github/jdk` |
> 
* **Clean 工具設置**
| 欄位 | 設定 |
| - | - |
| name | `linux_x86_64-server-fastdebug_clean` |
| Program | `make` |
| Arguments | `CONF=linux-x86_64-server-fastdebug clean` |
| Working directory(jdk 位置) | `/home/alien/docker/downloadJDK/jdk_github/jdk` |
> 
5. **建立 `Custom Build Application`**:
同樣設置 `Executable`、`target`、`Program argument`... 等等資訊,並移除預設的 `Build launch` 的 `Build` 選項
> 
* 設置完後運行 JDK,就可以看到程式的運行式
> 下圖是 `java` 指令的入口:`java.c`#`JavaMain`
>
> 
### CLion - Signal: SIGSEGV 錯誤
* 使用 CLion 調適 JDK 時有 **發生 `Signal: SIGSEGV` 錯誤**,這是因為 Ubuntu 預設使用 GNU 幫 C、C++ 進行編譯,所以會產生這個錯誤
所以須做以下
1. 在 `home` 目錄下創建 `.gdbinit` 文件
```shell=
touch ~/.gdbinit
```
2. 編輯 `.gdbinit` 文件,並撰寫內容
```shell=
# 編譯 .gdbinit 檔案
vim ~/.gdbinit
# 以下為內容
handle SIGSEGV pass noprint nostop
handle SIGBUS pass noprint nostop
```
> 
## Appendix & FAQ
:::info
[**JDK 參考**](https://juejin.cn/post/7068473519415230495)
:::
###### tags: `JVM`