2025/03/17

JEPでは語れないJava 24

このエントリーをはてなブックマークに追加

毎度おなじみ半年ぶりのJavaのアップデートです。

Java 24はLTSの1つ前のバージョンだからか、怒涛のJEP祭の様相を呈しています。

なんとJEPが24!!Standard JEPに限っても14もあります。こんなにJEPが多いのはJava 9以来はじめてです!

Java 24のJEPは以下の通り。

  • 404: Generational Shenandoah (Experimental)
  • 450: Compact Object Headers (Experimental)
  • 472: Prepare to Restrict the Use of JNI
  • 475: Late Barrier Expansion for G1
  • 478: Key Derivation Function API (Preview)
  • 479: Remove the Windows 32-bit x86 Port
  • 483: Ahead-of-Time Class Loading & Linking
  • 484: Class-File API
  • 485: Stream Gatherers
  • 486: Permanently Disable the Security Manager
  • 487: Scoped Values (Fourth Preview)
  • 488: Primitive Types in Patterns, instanceof, and switch (Second Preview)
  • 489: Vector API (Ninth Incubator)
  • 490: ZGC: Remove the Non-Generational Mode
  • 491: Synchronize Virtual Threads without Pinning
  • 492: Flexible Constructor Bodies (Third Preview)
  • 493: Linking Run-Time Images without JMODs
  • 494: Module Import Declarations (Second Preview)
  • 495: Simple Source Files and Instance Main Methods (Fourth Preview)
  • 496: Quantum-Resistant Module-Lattice-Based Key Encapsulation Mechanism
  • 497: Quantum-Resistant Module-Lattice-Based Digital Signature Algorithm
  • 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
  • 499: Structured Concurrency (Fourth Preview)
  • 501: Deprecate the 32-bit x86 Port for Removal

 

こんなにJEPがあるのですが、なんとAPIの変更はとてもわずかしかないのです。

JEP 484 Class-File APIとJEP 485 Stream GatherersによるAPI追加以外はほとんどありません。

しかたないので、今回に限ってJEPを語るにしてしまいましょう。本エントリーではいつも通りJEPで語らない部分を紹介し、次のエントリーでJEPについて紹介していきます。

なお、java.baseモジュール以外だと、java.desktopモジュールに変更がありますが、変更点が少ないので省略します。また、いつも通り、セキュリティ関連も省略します。

 

廃止になったAPI

Java 24で削除されたAPIは2つだけです。いずれもフィールドです。

フィールド

削除されたフィールドはすでに削除された機能に関連しているフィールドです。

  • java.awt.Window.warningString
  • javax.naming.Context.APPLET

1つ目のWindowクラスのwarningStringはpublicなフィールドではありません。

この文字列はgetWarningString()メソッドで返される文字列でした。セキュリティマネージャで設定されるパーミッションが設定されている場合はnullが返るようになっていたのですが、セキュリティマネージャが廃止されたため、常にnullを返すようになっています。

このため、getWarningString()メソッドで返す文字列が必要なくなったため、削除されました。

 

2つ目のContextクラスのAPPLETは、まぁしかたないですね。APPLETはなくなってしまいましたし。

 

廃止予定に追加されたAPI

Java 24では3つのクラスと2つのメソッドが廃止予定に追加されました。

クラス

  • java.awt.AWTPermission
  • java.util.zip.ZipError
  • javax.sound.sampled.AudioPermission

AWTPermissionクラスとAudioPermissionクラスはWindow.warningStringと同様にセキュリティマネージャが削除されたためです。

ZipError例外はエラー出ないZipExcpeion例外があるので、もはや使われていない例外のため削除予定です。

 

メソッド

  • java.awt.Window.getWarningString()
  • java.swing.JInternalString.getWarningString()

どちらもセキュリティマネージャが削除されたためで、現状はnullを返すだけになっています。

 

追加/変更されたAPI

いつもの通り、Preview JEPに関するAPI変更はここでは省略します。また、セキュリティ関連も省略します。

 

java.base/java.ioパッケージ

JEP 495で導入予定のjava.io.IOクラスに関連してConsoleクラスに2つのメソッドが追加されました。Java 23でもメソッドが追加されていましたが、追加の追加という感じ。

IOクラス自身はプレビューなので、Starndard JEPになったら説明します。まぁ、大したことではないんですけどね。

Consoleクラス

Consoleクラスには2つのメソッドが追加されました。

  • Console println()
  • Console readln()

printlnメソッドは改行を出力で、readlinメソッドは1行読み込みです。

いずれも引数があるオーバーロードがJava 23で導入されていたので、おまけのような感じ。

