#chiroito ’s blog

Java を中心とした趣味の技術について

コンテナ時代における最新のJava&JVM監視

私は、OpenJDKのCommitter業や仕事でミドルウェアのSolution Architectとして活動している関係上、最近はコンテナ上でJavaアプリケーションを動かすことが非常に多いです。

KubernetesでJavaアプリを監視する場合には、Elasticsearch+KibanaやPrometheus+GrafanaなどでログやMBeanを監視する方法が一般的に行われています。 Java 11では有償JDKに含まれていた機能がOpenJDKへ寄贈され、JDK Flight Recorder (JFR)として生まれ変わりました。JFRはJVMの内部の情報やその上で動くJavaアプリケーションの様々な情報をほとんど負荷無く記録し、ファイルとして取得できます。このファイルをJDK Mission Controlなどのツールを使って確認し、これまでより詳細に分析できます。

これまででも、コンテナ環境においてもJFRを使用できました。しかし、記録の開始・停止などの管理やファイルとしての取得などを運用する環境を効率よく構築するのは非常に大変でした。

そこで誕生したのが Cryostat (旧名:ContainerJFR)です。Cryostatはコンテナ上で動いているJavaアプリケーションに接続して、JFRを管理します。また、JFRの情報分析結果の簡易レポートの表示やJFRファイルのダウンロードができるWeb UIが用意されている他、JFRで記録された情報をGrafanaへ取込できるため、問題発生時に非常に役立つツールになっています。これはKubernetes Operatorも用意されており、環境の構築も簡単にできます。

f:id:chiroito:20210622105326p:plain
JFRの情報をGrafanaで確認

今回は、Kubernetes上にCryostatをインストールする方法、サンプルのJavaアプリケーションを監視する方法、分析に役立つツールへのアクセス方法を紹介します。

また、今回は用意した環境の都合上、KubernetesとしてOpenShiftを使用します。

今回の環境

  • OpenShift 4.5のクラスタ(Azure Red Hat OpenShift、Red Hat OpenShift Service on AWSも可)
  • クライアントとなるRHEL 8.2

OpenShiftへCryostatをインストール

Kubernetes上でCryostatを使う場合にはCryostat Operatorを使用すると便利です。Cryostat OperatorをはOperatorHub.ioに無いため、自らビルドする必要があります。

Operator SDKをインストール

Cryostat OperatorはOperator SDKに依存関係を持っています。そのため、Cryostat Operatorをインストールするには、まずOperator SDKをインストールします。

export ARCH=$(case $(uname -m) in x86_64) echo -n amd64 ;; aarch64) echo -n arm64 ;; *) echo -n $(uname -m) ;; esac)
export OS=$(uname | awk '{print tolower($0)}')
export OPERATOR_SDK_DL_URL=https://github.com/operator-framework/operator-sdk/releases/download/v1.8.0
curl -LO ${OPERATOR_SDK_DL_URL}/operator-sdk_${OS}_${ARCH}
chmod +x operator-sdk_${OS}_${ARCH} && sudo mv operator-sdk_${OS}_${ARCH} /usr/local/bin/operator-sdk

参考:Installation | Operator SDK

Cryostat OperatorをOpenShiftへインストール

Operator SDKがインストールし終わったらCryostat Operatorをインストールしましょう。

まずは、OpenShiftへログインして今回使用したいプロジェクト(名前空間)を指定します。

oc login -u [ユーザ名] -p [パスワード]  [APIのURL]
oc project [プロジェクト名]

次に、Cryostat Operatorのビルドに必要なmakegoをインストールします。そして、Cryostat Operatorのソースコードを落としてからmakeコマンドでビルドしてインストールします。

sudo yum install -y make go
git clone https://github.com/cryostatio/cryostat-operator.git
cd cryostat-operator
make cert_manager
make deploy_bundle

参考:GitHub - cryostatio/cryostat-operator: An OpenShift Operator to facilitate setup and management of Cryostast and expose the Cryostat API through Kubernetes Custom Resources.

Cryostat Operatorがインストールされたことを確認します。

oc get deployments cryostat-operator-controller-manager

以下のように表示されていれば成功です。

NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
cryostat-operator-controller-manager   1/1     1            1           1h

OpenShiftの場合はWebコンソールのInstalled Operatorsで表示される一覧に追加されています。

