NHN Cloud NHN Cloud Meetup!

JDK 13、メジャーアップデートのビフォーアフター

Java 13

6ヶ月ごとにGA配布されている新バージョンでは、OpenJDK13は2019年9月17日に配布されました。主な機能は以下のとおりです。

  • JEP350:Dynamic CDS Archives
  • JEP351:ZGC:Uncommit Unused Memory(エクスペリメンタル)
  • JEP353:Reimplement the Legacy Socket API
  • JEP354:Switch Expressions(2回目のプレビュー)
  • JEP355:Text Blocks(プレビュー)

注:https://openjdk.java.net/projects/jdk/13/

JEP350:動的CDSアーカイブ

OpenJDK12では、デフォルトCDSアーカイブに基本機能として追加されました。動的CDSアーカイブは拡張されたバージョンです。使用性を改良し、デフォルトCDSアーカイブにはないロードされたアプリケーションクラスとライブラリクラスを含むように改善されました。アプリケーション・クラス・データ共有((Application Class-Data Sharing、AppCDS)は、Javaアプリケーションの実行が終了する際に動作し、Javaプロセス間で共通使用されるメタデータを共有する方式で、性能(主に開始時間)を向上させるための機能です。

きっかけ

HotSpotからAppCDSを使ってアプリケーションクラスを保存すると、起動時間とメモリにおいてメリットが見られますが、依然として3つ程度の追加手続きが必要です。

  1. クラスのリストを生成するための複数回の試験実行(trial run)
  2. 生成されたクラスのリストを使ってアーカイブをダンプ
  3. アーカイブと実行

また、この3つの手順は基本提供のクラスローダーを使用するアプリケーションでのみ動作します。HotSpotから実験的に対応していますが、使用するのは容易にはいかないようです。

成果

JEP350ではこのような不便さを解決するために、Javaアプリケーション実行時、簡単にコマンドラインに-XX:ArchiveClassesAtExitオプションを加えることでAppCDSを有効化することができます。このように実行されたJavaアプリケーションは、終了時にjsaというシステムアーカイブファイルを作成します。当該ファイルを利用して、メタデータを共有するJavaアプリケーションを向上した性能で実行させます。また、下記のようにオプションの引数から生成されるアーカイブファイル名を指定することが可能です。

% bin/java -XX:ArchiveClassesAtExit=hello.jsa -cp hello.jar Hello

このように生成されたアーカイブファイルは、以下のように使用できます。

% bin/java -XX:SharedArchiveFile=hello.jsa -cp hello.jar Hello

この方法では、上述した「1.trial run」の手順を取り除き、簡単にAppCDSが使用できるようになります。また、基本提供のクラスローダーとカスタムクラスローダーの両方に対応しています。また、JEP350の改善機能は、アプリケーションの初回実行において自動アーカイブの作成を行えます。これにより、「2.アーカイブダンプ」の手順がなくなり、CDS/AppCDSの使用を自動化が可能になります。

JEP351:ZGC:Uncommit Unused Memory(エクスペリメンタル)

使用していないヒープメモリをOSに返却するようにZGCを改善するJEPです。

ZGCはJDK11から実験的に導入されたGCです。ZGCに関連する簡単な説明は、以前Big things in JDK 11でもお話しましたが、-XX:+UnlockExperimentalVMOptions XX:+UseZGCでZGCを有効化することができます。詳しい使い方や説明はこちらをご覧ください。

きっかけ

当時ZGCは、長時間使用しない場合でもメモリをOSに返却しないという問題がありました。これは、以下のような一部の環境では良い方法ではありませんでした。

  • 使用したリソースの分だけ料金を支払うコンテナ環境
  • 長い間アイドル(Idle)状態であったり、他のアプリケーションとリソースを共有する環境
  • 起動状態と実行状態のメモリ使用量が異なる環境(実行時は多くのメモリを使用するが、実行後は一定のメモリのみを使用する環境)

成果

したがって、ZGCはZPageというページキャッシュ内の使用していないメモリセットを定められたポリシーに従ってコミットを解除し、OSに返却するものとし、最小ヒープサイズ(-Xms)以下には減少しないように指定しました。(-Xmsと-Xmxが同一の場合は、この機能が暗黙的に無効化されます。明示的に無効化するには、-XX:-ZUncommitを使用します)一般的にページキャッシュは、LRU(Least Recently Used)方式を使用し、ページサイズ別に区分するため、メモリを解放する方法は比較的簡単です。しかし、問題はキャッシュからZPageを削除するタイミングを決定することにあります。

単純に一定時間が経過すると削除されるように設定することができますが、実際にこの方式はShenandoah GCではデフォルト5分で使用されています。ZGCも-XX:ZUncommitDelay=<seconds>(デフォルト300秒)で簡単な時間ポリシーを提供することができます。この方式のほか、新しいオプションを追加せずにGCが起こる頻度によってメモリ解放サイクルを設定することもできます。

JEP353:Reimplement the Legacy Socket API

java.net.Socketとjava.net.ServerSocket APIの基本実装を、メンテナンスとデバッグがしやすい形式にリファクタリングするJEPです。

きっかけ

java.net.Socketとjava.net.ServerSocketはJDK 1.0で初めて登場しましたが、メンテナンスとデバッグが困難な従来のJavaとCコードの混合した形態で実装されました。この実装では以下のような問題がありました。

  • スレッドスタックをI/Oバッファとして使用し、デフォルトのスレッドスタックサイズを何度も増やす必要があるという問題
  • ネイティブデータ構造を使って実装した非同期closeには、長年に亘り微妙な安定性/移植性の問題が存在する
  • 実装にはさまざまな同時実行の問題があり、これを解決するには精密検査が必要
  • ネイティブメソッドでスレッドをブロックする代わりに、将来的な環境では現在の実装が目的に合わなくなる

成果

したがって、java.net.Socketとjava.net.ServerSocket APIは、すべての操作をSPI(Service Provider Interface)メカニズムであるjava.net.SocketImplに委ね、内蔵の実装は「plain」実装という、SocketInputStreamとSocketOutputStreamクラスをサポートする非公開のPlainSocketImplによって実装されます。

以下は、新しい実装の内容です。

  • SocketImplは従来のSPIメカニズムであり、新しい実装に該当しない場合は、以前の実装を模倣して互換するように動作する。
  • Timeout(connect、accept、read)を使用しているソケット演算子を、non-blockingモードに変更して、ソケットをpollingて実装する。
  • SocketImplがGCされ、Socketが明示的に閉じられていない場合は、java.lang.ref.Cleanerメカニズムが使用される。
  • 接続のリセット処理は、以前の実装と同じように処理される。(接続リセット以降の読み込みに失敗)

そして、以下は以前の実装とは異なる動作状況に該当するものです。最初の2つを除き、残りは-Djdk.net.usePlainSocketImplオプションで緩和することができます。

  • PlainSocketImpl.getINputStream()で返されたInputStreamとOutputStreamがそれぞれjava.io.FileInputStreamとjava.io.FileOutputStreamをextendする。
  • ユーザー定義のSocketImplを使用するServerSocketは、プラットフォームSocketImplと一緒に返却されるSocketに接続することができない。(逆も同様)
  • InputStreamとOutputStreamは、以前の実装でEOF(End Of File)に対するテストを先に実行して-1を返すが、新規の実装では、Nullと範囲チェックを最初に実行する。検査手順の変更により、コードが壊れる可能性がある。
  • 受信キューで読んでいないバイトでソケットを閉じると、基本ソケットが正常終了する。Windows以外のプラットフォームでは、中断/強制終了につながる。
  • Oracle Solaris特定:ネットワークエラーが発生し、setsocketoptまたはioctl呼び出しに失敗した場合、接続リセットにより読み込みに失敗するが、このような方法は壊れやすくメンテナンスが困難なため、新規の実装ではこの動作に従わない。
  • Oracle Solaris特定:TCPソケット接続後、IPV6_TLCASSソケットオプションを変更できなくなった。以前の実装では、setTrafficClass関数に指定された値をキャッシュしてマスキングしていた。
  • 例外時、SocketExceptionを発生させるが、新規実装では一部の例外が発生しないことがある。また、例外メッセージが異なる場合もある。例)WindowsでSocketException時、以前の実装ではエラーコードに英語専用メッセージを使用していたが、新規実装ではシステムメッセージを使用する。
  • 特定のタスクを実行するときに性能が異なる場合がある。以前の実装では、ServerSocketでacceptメソッドを呼び出す複数のスレッドがカーネルで待機していたが、新規実装では、単一スレッドがacceptを呼び出すために待機して、残りのスレッドはキューからロックを得るために待機する。