とはいっても、Java 23とJava 24で追加されたメソッド群はまだPreviewなので、使うには--enable-previewオプションが必要です。

 

Readerクラス

Readerクラスにファクトリメソッドが追加されました。

Reader/Writerクラスはアブストラクトクラスで、基本的にはデコレーターパターンで機能を付け加えるという使い方をしていました。しかし、ファクトリーメソッドができたということは、今後Readerクラスの使い方が変わるかもしれません。

  • static Reader of(CharSequence cs)

ofメソッドでは、引数で指定したCharSequenceオブジェクトから読み込んでいくReaderオブジェクトを生成します。

動作としては、StringReaderクラスとほぼ同じです。

 

java.base/java.langパッケージ

Java 24は2024年にリリースされたUnicode 16.0をサポートしました。これに対応して、文字関連のAPIが追加されています。

Charater.UnicodeBlockクラス

Unicode 16.0では新たに絵文字が追加されたり、ヒエログリフが追加されたことが話題になりましたね。

これに応じて、UncodeBlockクラスでは新たに10のブロックに対応する定数が追加されています。

  • static final UnicodeBlock EGYPTIAN_HIEROGLYPHS_EXTENDED_A
  • static final UnicodeBlock GARAY
  • static final UnicodeBlock GURUNG_KHEMA
  • static final UnicodeBlock KIRAT_RAI
  • static final UnicodeBlock MYANMAR_EXTENDED_C
  • static final UnicodeBlock OL_ONAL
  • static final UnicodeBlock SUNUWAR
  • static final UnicodeBlock SYMBOLS_FOR_LEGACY_COMPUTING_SUPPLEMENT
  • static final UnicodeBlock TODHRI
  • static final UnicodeBlock TULU_TIGALARI

 

Charater.UnicodeScript列挙型

UncodeScript列挙型でもUnicode 16.0に対応するため、新たに7つの定数が追加されています。

  • GARAY
  • GURUNG_KHEMA
  • KIRAT_RAI
  • OL_ONAL
  • SUNUWAR
  • TODHRI
  • TULU_TIGALARI

 

Processクラス

メソッドが1つだけオーバーロードされました。

  • boolean waitFor(java.time.Duration duration)

waitForメソッドはプロセスの終了を待つというメソッドです。

Java 23までは引数なしで終了を待つものと、タイムアウトをlongとTimeUnit列挙型で指定する2つのオーバーロードがありました。

Java 24では、これらに加えてタイムアウトをDate & Time APIのDurationクラスで指定するものが加わっています。

 

java.base/java.lang.classfileパッケージ

java.lang.classfileパッケージとそのサブパッケージはJEP 484 Class-File APIで使用するパッケージです。

簡単な紹介は次回の「JEPで語る」でしますが、詳細な説明は「バイトコード入門」の続編として書く予定です。

 

java.base/java.lang.reflectパッケージ

java.lang.reflectパッケージはいつも通り新しいバージョンに合わせた定数が追加されています。

ClassFileFormatVersion列挙型

いつものように、Java 24に対応する定数が追加されました。

  • RELEASE_24

 

java.base/java.util.streamパッケージ

JEP 485 Stream Gatherersによって、インタフェースとクラスが追加されています。

Gathererインタフェース

中間操作のためのインタフェースが追加されました。Gathererインタフェースを使うと、終端操作のCollectorインタフェース相当の機能が中間操作で実現できます。

 

Gatherersクラス

Gathererインタフェース用のユーティリティクラスです。

Stream Gathererの使い方は、本ブログの Stream Gatherer 基礎編 を参照してください。

 

Streamインタフェース

StreamインタフェースにはGathererを使うためのメソッドが追加されました。

  • <R> Stream<R> gather(Gatherer<? super T, ?, R> gatherer)

Stream Gathererの使い方は、本ブログの Stream Gatherer 基礎編 を参照してください。

 

おわりに

なんとAPIの変更はこれだけ!!

追加されたAPIとしては、Class-File APIがあるのでむちゃくちゃ多いのですが、それ以外はほんのわずか。Java 24はAPIの変更よりも、次のLTSのために不要な実装を削除するなどの整理が主になっている感じです。

また、プリミティブ型のパターンマッチングなどの文法の変更も次のLTSでStandard JEPになることが予想されます。

 

さて、次のエントリーではJEPに関して簡単な説明を加えていきます。

2025/03/06

バイトコード入門 その3 バイトコード処理の構成

このエントリーをはてなブックマークに追加

ちょっと間があいてしまいましたが、バイトコード入門の3回目です。今回はバイトコードをどのように処理してJVMの構成について紹介します。前回のスタックマシンがここで活かされてきます。

  1. 準備編
  2. スタックマシン
  3. バイトコード処理の構成 (今回)

 

JVMのメモリ構成

JVMで管理しているメモリ領域というと、多くの方がヒープと考えるのではないでしょうか。たしかに、ヒープは重要なメモリ領域であることには違いはありませんが、ヒープ以外にもJVMが管理しているメモリ領域があるのです。

HotSpot VMの場合、JVMが管理しているメモリ領域は大別して4種類あります。

  • ヒープ: オブジェクトの配置用の領域
  • メタスペース: クラス定義、メソッド定義、コンスタントプールなど
  • ネイティブメソッドスタック: ネイティブメソッド用領域
  • JVMスタック: コールスタック用領域

JVMSはJVMの仕様については記述されていますが、実装については記述されません。メモリ構成も実装に近いため大まかにしか説明されていませんが、JVMS 2.5 Run-Time Data Areasに記述があります。

なお、下図はJVMスタックを省略しています。

 

1つ目のヒープは、一番なじみがあるメモリ領域だと思います。

Javaのオブジェクトは必ずこのヒープに配置されます。GCの種類によってヒープはさらに細分化されるのですが、それはGCによるものでJVMSでは定義されていません。

たとえば、世代別GCを使用している場合、Young領域とOld領域に分けられるなどがこれに相当します。

2番目のメタスペースは静的なデータを保持させる領域で、クラスローダーごとに管理されています。

メタスペースというのはHotSpot VMの実装に基づいた領域名で、メタスペースに保存するデータとしてはクラス定義、メソッド定義などがあります。また、コンスタントプールもメタスペースに保持されます。

JVMSでは2.5.4 Method Area2.5.5 Run-Time Constant Poolがメタスペースの一部になっています。

ネイティブメソッドスタックは、JNIやFFMでネイティブメソッドを使用する際に使われる領域です。

最後のJVMスタック領域が今回メインで取り上げるメモリ領域です。JVMSでは2.5.2 Java Virtual Machine Stacksで定義されています。

これ以外にスレッドごとにどこを実行しているかを保持しておくPCレジスタ(Program Counter)もあります(JVMS 2.5.1)。

 

JVMスタック領域

例外がスローされた時に出力されるスタックトレースはJavaの開発者であれば誰もが見たことがあるはずですが、その意味を考えたことがありますか?

何かのスタックをトレースしたものということは分かると思います。そのスタックというのは、メソッドがコールされた順序を保持しておくスタックを指しています。一般的にはコールスタックと呼ばれるスタックです。

Javaの場合、このコールスタックはJVMスタックと呼ばれます。そして、そのJVMスタックが配置される領域がJVMスタック領域です。

なお、コールスタックが使用されていても、前回紹介したスタックマシンとは呼ばれないことに注意が必要です。

さて、JavaのJVMスタックは、並列処理が可能なようにスレッドごとに作られます。

そして、JVMスタックに積まれるのがフレームです(まちがえないとは思いますが、AWTのjava.awt.Frameクラスではないです)。

フレームはメソッドコールごとにJVMスタックに積まれます。たとえば、以下のようにmainからfoo、barとコールされる場合を考えてみましょう。

public class Main {
    static void bar() {}

    static void foo() {	bar(); }
    
    public static void main(String... args) {
	foo();
    }
}

Mainクラスを実行すると、まずmainに対応するフレームが積まれます。fooメソッドがコールされるとそれに対応するフレームが積まれます。

そして、fooメソッドからbarメソッドがコールされると、barメソッドに対応するフレームが積まれます。

barメソッドが完了して、fooメソッドに戻る時に、barメソッドに対応するフレームは削除されます。

同様にfooメソッドの完了時にfooメソッドに対応するフレームが削除され、mainメソッドが完了する時にmainメソッドに対応するフレームが削除され、JVMスタックは空になります。

 

ところで、StackOverflowErrorという例外に遭遇したことがあるでしょうか。再帰などでコードにバグがある時に遭遇することが多い例外です。

再帰では自分自身を延々とメソッドコールするわけですが、メソッドコールの連なりが限度を超えた場合にStackOverflowError例外がスローされます。

もうお分かりだとは思いますが、StackOverflowError例外のスタックとはJVMスタックのことです。

JVMスタックにフレームを積み過ぎてあふれてしまうと、StackOverflowError例外がスローされるのです。

同様に例外発生時に提示されるスタックトレースのスタックもJVMスタックです。

例外発生時に、どのメソッドのコールされていたかは、JVMスタックをたどれば分かります。これがスタックトレースです。

実際には、次節で紹介するJVMスタックに積まれるフレームの情報を含めてスタックトレースが作られます。

 

フレーム

JVMスタックに積まれるフレームはJVMS 2.6 Framesで定義されています。

フレームの主要な構成要素は以下の2つです。

  • ローカル変数用配列
  • オペランドスタック

 

ローカル変数用配列

1つ目のローカル変数用配列は、ローカル変数とメソッドの引数を保持させる配列です。

ローカル変数およびメソッド引数がいくつ使用するのかは、ソースコードをコンパイルする時に調べることができます。このため、配列の要素数はその個数分になります。

ローカル変数もしくは引数がプリミティブ型の場合、その値が直接配列に保持されます。参照型の場合はその参照が保持されます。

また、インスタンスメソッドの場合、インデックス0には必ずthisが入ります。

 

とこで、ローカル変数/メソッド引数の名前はコンパイルすると情報として残りません。メソッド内では、ローカル変数用配列のインデックスで指定されます。

しかし、これだとクラスファイルを読んだだけだと何が配列に入っているのかが分かりにくいんですよね。

こんな理解度の低い人間のためのことを、javacはちゃんと用意してくれてあります。それがコンパイルオプションの-gです。

-gはデバッグ情報をクラスファイルに埋め込むためのオプションです。

たとえば、次のメソッドで試してみましょう。

 

    void sayHello(String name) {
        var text = "Hello, " + name + "!";
        System.out.println(text);
    }

 

これを-gを使用せずにコンパイルし、javap -vで表示させると次のようになります。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: astore_2
         7: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 7
        line 5: 14

 

赤字で示したlocal=3がローカル変数用配列のサイズになります。

そして、次回、詳しく説明しますが、aload_1やastore_2の1や2が配列のインデックスになります。

しかし、そのインデックス1や2に何が保持されているかは、バイトコードから推測するしかありません。

 

そこで-gを使用してコンパイルしてみます。コンパイル結果を次に示します。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2
         0: aload_1
         1: invokedynamic #7,  0              // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String;
         6: astore_2
         7: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;
        10: aload_2
        11: invokevirtual #17                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        14: return
      LineNumberTable:
        line 3: 0
        line 4: 7
        line 5: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   LHello;
            0      15     1  name   Ljava/lang/String;
            7       8     2  text   Ljava/lang/String;

 

-gオプションを使用することで、最後にLocalVariableTableという表が追加されました。

この表のSlotが配列のインデックスになります。

sayHelloメソッドはインスタンスメソッドなので、前述したようにインデックス0にはthisが入ります。

インデックス1には引数のname、インデックス2にはローカル変数のtextです。

 

クラスファイルを読み慣れてくれば、Local Variable Tableがなくても、配列のどこに何が保持されているかは分かってきます。しかし、慣れないうちは、コンパイルオプションの-gをつけてコンパイルすることをお勧めします。

 

オペランドスタック

フレームのもう一方の構成要素がオペランドスタックです。名前の通り、オペランドを保持させるスタックです。

もちろん、このスタックがJVMをスタックマシンたらしめているスタックです。

オペランドというのは、処理命令であるオペコードの処理対象のデータのことになります。

バイトコードにはloadやstoreといったオペコードがあります。これらはローカル変数用配列からスタックにデータを積む、スタックの先頭データを取り出してローカル変数用配列に保持させるというようにスタックに対する処理になります。

他のバイトコードもほとんどがスタックやスタックのデータに対する命令となります。

 

このスタックのサイズも、コンパイル時に決まります。

先ほどのsayHelloメソッドの場合を見てみましょう。sayHelloメソッドの先頭部分を再掲します。

 

  void sayHello(java.lang.String);
    descriptor: (Ljava/lang/String;)V
    flags: (0x0000)
    Code:
      stack=2, locals=3, args_size=2

このstack=2がオペランドスタックのサイズとなります。

 

ちなみに、args_size、つまり引数の個数が2になっているのは、インスタンスメソッドの暗黙の引数としてthisが渡されるからです。

 

バイトコード処理構成のまとめ

長くなってきたので、今回はここまでとして、まとめてみましょう。

  • JVMのメモリ領域のうち、バイトコード処理に使われるのはJVMスタック領域
  • JVMスタックはスレッドごとに作成され、メソッドコールごとにフレームが積まれる
  • フレームはローカル変数用配列とオペランドスタックなどから構成される
  • オペランドスタックを使用してバイトコードの処理を行う

 

たぶん、次のエントリーはJEPで語れないシリーズのJava 24になると思うので、その後のエントリーでやっとバイトコード処理について解説できるはずです。

2025/02/12

バイトコード入門 その2 スタックマシン

このエントリーをはてなブックマークに追加

前回からはじめたバイトコード入門。なかなかバイトコードに入れなくても申し訳ないのですが、今回はスタックマシンについて紹介します。

  1. 準備編 (前回)
  2. スタックマシン (今回)
  3. バイトコード処理の構成

 

スタックマシン

スタックマシンというのは計算モデルの1つです。

スタックマシンと、よく引き合いに出される計算モデルにレジスタマシンがあります。

この2つの計算モデルはメモリーの使い方にあります。

  • スタックマシン: メモリーをスタックとする計算モデル
  • レジスタマシン: メモリーをレジスタとする計算モデル

純粋なスタックマシンはスタックだけで構成しますが、通常はランダムアクセスができるメモリーと組み合わせで使われることが多いです。これはレジスタマシンでも同様で、通常はレジスタ以外にメモリーを使用します。

スタックマシンにはJVMの他にも.NET Frameworkでも使用されています。

一方、既存のほとんどのCPUがレジスターマシンになります。

たとえば、レジスターマシンで加算を行う場合、aレジスタの値とbレジスタの値を加算して結果をaレジスタに格納するというような命令になります(add a, bのような感じです)。

命令の対象が明確に記述されているので、分かりやすいはずです(とはいうものの、機械語でプログラムを書けと言われてもイヤですけど😰)

では、スタックマシンはどのように動作するのでしょう?

ここでは、単純な例としてHPの電卓でスタックマシンの動作を説明していきます。

 

HPの電卓

HPというと、紆余曲折あってPCやプリンターのHP Inc.とサーバーのHPEになっていますが、かつては計測機器を扱う会社でした。そんなHPが1970年代から2000年代にかけて電卓を作っていたのでした。

当時は、関数電卓やプログラム電卓といったらHPという感じで、そこそこ使われていました。プログラム電卓というのは、その名の通りプログラムが組める電卓です。

このHPの電卓は、なんといっても入力方式が独特でした。

たとえば、1+2を計算する場合、通常の電卓であれば 1 + 2 = と入力しますね。これに対し、HPの電卓は 1 [Enter] 2 [Enter] + と入力しました([Enter]というボタンがあったのです)。

[Enter]を省略して記述すると 1 + 2 は 1 2 + になるということです。この数式の書き方は逆ポーランド記法と呼ばれています。

 

逆ポーランド記法

ちょっと脱線気味ですが、逆ポーランド記法についても説明しておきましょう。

「逆」とついていることから分かるかもしれませんが、「逆」ではないポーランド記法もあります。というか、こちらが先ですね。

ポーランド記法はJan Łukasiewicz (ヤン・ウカシェヴィチ)が発案した数式の記法です。

演算子を先に記述することから前置記法とも呼ばれます。

私たちが通常使用している 1 + 1 のような記法は演算子が数値の間に記述されるので中置記法と呼びます。逆ポーランド記法は演算子が最後なので、後置記法になります。

 

逆ポーランド記法の利点はカッコを使用せずに数式を記述できるところです。

たとえば、以下の数式はどうでしょう。

(2 + 3) × 5 + (4 - 2) ÷ 2

これを逆ポーランド記法で記述すると以下のようになります。

2 3 + 5 × 4 2 - 2 ÷ +

カッコがないというのは、電卓でメモリ機能(M+やM-、MRCなど)を使わなくても計算ができるということで、入力が簡単になります。まぁ、逆ポーランド記法で考えなければいけないというハードルはありますけど。

そして、もう1つの利点が、逆ポーランド記法はスタックを使えば簡単に実装できるということです。

 

スタックを使用した逆ポーランド記法の計算

では、逆ポーランド記法の数式をスタックを使用して計算してみましょう。

ルールは簡単です。

  1. 数値であれば(HPの電卓では、[Enter]が入力されたら)その値をスタックに積む
  2. 演算子であれば、演算に必要な個数のデータをスタックから取り出し、計算結果を再びスタックに積む

では、1 + 2をやってみましょう。

 

電卓であれば、スタックの先頭を表示していれば計算結果が表示されます。

複雑な数式でも計算の途中結果がスタックに保持されているので、スタックとは別のメモリーを使用しなくても計算が実行できます。

 

電卓では計算だけですが、一般のスタックマシンでも同様に必要なデータをスタックに置き、処理の結果を再びスタックに置くという過程で処理が進みます。

これはJVMでも同様です。

では、次回は実際にJVMでどのようにスタックマシンが構成されているのか紹介する予定です。