f:id:chiroito:20210622104835p:plain
Webコンソールで確認

Cryostatをデプロイ

Cryostat Operatorのインストールが終わったので、Cryostatをデプロイしてみましょう。

cat <<EOF > cryostat.yaml
apiVersion: operator.cryostat.io/v1beta1
kind: Cryostat
metadata:
  name: cryostat-sample
spec:
  minimal: false
EOF
oc create -f cryostat.yaml

参考:cryostat-operator/config.md at main · cryostatio/cryostat-operator · GitHub

ここではmetadata.nameに名前を設定します。spec.minimalは最小構成でインストールするかどうかを表します。最小構成はCryostatのみをインストールします。最小構成ではカスタマイズされたGrafanaとその関連ツールはインストールされません。カスタマイズされたGrafanaがあると便利なのでfalseにする事をオススメします。

Cryostatがデプロイされたことを確認します。

oc get deployments cryostat-sample

以下のようになっていればデプロイ成功です。

NAME              READY   UP-TO-DATE   AVAILABLE   AGE
cryostat-sample   1/1     1            1           20h

こちらもOpenShiftの場合はWebコンソールのInstalled OperatorからGUIで作成できます。Cryostatタブを選択するとCryostatの一覧を表示します。そこでCreate Cryostatと言うボタンがあるので、ここから作成画面へ遷移します。

f:id:chiroito:20210622110434p:plain
Cryostatのページ

Cryostatを作成するページに移動するので、必要な情報を入力してCreateボタンを押せば作成されます。 ここではCLIで作成したとおり、名前をcryostat-sample、最小構成をfalseにして作成しています。

f:id:chiroito:20210622110627p:plain
Cryostatを作成

ここまでの作業が完了するとCryostatを使用する準備が完了しました。Webコンソール上のトポロジでは以下のようになっています。

f:id:chiroito:20210622113630p:plain
Cryostatインストール後の状況

環境構築はこれで終わりです。JFRを効率よく管理する環境が簡単に作れました。

Cryostat Web UIへのログイン

Cryostatは便利なWeb の UIを提供しており、デプロイが正常に完了していると、このUIへアクセスできるようになっています。このUIへアクセスするにはそのURLが必要になりますが、URLはocコマンドやWebコンソールから確認できます。

ocコマンドでURLを取得する場合は、先ほど作成したcryostat-sampleに対して以下のように実行します。

oc get route cryostat-sample

出力結果のHOST/PORTがCryostatのUIのURLになります。

NAME              HOST/PORT                                                                    PATH   SERVICES          PORT   TERMINATION   WILDCARD
cryostat-sample   cryostat-sample-jmx-sample3.apps.cluster-2ab2.2ab2.sandbox1193.opentlc.com          cryostat-sample   8181   reencrypt     None

また、OpenShiftのWebコンソールから確認するには、Installed OperatorsのCryostatからCryostatタブを選択しcryostat-sampleをクリックします。

f:id:chiroito:20210622155809p:plain
Cryostat一覧

右下のApplication URLにURLが記載され、リンクもされているのでクリックします。

f:id:chiroito:20210622155838p:plain
Cryostatリソースの詳細

このUIへアクセスすると以下のようにトークンを求められます。

f:id:chiroito:20210622155623p:plain
Cryostat UIへログイン

以下のコマンドを事項するとトークンが得られます。

oc whoami --show-token

出力されたトークンを入力してログインしましょう。

監視対象のアプリケーションをデプロイ

それでは、Cryostatで監視するJavaアプリケーションをデプロイしましょう。

CryostatではJava Management Extentions (JMX)を介してJFRを管理します。そのため、アプリケーションではJVM引数でJMXを有効化しなければなりません。JMXのポートはデフォルトで9091を監視します。他のポート番号にする場合はServiceのspec.ports.namejfr-jmxにして下さい。また、Java Discovery Protocol (JDP)でもJavaアプリケーションを検知できます。こちらについては難易度が高いのでServiceの名前にだけ注意するようにしましょう。

参考:GitHub - cryostatio/cryostat: A container-native JVM application which acts as a JMX bridge to other containerized JVMs and exposes a secure API for producing, analyzing, and retrieving JDK Flight Recorder data from your cloud workloads.

今回はサンプルイメージを用意しましたのでそのイメージを使用します。このサンプルでは以下のようにJMXを有効化しています。