SPI(Service Provider Interface):プラグイン形式で提供されるインターフェース。インターフェースのみ定義し、各実装は使用するベンダーで行う。例)Java Cryptography Extension

JEP354:Switch Expressions(2回目のプレビュー)

JDK 12でプレビューとして言及されたSwitch Expressionsの追記です。内容はJEP325と同じで、breakキーワードがyieldキーワードに変更されました。

int result = switch (s) {
    case "Foo": 
        yield 1;
    case "Bar":
        yield 2;
    default:
        System.out.println("Neither Foo nor Bar, hmmm...");
        yield 0;
};

Pythonを使用している方ならおなじみのキーワードだと思いますが、動作は似ています。Switch文の内部で生成された特定の値を返します。JDK12で登場したbreak value;yield value;に置き換えられました。return value;の制御権が関数呼び出しやコンストラクタにあれば、yield value;はSwitch文に制御権を伝達します。

JEP325で提案された矢印方式は、単一表現式を表すときは有効に動作します。

int j = switch (day) {
    case MONDAY  -> 0;
    case TUESDAY -> 1;
    default      -> {   //ブロックや既存のswitch-case文でyieldを使用
        int k = day.toString().length();
        int result = f(k);
        yield result;
    }
};

JEP355:Text Blocks(プレビュー)

