Java 14とJava 15の新機能についてJJUG CCC 2020 Springで話す予定だった内容が中止になり、Java 15のリリースも近づいてきたので新たに資料を加筆修正して以下のLINE Developer Meetupで発表しました!
本イベントでは話す予定ではない部分も(JJUG CCCで話す予定だったので)資料にはあり、オンラインでの発表だったということもあり原稿も準備していました。そこで折角ですので情報保障を兼ねて共有したいと思います。情報保障についてはこちらを見てみてください。
なお、今回はかなり特殊な事例であり、基本的には原稿が公開されるケースは少ないと考えています。LINE Developer Meetupでは他の分野でもスライドに書かれていない内容を聞くことができるので、ぜひイベントを聴講してみてください :)
なお、スライドは以下から確認できます。また、スライドおよび原稿は元々markdownで記載しているため、はてな記法ではそのまま表示されています。
www.slideshare.net
ではお楽しみください:)
LINEでソフトウェアエンジニアとして働いている久保田祐史と言います。現在は全社的なKafkaのプラットフォームを提供しているチームで働いています。
(ここからは端折る)
これまでの仕事上、JVMのトラブルシューティングに関わることが多かったため、過去にはOpenJDKやicedteaへのコードのコントリビューションだとか、JavaOneでのプレゼンテーションをしてきました。
またJava(JDK) Flight RecordのOSS化で役目をある程度果たしてしまいましたが、HeapStatsというJVMのGCやクラス参照情報やヒープ情報の収集、そしてリアルタイムデッドロック検知等を行う、トラブルシューティングツールの開発も行っていました
私はよくJJUG CCCという国内のJavaのカンファレンスでGCやJVMに関するセッションの他に、専らJavaの新機能について紹介することが多いのですが、今日も同じようにJava 14とJava 15で利用可能な新機能と変更点を紹介していきたいと思います。
新機能を中心に話すので非互換性については今回は話しません。
注意事項として、本資料は [`jdk.java.net`](http://jdk.java.net) からダウンロードできる`JDK 14.0.2` および`JDK 15 (build 32)`で極力動作確認していますが、全ての確認はできていませんのでコードを修正する前に動作確認してください
実装は各Javaのアップデートリポジトリ(http://hg.openjdk.java.net/jdk-updates/jdk14u/、http://hg.openjdk.java.net/jdk-updates/jdk15u/)、そして最新リポジトリ(http://hg.openjdk.java.net/jdk/jdk)で確認しています
さて、それでは見ていきましょう。
Java 14と15ではこのような数だけ変更がありました。
Java Enhancement Proposals、JEPは新機能レベルの変更、Compatibility and Specification Reviewsは非互換性を伴う変更です。
これ以外にバグ修正も行われていますが、基本的にバージョンアップ時でしか行われない変更はこの2つです。
ただし最近はバックポートという形で新機能レベルの追加がマイナーバージョンアップデートで行われることがあります。つい先日JDK Flight RecorderがJava 8に導入されたのもバックポートの一つです。
いずれにせよバージョンアップデートが半年ごとになりましたが、アップデートの数は今でもあまり変わらず多い状態です。
先程言いました通り、今回は新機能について話しますので、ここでは追加されたJEPを紹介していきます。
まずはじめに言語レベルやAPIレベルの変更について紹介していきます。Java 14も15もこの変更が最も多数を占めています。
ここからは新機能を一つずつ説明していきます。
パターンマッチングがJava 14からプレビュー機能として導入されました。なおここで言う「パターンマッチ」は正規表現のではなく、オブジェクトの型、つまりクラスです。
今まではBeforeで示されている通り `instanceOf` で型を比較してからわざわざキャストする必要がありましたが、今後はAfterで示されている通りキャストする必要なくそのままダイレクトに処理を書くことができます。意外に細かい修正ですが、これによってコードがスッキリすることで読みやすさやメンテナンスのしやすさが想像以上に改善されました。
将来的には switch 文でも同じように記載できるようにすることが展望としてありますが、現在のところはドラフト状態でまだ時期も内容も定まっていません。
Java 15では変更なしでSecond Previewとなっています。通例的には大きな問題がなければ次のJava 16で標準機能となる予定です。
(注意)上のスライドの下部コードは「どのようなフィールドやメソッドが生えるか」のイメージ図を記載していたため明記していませんでしたが、クラス宣言は以下のようにするのがより正確です。
public final class TestRecord(String s, int i) extends java.lang.Record {
(注意ここまで)
Java言語仕様において長らくの問題(課題)であった「データを表したいだけなのに、書く必要があるコード量が多すぎる」に対する解決策として、レコード型がサポートされました。基本的な使い方は型の名前とそこに含めるメンバを宣言するだけです。通常のクラスの宣言と異なるのは、`class`の代わりに`record`と宣言すること、型名のあとに引数を宣言する必要があることだけです。この引数が扱う「データ」となります。
具体的に言うと上のコードを記載すると下のようなコードに書かれているフィールドやメソッドが自動的に生成されるイメージです。書かれている通りに順番に言うと、
- 値は書き換えられない。つまりデータを表すフィールドは final になる。
- 値の取得はgetter経由で行う。このgetterは変数名と同じになる。getHogeは作成できない(されない)。
- 入力値に対する処理を行うことも可能です。
- インタフェースを実装できます、ただし継承(拡張)は不可能です。なぜならすでにjava.lang.Recordをextendsしているからです。
- 何度か触れてますが、新しいクラスじゃない「型」が導入されたように見えますが、バイトコードを覗くと実態は`java.lang.Record`を拡張したクラスであることが分かります。このクラスでtoStringなどのコメントアウトしている3つのメソッドが抽象メソッドとして宣言されているので、これらのメソッドがコンパイル時に自動的に生成されます。
Java 15ではこの基本的な使い方は変更されていませんが、一般的なクラスにおけるローカルクラスに対するローカルレコードという機能の追加などの変更が組み込まれています。
どういうことかというと、レコードを扱いたいアプリケーションとして考えられるパターンとして、あるクラスの中に単純な変数を複数持つ内部の値を扱うことが考えられます。現在は、こういった内部の値をヘルパークラスで扱っている実装が多いと考えられますが、これをレコードタイプとして扱えるようにする、というモチベーションです。
これ以外に後ほど説明するSealedクラスなどの対応も行われています。今後も変わる可能性があるので、実装について興味がある方は最新javacでコンパイルした結果をjavapでバイトコードを確認して確認することをお勧めします。
Java 13から導入されたテキストブロックがセカンドプレビューになりました。変更点は改行と空白を示す2つのエスケープ文字が導入されただけです。
具体的な使い方は表示されているとおりです。例えばやたら長い長文を記述したい時でもコード上ではみやすさのために改行を加えたいだとか、文末に空白を明確に記述したいときだとかに使うことができます。
この機能はJava 15で標準機能として昇格しました。
Javaアプリケーションで利用するメモリはGCによってJavaヒープ上で管理されます。しかしながら、アプリケーションによってはJavaヒープ外のメモリにアクセスしたいケースもあります。
これまではByteBufferの`allocateDirect`メソッドを利用する方法や、JNI(_Java Native Interface_)を利用する方法がありました。ほかに非公式ですが、`sun.misc.Unsafe.allocateMemory()`を利用する方法もあります。
しかし、これらの方法にはいくつか問題があります。
ByteBufferは1オブジェクトの上限がInteger.MAX_VALUE(2G)に限定され、全体で`-XX:MaxDirectMemorySize`で指定した値以下にする必要がありますが、メモリ割り当て解放のタイミングはGCに委ねられているため、意図せず超えてしまう問題がありました。
また、JNI(_Java Native Interface_)はC言語などでコードを書く必要があり、メモリ管理をすべて自分で行う必要があります。
そして、`sun.misc.Unsafe`クラスは非公式であるため将来的に無警告で削除される可能性があるうえ、名前のとおりに容易にJVMをクラッシュさせる安全でない操作を引き起こすことができるという問題を抱えています。管理してないメモリをいきなり触るとか。
このため、Java 14からはJavaヒープ外のメモリを扱うためのJava APIが新たに導入されました。JEPのページでは、メモリへの逆参照を行うサンプルとしてこのようなコードが示されています。
なお、このAPIはIncubatoorと書かれていますが、これは今後APIが削除されたり変更される可能性があるAPIを指し、標準モジュールとは異なる特別なモジュールで提供されています。本来は非互換性を極力廃するように変更が行われますが、これらのAPIはその対象外です。このAPIを利用する場合は`--add-modules jdk.incubator.foreign`を指定する必要があります。
また、このAPIはかなり広範囲がサポートされており、次のバージョンではAPIの利用方法が変わる可能性があります。Java 15でもmapped memory segmentのサポートが拡張されるなど、多数のAPIのリフレッシュが行われています。興味がある方はぜひAPIドキュメントを参照しながら使ってみてOpenJDKコミュニティにフィードバックしてください。あなたの貢献により良くなる可能性があります。Records以上に変わる可能性があるので常に最新APIをドキュメントから確認するようにしてください。
ここから Java 15 で追加された機能です。Java 15では新たに二種類のクラスが追加されました。この内の一つがシールドクラスです。
これは誰がサブタイプになれるかを完全に制限することが可能なクラスです。これによって今までのアクセス修飾子よりも、クラスやインターフェースの作者が、より明示的にどのコードが実装を担うかを宣言することができるようになりました。副次的な効果として、サブクラスを完全にコントロールすることができるので、パターンマッチングもより網羅的に行うこともできるようになります。
通常のクラス宣言と違うのはclassの前にsealedと宣言すること、そして継承や拡張を許可するサブタイプを`permits`宣言のあとに記載します。今回はパッケージ名も含めてFQDNで記載していますが、同一パッケージであればクラス名だけで指定することも可能です。
特徴としては、封印された型の匿名のサブクラス(およびラムダ)は禁止されています。また、特に指定がない限り、sealedのabstract、つまり抽象的なサブタイプはsealedとなり、通常のサブタイプはfinalとなります。各サブタイプの宣言時に`non-sealed`と明示的に指定することでこの制約は回避することができます。
もう一つはHidden Classesと呼ばれるクラスです。このクラスは限定された方法以外では他のクラスやインターフェースからはアクセスできない、他のクラスから完全に秘匿されることを目的としています。
完全に秘匿するということはどういうことでしょうか?実は今までの実装ではリフレクションを利用するなどで無理矢理読もうと思えば読むことができるのです。
現在の実装はあるクラスのバイトコードがコンパイル時に作成されたもの(静的クラス)か、ランタイム時に作成されたもの(動的クラス)かを区別しません。なお、コンパイル時に作成したものを静的クラス、ランタイム時に作成したものを動的クラスと言います。このため、ある動的クラスが同じクラスローダー階層にいる別のクラスから名前を元にリンクしようとする度に可視化されてしまうため、完全な秘匿を実現する方法がありませんでした。これによって、動的クラスのライフサイクルがリンクしている静的クラスが開放されるまで保持され続けるなど、想定以上に長くなってしまう問題がありました。
これを回避するために、フレームワークにより実行時に生成され、他のクラスから秘匿されつつリフレクションを介して間接的に使用でき、他のクラスから独立してアンロードできることを目的としたのがHidden Classesです。
通常のクラス、またはインターフェースは`ClassLoader::defineClass`を呼び出すことで作成されますが、隠しクラス、インターフェースはMethodHandles.Lookup#defineHiddenClassを呼び出すことで作成されます。このメソッドはbyte配列から隠されたクラスを生成し、reflective accessをもつlookupオブジェクトを返します。このlookupオブジェクトが隠しクラスのClassオブジェクトを取得する唯一の方法となり、他の方法からはアクセスできないようにすることで、完全な秘匿を達成しています。
このほか、細かい変更としては次のようなものがあります。
まず1つは、LegacyなSocket APIの再実装がJava 13で行われました。これに基づき、周辺APIの再実装を進めようという内容です。この実装はJava 1.0から導入されており、C言語とJava言語が共存している状態でメンテナンス性が損なわれていました。他、非同期クローズなどの並行処理で難があったのでそういった部分の改善が行われる予定です。
ByteBuffer APIが不揮発性メモリにも対応しました。
`java.rmi.activation`パッケージに属するクラスやインターフェース及び関連クラスが非推奨化され、`rmid`ツールが非推奨化に伴い警告が出るようになりました。
つづいてJVMの挙動に関する変更について紹介していきます。この変更は大きなものが2つあります
(きしださんが説明するので21までSkip。以下は元ある原稿)
本来はnullを想定していない処理中にJavaアプリケーションがnullを受け取ってしまった際に、`NullPointerException`が発生します。
Javaのコードを書いたことがある人にとっては、NPEは珍しくないと思いますが、念のために説明すると、NPEの基本的な対処方法は、スタックトレースから例外が発生したコードの行を確認し、どのオブジェクトがnullとなったかを推定することでコードの修正を行うことが挙げられます。
例えば次のようにSampleクラスを実行した結果、NPEが発生したスタックトレースを受け取った場合は、Sample.javaの4行目に`null`となったオブジェクトがあったと判断することができます。
もしこのSample.javaの4行目が次のようなコードだった場合、Nullとなっている可能性があるオブジェクトは`person`か`person`が持つ`name`の二箇所であり、比較的範囲が絞られているため対処も簡単です。
今回に関して言えば8行目でnameにnullを入れているので明らかにこれが原因です。
しかし次のコードのように、オブジェクトを多数利用している場合はどこでnullになったのか一見では解らないぐらいに被疑箇所が多く、かつそれぞれのオブジェクトがまったく別の処理で作成していた場合では、調査に時間を要することも珍しくありませんでした。
現実的には三次元配列はめったに使わないし、オブジェクトがNullableかそうでないかは考慮したうえで実装しますが、世の中思わぬところでnullになって調査が難航することは稀に良くあります。
このため、Java 14からはこのオプションを設定することで、具体的にどの処理を行った際にどのオブジェクトがnullだったかを表示するように変更が行われました。
このオプションを付けて先程のSampleクラスを改めて実行してみましょう。
ちょっと見にくいのでエラーメッセージを改行してみます。今後出るエラーメッセージも必要に応じて改行しています。
このように、どのような処理中にどのオブジェクトがnullであったかを完璧に表示しています。これによって先程のようなメソッドでもどこがnullであったかを知ることができます
nullを表示するエラーメッセージは処理やnullの箇所に応じて出力内容が変わります。特にローカル変数についてはひと手間必要なので追加で紹介します。
次のようなローカル変数`names`がnullになっていたケースを見てみましょう
実際に実行してみるとObject配列を読み込もうとした際に(When)ローカル変数(What)がnullであったため読み込めなかったことを示しています。実際のローカル変数名はここに書かれている内容(`local1`を指す)ではありませんが、通常のコンパイル方法ではローカル変数情報が含まれていないためこのような表示のされ方をします。
ローカル変数の名前を明確に表示したい場合はローカル変数を含むデバッグ情報が必要なため、次のようにクラスファイルをコンパイルする際に`-g`オプションを指定する必要があります。
`local1`ではなくローカル変数の名前が`names`と明確に出力されるようになりました。
この出力は非常に便利ですが、ローカル変数のデバッグ情報が増えるため、クラスファイルのサイズが増大するため配布時に影響が出る、クラスロードを行う時間が多少延びる、そしてリバースエンジニアリングのヒントが増えて行いやすくなるという問題もあります。このため、このオプションは基本的には開発環境で利用することをお勧めします。
この機能全体の懸念点としてパフォーマンスへの影響が挙げられますが、NPE発生時、正確にはスタックトレースの生成時にメッセージを取得するようにしているため、特筆すべきレベルの大きな影響はありません。頻繁にNPEを発生させているアプリケーションの場合は性能に影響がある可能性もありますが、そのようなアプリケーションはそもそも処理の見直しを行ってからパフォーマンスを確認してください。
Biased Lockingがデフォルトで無効化されました。
このBiased lockingはuncontented lockingのオーバヘッドを下げるためにHotSpot VMにかなり早い時期に導入されました(当時はOSSじゃないので伝聞です)。
これは、あるthreadがmonitorを取得しようとするまでモニタは特定のスレッドに所有されたままであると仮定することでatomic操作回数を減らし、多くのスレッドがシングルスレッド方式で使用しているオブジェクトに対する同期操作(を多く行っているケースで)の性能改善を図っていました。
初期のAPI(java.util.Vectorなど)はアクセスの度に同期をとる実装になっており、非同期コレクションはJava 1.2から、マルチスレッドを考慮したAPIはJava 5からと導入が遅めだったため、初期は実際に目覚ましい効果がありましたが、並列処理用のAPIが充実してきた昨今では逆に性能が悪化している(ベンチマークツール、SPECjbbより判明)状況でした。
また、Biased lockingによってJVMのコードもメンテナンスしづらくなっているので非推奨化して最終的には削除する方針となり、Java 15からはデフォルトで無効になりました。
- XX:-UseBiasedLockingで以前のJavaでも無効化することは可能です。逆に有効にしたい場合は-を+に変更することで可能ですが、将来的には削除されるので無効化するのが望ましいでしょう。
(練習通りいくと時間が足りないので、GCについてはまとめてこのスライドで説明する。27までskip、以下は元ある原稿)
続いてGC関係について説明していきます。Java 14では細々とした変更が入り、Java 15では試験的なGCが標準機能となりました。
Java 15で導入された2つのGCを軽く説明すると、ZGCはテラバイト級までの巨大なヒープを扱いながら、アプリケーション停止時間を10ミリ秒以下にすることを目的としたGCで、Shenandoah GCはG1 GCのようにリージョン単位でヒープを管理しつつ並列コンパクションを採用したGCです。
NUMAとは複数のプロセッサが共有しているメモリへのアクセスコストを、プロセッサとメモリ領域ごとに偏りを持たせたアーキテクチャです。
しかし、JavaはGCによってメモリ管理が行われているため、未対応であったG1 GCを利用している場合は、この偏りを無視してメモリを利用する可能性がありました。
現在のデフォルトであるG1 GCは、NUMAノードを意識してヒープリージョンの配置を行うように改善が行われました。
これにより、NUMAアーキテクチャを採用しているサーバ上でG1 GCを利用してJavaアプリケーションを運用している場合、パフォーマンスの改善が見込まれます。
非推奨であったCMS GCが完全に削除されました。
ただし、オプションを指定して実行すると、このような警告が出てデフォルトのGC、今だとG1 GCが選択されて実行されます。poorな環境だとSerial GCが選択されます。
起動失敗しないのでCMSのつもりでG1で動いてて「アプリケーションの動作がおかしい!」とか「ログ内容がおかしい!」とならないように注意しましょう。
いずれにしてもGCアルゴリズムはちゃんと明確に指定しましょう。
Java 11から試験機能として導入された、テラバイト級までの巨大なヒープを扱いながら、アプリケーション停止時間を10ミリ秒以下にすることを目的としたZGCはLinuxのみだったが、macOSおよびWindowsでも利用できるようなりました。
ZGCは弊社のHBaseチームが検証して記事を出してますので興味ある方はそちらを確認してみてください。
次のようなオプションでYoung GCをパラレルで、Old GCをシリアルでGCを実行する組み合わせが非推奨になった
(補足)
一般的に、システムリソースを気にするならYoung GCのほうが実行回数が多いので、こちらをシリアルにしてOld GCも合わせてシリアル、または回数が少ないのでパラレルにするのは合理的だが、逆にするシチュエーションはあまりないのでメンテナンス性を考慮して将来的に削除される予定となった。
最後にツールの変更です。これはJava 14の変更のみです。
(補足)
ただし、JEPにはなっていませんがJava 15でも`rmic`が削除されたりはされています
パッケージングツールです、平たく言うと各OSに合わせたインストーラーを作成するためのツールです。
Windowsだけは外部のWiXツールを追加でインストールする必要があります。
このツールはincubatorなAPIに依存しているため、incubatorと記載されています。
今後のJavaアプリケーションを提供する流れとしては、カスタムランタイムイメージをjlinkで作成し、その後インストーラをこのツールで作ってユーザに提供する、と想定されています。
`--input`でjarファイルが置かれているディレクトリを指定し、メインJARファイル、メインクラスをそれぞれ指定して実行します。すると、macOSであればsample-1.0.dmgなど、OSに合わせたインストーラが作成されます。
カスタムイメージでの作り方は依存しているモジュールを表示する。モジュールを指定してjlinkでカスタムイメージを作成する。
そしてこのツールで`jlink`で作ったカスタムイメージを`--runtime-image`で指定しているだけです。この方法を利用するメリットはインストールされるサイズが抑えられることと、インストーラそのもののファイルサイズも抑えられるところです。
JavaはRun anywhereを掲げておりJARファイルの提供だけで済むのが言語のメリットの一つではありますが、Javaの実行バイナリがより多くのベンダや団体から提供されるようになったので、このようなインストーラでバイナリごとインストールさせるメリットが以前より大きくなったと捉えることができます。
(いとうさんが説明するので37までskip、以下は元ある原稿)
HeapStatsの商売敵というか元々プロプライエタリであったOracle社製トラブルシューティングツールであるJava Flight RecordがJava 11からOSS化され、つい最近例外中の例外的にJava 8にもバックポートが行われました。
Javaは商標なのでJDK Flight Recorderという名前になりました。
このJDK Flight Recorderにストリーミング機能が新しく追加されました。これまではJFRで取得した情報はダンプしたファイル経由で読み取る必要があり、リアルタイムでのモニタリングが行いにくいという問題がありました。
この新機能により`RecordingStream`クラスを通じて、リポジトリを経由して別のJavaプロセスから継続的に取得したり、もちろん同じJavaプロセス内でも取得したりすることが可能になりました。
まずは次のJFRSampleクラスのように、JFRをローカルで開始して記録を行い、ストリームを作成して継続的に標準出力してみましょう。このクラスを実行すると、CPU負荷イベントを10秒ごとに出力します。
CPU負荷イベントの情報はこんな感じで出力されます。
単純に定期的に出力するには分かりやすい値だったためCPU情報を収集していますが、他にデフォルトで用意されているイベントを収集することができます。用意されているイベントはJDK内のhttps://hg.openjdk.java.net/jdk/jdk14/file/e568ce785bdf/src/hotspot/share/jfr/metadata/metadata.xmlに定義されています。
ThreadStartやThreadPark、JavaMonitorなどのスレッド情報から、ClassLoadなどのクラスローディング情報、ヒープ使用量や各GCアルゴリズムごとの統計情報など多種多様なイベントを出力できます。
これ以外にもJFR APIを利用して、具体的にはjdk.jfr.Event(https://docs.oracle.com/en/java/javase/14/docs/api/jdk.jfr/jdk/jfr/Event.html)クラスを拡張することで、自分で独自にイベントを作ることも可能です。この機能は今回導入された機能ではなく、Java 9から提供されています。
また、JFRはどういったイベントを有効にするかの情報が設定されている基本構成としてdefaultとprofileの設定が用意されており、この構成を利用してストリームを作成することも可能です。記録するイベントの設定漏れを防げるなどの理由から、基本はこちらのほうがお勧めです。
なお、profile構成を利用した場合は出力される情報量が多いため、それなりにJavaアプリケーションにオーバーヘッドが掛かるので注意してください。いきなりプロダクションで有効にするのではなく、性能がどれだけ落ちるか確認したほうが良いでしょう。
ローカルJavaプロセスの情報であればなにもJFRを利用する必要はありません。あるJavaプロセスで収集しているCPU負荷イベント情報を、別のJavaプロセスから取得してみましょう。そのためにはまずは、対象JavaプロセスのJFRリポジトリパスを`jcmd`から確認します。
このリポジトリパスはデフォルトではOSのテンポラリディレクトリですが、Javaの起動時にこのオプションを使って指定することもできます。
このリポジトリパスへのストリームをJFRRemoteSampleのように開くことで、別のJavaプロセスから監視を行うことができます。せっかくなのでメソッドプロファイリングを行うイベントを標準出力しました。
このように別のJavaプロセスからもリポジトリを参照することで、リアルタイムに情報の収集が行えます。
なお対象のJavaプロセスを終了するとストリームが終了するため、自動的に収集も終了します。また、対象Javaプロセスで記録していない情報以外は当然ながら収集することはできません。
これまではファイルをダンプして事後解析が必要でしたがこれによりだいぶ楽になりました。リポジトリを見れば最大でも1秒以内というほぼリアルタイムで時系列データを得ることができ、事後解析も楽にすばやく取りかかることができると期待できます。
注意事項として、JFRは様々な情報が収集できるため多角的な解析手段としては最高峰ですが、OOMEやクラッシュ時のメモリ情報収集は苦手であるということに注意してください。
Leak Profilerの実装的な問題により、長期的なメモリリークであれば収集できる可能性は十二分にあるが、超短期間のメモリ使用量上昇、ようは瞬間的なスパイクによるOOMEであればオブジェクトサンプリングが取得できない可能性があります。
このため、JFRとは別に`HeapDumpOnOutOfMemoryError`オプションでプロセスが死亡する際にヒープダンプを取得する必要があります。
この他、細々とした変更点として、新しい暗号署名が追加されました。
Java 8で導入され、Java 11で非推奨化されたJavaScriptエンジンであるNashornとjjsツールが削除されました。
そしてSplarisとSPARCポートが削除されました。
以上です、ご清聴ありがとうございました