-Dcom.sun.management.jmxremote.port=9091
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false

設定の簡素化のために認証は無効化していますが、CryostatはJMXの認証にも対応しているので必要な場合は設定して下さい。

認証の設定方法:https://github.com/cryostatio/cryostat#monitoring-applications

サンプルアプリをデプロイ

それではサンプルアプリケーションをデプロイします。

cat <<EOF > myapp.yaml
apiVersion: v1
kind: List
items:
- apiVersion: apps/v1
  kind: Deployment
  metadata:
    labels:
      app: jmx-container-sample
      app.kubernetes.io/component: jmx-container-sample
      app.kubernetes.io/instance: jmx-container-sample
    name: jmx-container-sample
  spec:
    replicas: 1
    selector:
      matchLabels:
        deployment: jmx-container-sample
    template:
      metadata:
        labels:
          deployment: jmx-container-sample
      spec:
        containers:
        - image: 'quay.io/cito/jmx-container-sample:1.0-SNAPSHOT'
          name: jmx-container-sample
          ports:
          - containerPort: 8080
            protocol: TCP
          - containerPort: 9091
            protocol: TCP
- apiVersion: v1
  kind: Service
  metadata:
    labels:
      app: jmx-container-sample
      app.kubernetes.io/component: jmx-container-sample
      app.kubernetes.io/instance: jmx-container-sample
    name: jmx-container-sample
  spec:
    ports:
    - name: 8080-tcp
      port: 8080
      protocol: TCP
      targetPort: 8080
    - name: jfr-jmx
      port: 9091
      protocol: TCP
      targetPort: 9091
    selector:
      deployment: jmx-container-sample
EOF
oc apply -f myapp.yaml

サンプルアプリケーションがデプロイされたことを確認します。

oc get deployments jmx-container-sample

以下のように出力されればデプロイされています。

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
jmx-container-sample   1/1     1            1           20h

JFRの管理

サンプルアプリケーションを起動すると、Cryostatによって自動で検知されJFRが有効なJavaアプリケーションと言うことで管理の対象となります。

サンプルアプリケーションがCryostatで管理の対象となったことを確認します。

oc get flightrecorders

以下のようにサンプルアプリのpod名と同じものがあれば成功です。今回はjmx-container-sampleで始まるものがサンプルアプリケーションになります。

NAME                                    AGE
cryostat-sample-5b674ff96c-rshvn        4m10s
jmx-container-sample-8649b7699d-9v2fl   4m21s

CryostatではCryostat自身も管理されるため、先程デプロイしたcryostat-sampleのpodも含まれます。

また、この情報もWebコンソールから確認できます。Installed OperatorsCryostatからFlight Recorderタブを選択して下さい。

f:id:chiroito:20210622123407p:plain
FlightRecorderrを確認

JFRを起動

CryostatはRecordingというリソースとCryostat Web UIのどちらかでJFRを起動できます。 Recordingリソースでは記録したいJFRのイベントを詳細に指定しなければなりませんが、記録の情報はKubernetesのリソースとして管理されます。Cryostat Web UIではRecordingリソース同様にイベントを詳細に指定することもできますが、イベントのテンプレートも指定できます。しかし、こちらの方法ではRecordingリソースは生成されないためKubernetes上で記録を管理できません。

リソースとしての起動

全てのイベントを記録するのは非常に膨大になるので、試しに今回はサンプルアプリケーションに対し通信の入出力の情報を記録を試みます。対象となるJFRはspec.flightRecorder.nameに先程のoc get flightrecordersで出力されたものを指定します。記録するイベントはspec.eventOptionsで指定します。どの様なイベントがあるかはこちらを参照して下さい。

cat <<EOF > recording.yaml
apiVersion: operator.cryostat.io/v1beta1
kind: Recording
metadata:
  name: cont-recording
spec:
  name: cont-recording
  eventOptions:
  - "jdk.SocketRead:enabled=true"
  - "jdk.SocketWrite:enabled=true"
  duration: 0s
  archive: true
  flightRecorder:
    name: jmx-container-sample-8649b7699d-9v2fl
EOF
oc create -f recording.yaml

参照:cryostat-operator/api.md at main · cryostatio/cryostat-operator · GitHub

記録が開始できているかを確認します。