JDK15で提供されるJEP378のプレビューです。他の言語でも表示されるText BlockがJavaにも追加されます。従来では複数行のテキストを使用するとき、+とnew lineで接続していましたが、この提案では、“””によってmulti lineでテキストを入力できるようになります。

HTML example(従来のパターン)
String html = "<html>\n" +
              "    <body>\n" +
              "        <p>Hello, world</p>\n" +
              "    </body>\n" +
              "</html>\n";
HTML example(将来像)
String html = """
              <html>
                  <body>
                      <p>Hello, world</p>
                  </body>
              </html>
              """;
SQL example(従来のパターン)
String query = "SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`\n" +
               "WHERE `CITY` = 'INDIANAPOLIS'\n" +
               "ORDER BY `EMP_ID`, `LAST_NAME`;\n";
SQL example(将来像)
String query = """
               SELECT `EMP_ID`, `LAST_NAME` FROM `EMPLOYEE_TB`
               WHERE `CITY` = 'INDIANAPOLIS'
               ORDER BY `EMP_ID`, `LAST_NAME`;
               """;
Polyglot language example(従来のパターン)
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("function hello() {\n" +
                         "    print('\"Hello, world\"');\n" +
                         "}\n" +
                         "\n" +
                         "hello();\n");
Polyglot language example(将来像)
ScriptEngine engine = new ScriptEngineManager().getEngineByName("js");
Object obj = engine.eval("""
                         function hello() {
                             print('"Hello, world"');
                         }

                         hello();
                         """);

この提案は、Java stringの可読性を向上させ、エスケープ文字列の使用を避けるために作られました。特殊文字のエスケープ方法は従来の文字列の使用方法と同じで、必要な場合には次のString関連の関数を使用することができます。

  • String::stripIndent()
  • String::translateEscapes()
  • String::formatted(Object… args)

以前のJDKの変更内容は、こちらからご確認いただけます。

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop