2020/04/18

Java 14 の新機能 (10) - JEP 370: Foreign-Memory Access API (Incubator)

JEP 370: Foreign-Memory Access API (Incubator)

Foreign-Memory Access API とは?

Java プログラムにおいて使用するメモリは、通常ヒープ領域の中から確保されます。ヒープはガーベジコレクタの管理下にあり、オブジェクトの生成に応じて必要なメモリがヒープから割り当てられ、オブジェクトが消滅したメモリはガーベジコレクタによって識別されて未使用ヒープに戻ります。

しかし Java でも、オフヒープメモリ(Off-Heap-Memory) または外部メモリ (Foreign-Memory) と呼ばれる、ガーベジコレクタの管理外のメモリを使用する需要はあり、以前からオフヒープメモリアクセスを行う実装は存在していました。大きく分けて3つの方法があります。

  • java.nio.ByteBuffer を使う
    • Java 標準 API なので安全
    • バイト単位での読み書きを意識しないといけない
    • アドレス/インデックス指定ではなく現在位置カーソルを動かして読み書き
    • 書き込んだ後は flip() しないと読み込めない
    • 2GB までしか扱えない
  • sun.misc.Unsafe を使う
    • ユーザプログラムからは使用しないことが強く推奨されている
    • メモリの解放を明示的に呼ばないといけない (うっかりするとメモリリーク)
    • 予期しない結果になるオペレーションがいろいろあって、危ない
  • JNI を使う
    • Unsafe よりもっと危ないし、めんどくさい
    • でも自由度は高い
ここに、Java 14 で新しい API が追加されました。Foreign-Memory Access API です。まだ Experimental レベルのもので、jdk.incubator.foreign モジュールとして提供されています。特徴は以下の通り。
  • 安全に外部メモリアクセスできるように設計されている
  • メモリ領域の構造を定義し、それに基づいてダイレクトアクセスできる
  • 2GB を超えるメモリでも大丈夫
今まで苦労していたことがいろいろ解決する便利な API になりそうです。これくらいお手軽だと、ライフサイクルがはっきりしているので、GC の対象外にしておきたいというくらいの軽い理由で使ってもいいかもしれません。

サンプルコード

JEP 370 のページに載っているコードが微妙に間違っているようなので、簡単なサンプルを書いてみました。
import java.lang.invoke.VarHandle;
import java.nio.ByteOrder;

import jdk.incubator.foreign.MemoryAddress;
import jdk.incubator.foreign.MemoryLayout;
import jdk.incubator.foreign.MemoryLayout.PathElement;
import jdk.incubator.foreign.MemorySegment;
import jdk.incubator.foreign.SequenceLayout;

public class ForeignMemory {
  private static final long K = 0x400L;
  private static final long M = K * K;

  public static void main(final String[] args) throws InterruptedException {
    // 要素数512M個×64bit(long)=4GB のメモリを割り当てるためのレイアウトを構成する
    SequenceLayout longArrayLayout = MemoryLayout.ofSequence(512L * M,
        MemoryLayout.ofValueBits(Long.SIZE, ByteOrder.nativeOrder()));
    // try-with-resources 構文を使えば、確実にメモリを解放できる
    try (MemorySegment segment = MemorySegment.allocateNative(longArrayLayout)) {
      // 割り当てたメモリのアドレス情報を取得
      MemoryAddress base = segment.baseAddress();

      // long 配列のようにアクセスするための VarHandle を構成する
      VarHandle longElementHandle = longArrayLayout.varHandle(long.class, PathElement.sequenceElement());
      long n = longArrayLayout.elementCount().orElse(0L);

      // i 番目の要素に i*100 を書き込む
      for (long i = 0; i < n; i++) {
        longElementHandle.set(base, i, (long) i * 100);
      }

      // i 番目の要素を取り出し、全部表示するわけにもいかないので、32M個に1回だけ表示する
      for (long i = 0; i < n; i++) {
        long x = (long) longElementHandle.get(base, i);
        if (i % (32L * M) == 0) {
          System.out.println("i=" + i + ", x=" + x);
        }
      }
    }
  }
}

2GB を超えるメモリを扱えることを確認するため、4GB になるように SequenceLayout を構成してみました。long 型の512M要素の配列の形にしています。
メモリの割り当てと解放は、try-with-resources 構文を使うことができます。解放漏れはありません。
中身へのアクセスは、Java 9 で追加された VarHandle を使います。VarHandle を使うことで、変数へのアクセスと同等に、型を意識してアクセスすることができます。