oc get recordings

出力結果は以下のようになります。

NAME             AGE
cont-recording   25h

Cryostat Web UIで起動

Cryostat Web UIでJFRを起動する場合には、Cryostat Web UIにログインしてから左側のメニューからRecordingsを選択します。右側にFlightRecorderとして管理されているリソースのリストがあるので、そこからJFRを起動したいJVMを指定します。サンプルJavaアプリをデプロイした場合はjmx-container-sampleから始まるJVMを指定して下さい。そうすると既に動いているJFRが表示されます。

f:id:chiroito:20210622155457p:plain
記録の一覧表示

この例では起動時に起動したのでそのJFRが記載されています。

Cryostat Web UIでJFRを起動

JFRを起動するにはCreateボタンを押して、作成画面へ行きます。

f:id:chiroito:20210622164434p:plain
JFRを起動

この画面では記録の名前や記録の期間、記録するイベントなどJFR自体の設定が行なえます。最低限はこの3つを設定すれば大丈夫です。Continuousのチェックボックスは継続して記録を取り、ファイルをダンプする時点から逆算して指定した期間分の情報が取れるようになっています。

オススメの設定は、名前は人間が分かりやすい名前が良いです。期間はContinuousを設定しつつ、障害を検知してからどれくらいの時間あればJFRファイルをダウンロードできるかを逆算しておき余裕を持って設定しておきましょう。イベントは通常はContinuousを選択し、具体的なトラブルがあったらProfilingやAlllを設定しましょう。

設定内容の詳細についてはJFRのドキュメントを参照下さい。

Grafanaへアクセスする

Cryostatの便利なポイントの1つは、JFRの内容をGrafanaで見られることです。JFRの内容をGrafanaで見るには、Cryostat Web UIのRecordingタブから見たいRecordingsの右側にあるメニューをクリックしてView in Grafana ...を選択します。そうすると、Grafanaのページへ遷移します。

f:id:chiroito:20210623110710p:plain
JFRをGrafanaで開く

Grafanaにアクセスすると認証情報の入力が求められます。この認証情報は、以下のコマンドを実行すると得られます。ユーザ名はadminですが、パスワードは環境によって異なります。

oc get secret cryostat-sample-grafana-basic -o json | jq -crM .data.GF_SECURITY_ADMIN_USER | base64 -d
oc get secret cryostat-sample-grafana-basic -o json | jq -crM .data.GF_SECURITY_ADMIN_PASSWORD | base64 -d

Grafanaへログインが成功すると、通常のGrafanaと同じように操作ができます。JFRのイベントはjfr-datasourceというデータソースに含まれています。好きなようにダッシュボードなどを作成してみて下さい。

f:id:chiroito:20210622105326p:plain
ダッシュボードの例

JFRをダウンロード

Grafanaは非常に便利ですが、JFRの情報を詳細に分析するには、現時点では専用のJDK Mission Controlの方が優れています。 Grafanaを使った分析で気になる点が見つけられたら、これまで通りJDK Mission Controlを使ってより詳細に分析しましょう。

Cryostatでは取得していたJFRのファイルをダウンロードできます。Cryostat Web UIのRecordingsでダウンロードしたいRecordingの右側をクリックするとメニューが出るので、Download Recordingをクリックしてダウンロードします。

f:id:chiroito:20210623110710p:plain
JFRファイルをダウンロード

ダウンロードしたファイルはJDK Mission Controlで開けますので、JMCを使用して詳細に分析ができるようになります。

f:id:chiroito:20210623111332p:plain
JDK Mission Control

最後に

これでコンテナ上において JDK Flight Recorder をより便利に使うためのツールである Cryostat の紹介は終わります。 CryostatとCryostat Operatorの開発はGithub上で行われているので、興味がある場合にはぜひスターを付けて下さい。

github.com

github.com

また、Cryostatの開発者が書いた記事の翻訳もありますので興味のある方はぜひご覧下さい。

rheb.hatenablog.com

JJUG CCC 2020 Fall で「パフォーマンスのトラブルシュート入門」という内容で喋ってきました

2020年11月7日(土)に開催された JJUG CCC 2020 Fall にて「パフォーマンスのトラブルシュート入門」というタイトルで話をしてきました。

動画はこちらです。


CCC 2020 Fall A02-パフォーマンスのトラブルシュート入門

スライドはこちらになります。

speakerdeck.com

Twitterの履歴はこちらです。#jjug_ccc_a というハッシュタグの 11:00~12:00 の間が私のセッション分になります。

twitter.com

Twitter などでいただいた質問は以下になります。当日回答した質問もありますが、質問部分は公開されないと思うのでこちらに記載しておきます。類似の質問はこちらでまとめさせていただきました。

JFR Stream で、jfrダンプがしなくてもモニタリングできるようになったらいいですね。でもEventの始まりと終わりで異常処理考慮しなくちゃいけないの大変ではありませんか?

JFR Event Stream では、記録の終了後にダンプされた JFR ファイルを読み込む方法に加え、稼働中の記録を内部的に作成しているチャンクファイルから読み込む方法があります。後者の方法を使うことで JFR ファイルをダンプしないでもモニタリングできるようになっています。このチャンクファイルはデフォルトで 1 秒ごとに記録を書込んでいます。そのため JFR Event Stream で読み込む際にはイベントが記録されてから最大で 1 秒遅れるためリアルタイムには処理されないため注意してください。 イベントの書込み処理の実装では、例外処理も気にする必要があります。イベントを書込む処理は基本的には try - catch -finally で囲まれていることが多いので、その場合は finally できちんと書込むようにして下さい。参考までに私が開発している Jfr4Jdbc では JDBC のライブラリ群が投げる SQLException に加えて、非チェック例外である RuntimeException も気にするように実装しています。

JFRが使えるのはJDLK11からですか?

OpenJDK で JDK Flight Recorder が使えるのは OpenJDK 11 からです。その後、OpenJDK 8 update 262 にバックポートされました。前身である Oracle JDK の Java Flight Recorder は HotSpot では Java 7 update 40 から使用できます。Oracle JDK の JRockit の場合はそれ以前のバージョンでも使用できます。

クラウドのサーバーレス環境でもフライトレコーダーって使えるんですか?
コンテナ系のサービスだと、どう監視するのがいいんだろ……?

Java の標準機能のためどの様な環境でも使用できます。サーバレスやコンテナだから使えないという機能ではありません。ただし、ダンプファイルをどこかの永続化ストレージに格納する必要があるので、サーバレスのプロセスが停止する際にオブジェクトストレージなどへアップロードする処理が必要です。

Kubernates や OpenShift などのコンテナオーケストレーションツールを使用する場合は Red Hat から JFR のオペレータが出ているためこちらをご利用下さい。コンテナオーケストレーションツールを使用していない場合は、永続ストレージに JFR や収集した情報を記録する様にして下さい。

この辺の内容のまとまった情報のあるおすすめの書籍などあれば

概要や使い方を紹介した資料はいくつかありますが、JFR の全体や内部構造などのまとまった資料はありません。様々なイベントで Oracle 社のエンジニアが発表している資料が良いと思います。これらの動画は Youtube などで公開されています。

JFR の利用が原因でパフォーマンスが劣化するといったことは考えられますでしょうか?
フライトレコーダーって本番で動きっぱなしにしてもリソース的に大丈夫なものなんでしょうか?(環境次第な気もしますが)
パフォーマンス計測をし続けることにどの程度負荷かかるんだろな

JFR のイベントを書込む処理を記述していても、JFR が動いていなければパフォーマンスは劣化しません。これはイベントの書込み処理などイベントに関わるコードが JIT コンパイラによって取り除かれるように実装されているためです。 JFR を動かす場合には default と profile という標準設定群があります。一般的な使用方法では default で良く、さらに詳細に分析したい場合には profile を使用します。default では一般的な使用方法では 1~2% の負荷になります。この負荷はイベントを書込む頻度などにもよります。profile ではもう少し大きな負荷になるため、本番環境での使用は良く確認してから試しましょう。

JFRは常に有効にしておくものでしょうか?それとも何か発生したら調査のために有効にするものでしょうか?

JFR は常に有効にしておくことをオススメします。JFR は内部的に循環バッファを使用しています。そのため、新しいイベントが書込まれると過去の分はどんどん削除されていきます。そのため、問題が起きたときにダンプすれば過去に遡ってバッファに残っている分の情報が入手できます。また、正常時のダンプも忘れないように取得しておきましょう。

FR カスタム Event を備えたフレームワークはどんなものがあるのでしょうか

Oracle 社の Oracle WebLogic Server (WLS) と私が開発している Jfr4Jdbc があります。WLS では Java EE に関するイベントやそれに付随するシステムのイベントを書込んでくれます。これは、特別な処理は不要で WAR ファイルや EAR ファイルをデプロイするだけで各種イベントが記録されます。 また、Jfr4Jdbc は JDBC に関するイベントを書込みます。これらのイベントにはコネクションの接続や切断、クエリの実行、コミット、ロールバックといった JDBC の API を呼び出すだけで記録されるようになります。また、コネクションプールの使用数や割り当て待ち数といった情報も記録されます。これらの情報を取得するために必要な事はデータソースをラップするか、JDBC の 接続 URL に jfr: を追加するだけです。

JFRで記録できるイベントの一覧はどこかで確認できるんでしょうか?

JFR で記録できるイベントは日々追加されています。一覧が記録されている情報はありません。一覧を確認するには、JFR を遊行した Java プロセスを起動し、JDK Mission Control で接続して記録を開始し始めるときに一覧を出せます。また、一度 JFR ファイルをダンプし、その情報に含まれる[環境]-[記録]から確認できます。

JFRが使えないレガシー環境ではどれがおすすめですか?

今回の講演でも紹介しましたが、JFR が使えないレガシー環境では、OS、JVM、アプリケーションできちんとログやメトリクス、リソースといった情報を取るようにしなければなりません。そのためには JVM に標準で含まれているログ機能や JMX/MBean といった物を有効活用するようにします。また、アプリケーションサーバ(フレームワーク含む)がきちんと情報を得られる物を選択しましょう。

OS情報の計測によく使うツールやノウハウがありましたら教えてください。

Linux の場合は iostat/mpstat/sar など OS が使用するリソースの情報を収集系のツールがあります。これらのツールを適切に使うことがオススメです。CPU等の情報は、まとめて取るのではなく論理CPUやディスクなどを個別に収集するようにオプションを設定しましょう。時刻情報が付かないツールも多いので、収集時は時刻情報も含まれるよう工夫すると良いです。 以下がサンプルです(最近動かしてないので動くかは不明ですが・・・)

RESULT_CASE=sample
DATE_FORMAT="%Y %m %d %H %M %S"
INTERVAL_SEC=10
TIMES=1080
mpstat -P ALL ${INTERVAL_SEC} ${TIMES} | awk -v df="${DATE_FORMAT}" '(NR==3 || (NR>3 && /^[0-9].+[0-9]$/)){print strftime(df), $0}' >> ${RESULT_CASE}_mpstat_`date +\%Y\%m\%d`.log &
vmstat -n -S m ${INTERVAL_SEC} ${TIMES} | awk -v df="${DATE_FORMAT}" '(NR>=2){print strftime(df), $0}' >> ${RESULT_CASE}_vmstat_`date +\%Y\%m\%d`.log &
iostat -mx -d -p ALL ${INTERVAL_SEC} ${TIMES} | awk -v df="${DATE_FORMAT}" '(NR==3 || (NR>3 && /.+[0-9]$/)){print strftime(df), $0}' >> ${RESULT_CASE}_iostat_`date +\%Y\%m\%d`.log &

The changes of JVM Specifications - JVM仕様の変更点

Java 8 以降の Java 仮想マシンの仕様のうち、各バージョンの差分を洗い出しました。the が that になったり、item が entry になる変更で、文の意味が変らない変更は除いています。

I listed the differences between each version of the Java Virtual Machine specification for Java 8 and later. I excluded changes that do not change the meaning of the statement, such as the changing of "the" to "that" or the changing of "item" to "entry".

元の情報はこちらです。

The original information is here.

docs.oracle.com

The changes of "The Java Virtual Machine Specification Java SE 15 Edition"

The changes of "The Java Virtual Machine Specification Java SE 14 Edition"

Nothing

The changes of "The Java Virtual Machine Specification Java SE 13 Edition"

The changes of "The Java Virtual Machine Specification Java SE 12 Edition"

The changes of "The Java Virtual Machine Specification Java SE 11 Edition"

The changes of "The Java Virtual Machine Specification Java SE 10 Edition"

The changes of "The Java Virtual Machine Specification Java SE 9 Edition"