NetBeans
Java NetBeans
お正月休みも今日で終わってしまいます。
何処にも出かけず家でごろごろしていました。(^_^;)
居場所に困るのでこうやってパソコンでプログラムを組んで時間を無駄に使ってます。
今日は PhantomReference を試してみます。
その前に前回、前々回と PhantomReference の兄弟の SoftReference と WeakReference の御浚いをしておきましょう。
SoftReference はリファレントがソフト到達可能となっても使用頻度とヒープの状況から、ガーベージ・コレクタがリファレントを解放すべきだと判断するまで GC 対象にならない。
Weak Reference はリファレントが弱到達可能となったら GC 対象となる。
言葉での説明より簡単なプログラムと組んで実行してみたほうが理解しやすいです。
では、非常に解りやすいプログラムを組んでみました。
プログラムの中にいる”ぼく”はあなたの場合、SoftReference タイプでしょうか?
それとも Weak Reference タイプでしょうか?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
|
package jp.yucchi.softlovereference; import java.lang.ref.Reference; import java.lang.ref.SoftReference; /** * * @author Yucchi */ public class SoftLoveReference { public static void main(String[] args) { var softLoveReference = new SoftLoveReference(); // suzu をソフト到達可能オブジェクトにする。(boku から suzu の強参照を削除) softLoveReference.boku.suzu = null; System.out.println("--- 僕からすずへの愛を無くした。 ---"); // ソフト到達可能となった suzu はすぐにガーベジ・コレクト対象とならずに // ヒープの空き容量が不足したときに回収対象となる。 System.gc(); // リファレントを取得 var love = softLoveReference.boku.yui.suzu; if (love.get() != null) { // ソフト到達可能オブジェクト suzu はまだ消えていない。 love.get().isLove(); // リファレントをクリアする。 love.clear(); if (love.get() == null) { // ソフト到達可能オブジェクト suzu は消えている。 System.out.println("ガッキー、今までごめんね。僕のすずへの愛は完全に消えた。" + System.lineSeparator() + "これからはガッキーお前だけを愛する!"); } } else { // ソフト到達可能オブジェクトが消えている。 System.out.println("僕のすずへの愛は完全に消えた。"); } } Boku boku; public SoftLoveReference() { boku = new Boku(); boku.yui = new Yui(); boku.suzu = new Suzu(); // yui から suzu はソフト参照 boku.yui.suzu = new SoftReference<>(boku.suzu); } private static class Boku { Boku() { System.out.println("僕は二人の素敵な女性を愛してしまった。"); } Yui yui; Suzu suzu; } private static class Yui { Yui() { System.out.println("僕はガッキーを愛してる。"); } // 参照型フィールド Reference<Suzu> suzu = null; } private static class Suzu { Suzu() { System.out.println("僕はすずを愛してる。"); } void isLove() { System.out.println("ごめんね。ガッキー・・・ 僕のすずへの愛はまだ完全に消えていない。"); } } } |
このプログラムの実行結果はつぎのようになります。
僕は二人の素敵な女性を愛してしまった。
僕はガッキーを愛してる。
僕はすずを愛してる。
— 僕からすずへの愛を無くした。 —
ごめんね。ガッキー・・・ 僕のすずへの愛はまだ完全に消えていない。
ガッキー、今までごめんね。僕のすずへの愛は完全に消えた。
これからはガッキーお前だけを愛する!
次は Weak Reference を使ったプログラムです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
|
package jp.yucchi.weaklovereference; import java.lang.ref.Reference; import java.lang.ref.WeakReference; /** * * @author Yucchi */ public class WeakLoveReference { public static void main(String[] args) { var weakLoveReference = new WeakLoveReference(); // suzu を弱到達可能オブジェクトにする。(boku から suzu の強参照を削除) weakLoveReference.boku.suzu = null; System.out.println("--- 僕からすずへの愛を無くした。 ---"); // 弱到達可能となった suzu はガーベジ・コレクトの回収体制となる。 System.gc(); // リファレントを取得 var love = weakLoveReference.boku.yui.suzu; // 弱到達可能オブジェクト suzu は消えている。 if (love.get() != null) { // true の部分は実行されることはない。 love.get().isLove(); love.clear(); if (love.get() == null) { System.out.println("ガッキー、今までごめんね。僕のすずへの愛は完全に消えた。" + System.lineSeparator() + "これからはガッキーお前だけを愛する!"); } } else { // 弱到達可能オブジェクトが消えている。 System.out.println("僕のすずへの愛は完全に消えた。 ガッキー、愛してる!"); } } Boku boku; public WeakLoveReference() { boku = new Boku(); boku.yui = new Yui(); boku.suzu = new Suzu(); // yui から suzu 弱参照 boku.yui.suzu = new WeakReference<>(boku.suzu); } private static class Boku { Boku() { System.out.println("僕は二人の素敵な女性を愛してしまった。"); } Yui yui; Suzu suzu; } private static class Yui { Yui() { System.out.println("僕はガッキーを愛してる。"); } // 参照型フィールド Reference<Suzu> suzu = null; } private static class Suzu { Suzu() { System.out.println("僕はすずを愛してる。"); } void isLove() { System.out.println("ごめんね。ガッキー・・・ 僕のすずへの愛はまだ完全に消えていない。"); } } } |
このプログラムの実行結果はつぎのようになります。
僕は二人の素敵な女性を愛してしまった。
僕はガッキーを愛してる。
僕はすずを愛してる。
— 僕からすずへの愛を無くした。 —
僕のすずへの愛は完全に消えた。 ガッキー、愛してる!
プログラムの詳細はコメントと前回、前々回のエントリーを参照してくださいませ。
さて、あなたはどちらのタイプでしたでしょう?
それでは PhantomReference を試してみましょう!
PhantomReference を使うには モジュール java.base の java.lang.Object java.lang.ref.ReferenceQueue<T> クラスが必要となります。
そういえば、モジュール java.base の java.lang.Object java.lang.ref.Reference<T> のことも忘れていたのでこれらを先にざっくりと調べます。
参照オブジェクトための抽象基底クラスである Reference<T> クラスから調べます。
クラスReference<T>
このクラスは、すべての参照オブジェクトに対して共通のオペレーションを定義します。
参照オブジェクトはガベージ・コレクタと密接に連携して実装されるので、このクラスを直接サブクラス化することはできません。
メソッドは6個あります。
public void clear()
この参照オブジェクトをクリアします。 このメソッドを呼び出しても、このオブジェクトはキューに入りません。
このメソッドはJavaコードによってのみ呼び出されます。ガベージ・コレクタが参照をクリアするときは、このメソッドを呼び出さずに直接行います。
protected Object clone()throws CloneNotSupportedException
常時 CloneNotSupportedExceptionをスローします。 Referenceは、効果的にクローニングできません。 かわりに新規のReferenceを構築します。
public boolean enqueue()
この参照オブジェクトをクリアし、それが登録されているキューに登録オブジェクトを追加します。
このメソッドはJavaコードによってのみ呼び出されます。ガベージ・コレクタが参照をキューに入れるときは、このメソッドを呼び出さずに直接行います。
戻り値:この参照オブジェクトがキューに入れられた場合はtrue。すでにキューに入れられているか、作成時にキューに登録されなかった場合はfalse
public T get()
参照オブジェクトのリファレントを返します。 プログラムまたはガベージ・コレクタによって、この参照オブジェクトがすでにクリアされている場合、このメソッドはnullを返します。
戻り値:この参照が表すオブジェクト。この参照オブジェクトがクリアされている場合はnull
public boolean isEnqueued()
この参照オブジェクトが、プログラムまたはガベージ・コレクタによってキューに入れられているかどうかを判定します。
この参照オブジェクトが生成されたときにキューに登録されていない場合、このメソッドは常にfalseを返します。
戻り値:この参照オブジェクトがキューに入れられている場合にだけtrue
public static void reachabilityFence(Object ref)
指定された参照によって参照されるオブジェクトが、オブジェクトが到達不能になる可能性のあるプログラムの以前のアクションに関係なく、「強く到達可能な」のままであることを保証します。
このため、参照されたオブジェクトは、少なくともこのメソッドが呼び出されるまでガベージ・コレクションによって再要求できません。
次はモジュール java.base の java.lang.Object java.lang.ref.ReferenceQueue<T> を調べてみます。
クラスReferenceQueue<T>
参照キューです。到達可能性が適切に変更されたことが検出されると、登録されている参照オブジェクトはガベージ・コレクタによって参照キューに追加されます。
コンストラクタは引数の無いものが一つだけあります。
public ReferenceQueue()
新しい参照オブジェクト・キューを構築します。
メソッドは3個あります。
public Reference<? extends T> poll()
このキューをポーリングして、参照オブジェクトが利用可能かどうかを確認します。
参照オブジェクトが遅延なしで利用可能な場合、それがキューから削除されて、返されます。 それ以外の場合、このメソッドは即座にnullを返します。
戻り値:利用可能な参照オブジェクトがあった場合はその参照オブジェクト、そうでない場合はnull
public Reference<? extends T> remove()throws InterruptedException
このキューの次の参照オブジェクトを削除します。参照オブジェクトが利用可能になるまでブロックされます。
戻り値:参照オブジェクト。1つが利用可能になるまでブロックを行う
例外:InterruptedException – 待機中に割り込まれた場合
public Reference<? extends T> remove(long timeout)throws IllegalArgumentException, InterruptedException
このキューの次の参照オブジェクトを削除します。参照オブジェクトが利用可能になるか、指定されたタイム・アウトの期限が切れるまでブロックします。
このメソッドはリアルタイム保証を行いません。Object.wait(long)メソッドを呼び出した場合と同様にタイム・アウトのスケジュールを作成します。
パラメータ:timeout – 値が正の場合、このキューに参照が追加されるのを待つ間、timeoutミリ秒の間ブロックされる。 0の場合、無期限にブロックされる。
戻り値:指定されたタイム・アウト期間に利用可能になった場合は参照オブジェクト、そうでない場合はnull
例外:IllegalArgumentException – timeout引数の値が負の場合
InterruptedException – タイム・アウト待機中に割り込まれた場合
それでは次はモジュール java.base の java.lang.Object java.lang.ref.Reference<T> java.lang.ref.PhantomReference<T> クラスを調べます。
クラスPhantomReference<T>
ファントム参照オブジェクトです。ファントム参照オブジェクトがキューに入れられるのは、キューに入れておかないとそれらのリファレントが再生される可能性があるとコレクタが判断したときです。
幻の参照は、死後のクリーンアップ・アクションをスケジュールするために最も頻繁に使用されます。
ある時点でオブジェクトが「ファントム到達可能」であるとガベージ・コレクタが判断したとします。
その時点で、そのオブジェクトへのすべてのファントム参照と、そのオブジェクトに到達可能な他のファントム到達可能オブジェクトへのすべてのファントム参照を原子的にクリアします。
同時に、または後で、参照キューに登録されたそれらの新たにクリアされたファントム参照をエンキューします。
再生可能なオブジェクトをそのままにしておくために、ファントム参照のリファレントを取り出すことはできません。ファントム参照のgetメソッドは、常にnullを返します。
コンストラクタは引数を二つ取るものが一つだけあります。
public PhantomReference(T referent, ReferenceQueue<? super T> q)
指定されたオブジェクトを参照し、指定されたキューに登録されている新しいファントム参照を作成します。
nullキューでファントム・リファレンスを作成することは可能ですが、そのようなリファレンスはまったく役に立たない: そのgetメソッドは常にnullを返し、キューを持たないのでエンキューされません。
パラメータ:referent – 新しいファントム参照が参照するオブジェクト
q – 参照が登録されるキュー。登録が必要ない場合はnull
メソッドは一つだけあります。
public T get()
参照オブジェクトのリファレントを返します。
ファントム参照のリファレントは常にアクセス不可能なため、このメソッドは常にnullを返します。
戻り値:null
これで PhantomReference を使うための API をみてみました。
と、思いきや JDK 9 から java.lang.Object protected void finalize() throws Throwable が Deprecated になってしまいました。
代わりにモジュール java.base の java.lang.Object java.lang.ref.Cleaner クラスを使うことが推奨されているのでこちらもざっくりとみてみます。
クラスCleaner
Cleanerは、一連のオブジェクト参照と対応するクリーニング・アクションを管理します。
クリーナは、オブジェクトが有線で到達可能になったことがクリーナに通知された後で実行するために、クリーニング動作はregisteredです。
クリーナは、PhantomReferenceとReferenceQueueを使用して、reachabilityが変更されたときに通知を受け取ります。
各クリーナは、独立して動作し、保留中のクリーニング動作を管理し、クリーナがすでに使用されていないときにスレッドおよび終了を処理します。
オブジェクト参照と対応するクリーニング・アクションを登録すると、Cleanableが返されます。 最も効率的な使用法は、オブジェクトが閉じられたり不要になったときに、cleanメソッドを明示的に呼び出すことです。
クリーニング・アクションは、すでに明示的にクリーニングされていない限り、オブジェクトがファントム到達可能になったときに呼び出されるRunnableです。
クリーニング動作は、登録されているオブジェクトを参照してはならないことに注意してください。 そうであれば、オブジェクトは想像を絶するものにならず、クリーニング動作は自動的に呼び出されません。
クリーニング・アクションの実行は、クリーナに関連するスレッドによって実行されます。
クリーニング・アクションによってスローされたすべての例外は無視されます。
クリーナおよびその他のクリーニング・アクションは、クリーニング・アクションの例外の影響を受けません。
スレッドは、登録されているすべてのクリーニング処理が完了し、クリーナ自体がガベージ・コレクタによって再利用されるまで実行されます。
System.exit中のクリーナの動作は実装固有です。 クリーニング動作の呼び出しの有無は保証されていません。
ほかで指定がない場合、null引数をコンストラクタまたはこのクラスのメソッドへ渡すと、NullPointerExceptionがスローされます。
ネストされたクラスが一つあります。
public static interface Cleaner.Cleanable
Cleanableは、Cleanerに登録されているオブジェクトとクリーニング・アクションを表します。
ネストされたクラスにメソッドが一つだけあります。
void clean()
クリーニング可能なものの登録を解除し、クリーニング動作を呼び出します。 クリーニング可能なクリーニング動作は、cleanの呼び出し回数に関係なく、最大で1回呼び出されます。
Cleaner クラスにはメソッドが3個あります。
public static Cleaner create()
新しいCleanerを返します。
クリーナは、ファントム到達可能オブジェクトを処理し、クリーニング・アクションを呼び出すdaemon threadを作成します。
スレッドの「コンテキスト・クラス・ローダー」はsystem class loaderに設定されます。
スレッドはパーミッションを持たず、SecurityManager is setの場合にのみ実行されます。
ファントムに到達し、登録されたすべてのクリーニング動作が完了すると、クリーナは終了します。
戻り値:新しいCleaner
例外:SecurityException – 現在のスレッドがスレッドの作成または開始を許可されていない場合。
public static Cleaner create(ThreadFactory threadFactory)
ThreadFactoryからThreadを使用して新しいCleanerを返します。
スレッド・ファクトリのnewThreadメソッドからのスレッドはdaemon threadに設定され、ファントム到達可能オブジェクトを処理し、クリーン・アクションを起動します。
各呼び出しで、thread factoryは、クリーニング・アクションを実行するのに適したスレッドを提供する必要があります。
ファントムに到達し、登録されたすべてのクリーニング動作が完了すると、クリーナは終了します。
パラメータ:threadFactory – 新しいThreadを返してクリーニング・アクションを処理するThreadFactory
戻り値:新しいCleaner例外:IllegalThreadStateException – スレッド・ファクトリからのスレッドがnot a new threadの場合。
SecurityException – 現在のスレッドがスレッドの作成または開始を許可されていない場合。
public Cleaner.Cleanable register(Object obj, Runnable action)
オブジェクトにファントム到達可能になったときに実行するオブジェクトとクリーニング・アクションを登録します。
クリーニング動作の動作については、上記の「APIノート」を参照してください。
パラメータ:obj – 監視するオブジェクト
action – オブジェクトがファントム到達可能になったときに呼び出すRunnable
戻り値:Cleanableインスタンス
これで今度こそ PhantomReference を使う為の必要最小限の情報をざっくりと API ドキュメントから学んだ。
簡単に PhantomReference ってけっきょくどうなの?ってのを考えてみると
ファントム到達可能オブジェクトを ReferenceQueue に入れる。
注意:) ファントム到達可能オブジェクトの ReferenceQueue への挿入はガーベジ・コレクトと非同期で行われます。よって GC が働いてすぐに挿入されると考えてはいけないようです。
Reference<T> クラスの三つあるメソッド poll()、remove()、remove(long timeout) の何れかによって ReferenceQueue に入れられたオブジェクトを取り出してクリーンアップする。
これとは別に finalize() か Cleaner クラスを使ってクリーンアップする方法もあります。
クリーンアップの際はリソースの解放処理が必要であれば必ず実行するようにしておくのが無難かもしれない。
正しく理解できているかは怪しいけどプログラムを組んでみることにする。
とりあえず、前回、前々回に組んだ SoftReference と WeakReference の PhantomReference バージョンとしてみよう。
まず、JDK 1.8 対応のものから組んで、最新の JDK 11 で使える Cleaner クラスを使ったものに変更してみる。
そんなこんなで次のようなプログラムを組んでみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
|
package jp.yucchi.phantomexterminationclassic; import java.lang.management.GarbageCollectorMXBean; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryUsage; import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.util.ArrayList; import java.util.List; /** * * @author Yucchi */ public class PhantomExterminationClassic { private static final ReferenceQueue<MemoryConsumer> REFEREN_CEQUEU = new ReferenceQueue<>(); public static void main(String[] args) { System.out.println(System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")); System.out.println("JDK " + System.getProperty("java.version") + System.lineSeparator()); List<PhantomReference<MemoryConsumer>> list = new ArrayList<>(); for (int i = 0; i < 1_000; i++) { MemoryConsumer memoryConsumer = new MemoryConsumer(); PhantomReference<MemoryConsumer> phantomReference = new PhantomReference<>(memoryConsumer, REFEREN_CEQUEU); list.add(phantomReference); checkStatus(); } } private static void checkStatus() { MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); MemoryUsage usage = memoryBean.getHeapMemoryUsage(); long usedMemory = usage.getUsed(); List<GarbageCollectorMXBean> gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); StringBuilder gcStatus = new StringBuilder(); gcMXBeans.forEach(e -> { gcStatus .append(e.getName()) .append(" ") .append("GC Count: ") .append(e.getCollectionCount()) .append(" ") .append("GCTime = ") .append(e.getCollectionTime()) .append(" "); }); System.out.print("UsedMemory: " + usedMemory + " "); System.out.println(gcStatus); // Reference<?> ref; // while ((ref = REFEREN_CEQUEU.poll()) != null) { // System.out.println(" --- POLL:" + ref + " ---"); // if (Double.parseDouble(System.getProperty("java.specification.version")) < 1.9) { // ref.clear(); // } // } } private static class MemoryConsumer { private byte[] buff; MemoryConsumer() { this.buff = new byte[100 * 1024 * 1024]; } @Override protected void finalize() { System.out.println(" --- finalize ---"); this.buff = null; } } } |
このプログラムは finalize() メソッドをオーバーライドして利用しています。
finalize() メソッド内で this.buff = null; として OutOfMemoryError を回避しています。(このようなことは JDK 11 では必要ではありませんでした。)
Reference<T> クラスの poll() メソッドはコメントアウトして実行できないようにしてあります。
ちなみに finalize() メソッドも使わなければ OutOfMemoryError でプログラムは途中で止まってしまいます。
プログラムの実行結果は次のようになります。
Windows 10 10.0 amd64
JDK 1.8.0_144
UsedMemory: 115595064 PS Scavenge GC Count: 0 GCTime = 0 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 241927592 PS Scavenge GC Count: 0 GCTime = 0 PS MarkSweep GC Count: 0 GCTime = 0
— finalize —
— finalize —
UsedMemory: 321118712 PS Scavenge GC Count: 1 GCTime = 128 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 430987664 PS Scavenge GC Count: 1 GCTime = 128 PS MarkSweep GC Count: 0 GCTime = 0
— finalize —
— finalize —
UsedMemory: 525473600 PS Scavenge GC Count: 2 GCTime = 257 PS MarkSweep GC Count: 0 GCTime = 0
略
UsedMemory: 6299540808 PS Scavenge GC Count: 26 GCTime = 30760 PS MarkSweep GC Count: 24 GCTime = 6659
UsedMemory: 6404398424 PS Scavenge GC Count: 26 GCTime = 30760 PS MarkSweep GC Count: 24 GCTime = 6659
UsedMemory: 6509256040 PS Scavenge GC Count: 26 GCTime = 30760 PS MarkSweep GC Count: 24 GCTime = 6659
UsedMemory: 6614113656 PS Scavenge GC Count: 26 GCTime = 30760 PS MarkSweep GC Count: 24 GCTime = 6659
57秒ほどで終了しました。
ちょっと遅いですね。
NetBeans のプロファイラで確認してみます。
JDK 1.8 の標準のガーベジ・コレクタってこんなものなのか?
それでは先ほどのプログラムを Reference<T> クラスの poll() メソッドを使うように変更した実行結果は次のようになりました。
Windows 10 10.0 amd64
JDK 1.8.0_144
UsedMemory: 115595064 PS Scavenge GC Count: 0 GCTime = 0 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 241927592 PS Scavenge GC Count: 0 GCTime = 0 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 315733616 PS Scavenge GC Count: 1 GCTime = 129 PS MarkSweep GC Count: 0 GCTime = 0
— POLL:java.lang.ref.PhantomReference@548c4f57 —
— POLL:java.lang.ref.PhantomReference@1218025c —
UsedMemory: 425602568 PS Scavenge GC Count: 1 GCTime = 129 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 525440640 PS Scavenge GC Count: 2 GCTime = 260 PS MarkSweep GC Count: 0 GCTime = 0
— POLL:java.lang.ref.PhantomReference@816f27d —
— POLL:java.lang.ref.PhantomReference@87aac27 —
UsedMemory: 640519896 PS Scavenge GC Count: 2 GCTime = 260 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 745377512 PS Scavenge GC Count: 2 GCTime = 260 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 850235128 PS Scavenge GC Count: 2 GCTime = 260 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 955092744 PS Scavenge GC Count: 2 GCTime = 260 PS MarkSweep GC Count: 0 GCTime = 0
UsedMemory: 629860128 PS Scavenge GC Count: 3 GCTime = 569 PS MarkSweep GC Count: 1 GCTime = 43
— POLL:java.lang.ref.PhantomReference@3e3abc88 —
略
— POLL:java.lang.ref.PhantomReference@cb644e —
UsedMemory: 5880074344 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 5984931960 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6089789576 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6194647192 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6299504808 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6404362424 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6509220040 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6614077656 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6718935272 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
UsedMemory: 6823792888 PS Scavenge GC Count: 26 GCTime = 30258 PS MarkSweep GC Count: 24 GCTime = 6566
56秒で終了しました。
finalize() メソッドを使ったのとほぼ同じですね。
ちなみに64行目の ref.clear() メソッドを実行して参照オブジェクトをクリアしないと OutOfMemoryError でプログラムは途中で止まってしまいます。
JDK 11 では clear() メソッドで参照オブジェクトをクリアしなくてもOutOfMemoryError でプログラムで止まることはありません。
いちおう NetBeans のプロファイラで確認します。
大きな違いはなさそうです。
それでは JDK 11 に切り替えて finalize() メソッド を利用したプログラムを実行してみます。
JDK 1.8 との比較のためにあえて finalize() メソッド を使ってます。あとで Cleaner クラスを使ったものも試してみます。
プログラムの実行結果は次のようになりました。
Windows 10 10.0 amd64
JDK 11.0.1
UsedMemory: 113246208 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 222298112 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 331350016 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 440401920 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
— finalize —
— finalize —
— finalize —
— finalize —
UsedMemory: 546494784 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 655546688 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
— finalize —
— finalize —
UsedMemory: 328435288 G1 Young Generation GC Count: 2 GCTime = 7 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 437487192 G1 Young Generation GC Count: 2 GCTime = 7 G1 Old Generation GC Count: 0 GCTime = 0
略
— finalize —
— finalize —
— finalize —
UsedMemory: 2073652944 G1 Young Generation GC Count: 106 GCTime = 244 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 2182704848 G1 Young Generation GC Count: 106 GCTime = 244 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 2291756752 G1 Young Generation GC Count: 106 GCTime = 244 G1 Old Generation GC Count: 0 GCTime = 0
15秒で終了しました。
JDK 11 のガーベジ・コレクタ優秀ですね。
確か JDK 9 から G1GC が標準になったはず。
これも NetBeans のプロファイラで覗いてみます。
この違いを目の当たりにすると JDK 1.8 はもう使えない。(^_^;)
では、Reference<T> クラスの poll() メソッドを使うように変更して実行してみます。
プログラムの実行結果は次のようになります。
Windows 10 10.0 amd64
JDK 11.0.1
UsedMemory: 113246208 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 222298112 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 331350016 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 440401920 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 110281288 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
— POLL:java.lang.ref.PhantomReference@d8355a8 —
— POLL:java.lang.ref.PhantomReference@5e5792a0 —
— POLL:java.lang.ref.PhantomReference@26653222 —
— POLL:java.lang.ref.PhantomReference@3532ec19 —
UsedMemory: 219333192 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 328385096 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 437437000 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 110345216 G1 Young Generation GC Count: 2 GCTime = 7 G1 Old Generation GC Count: 0 GCTime = 0
— POLL:java.lang.ref.PhantomReference@68c4039c —
略
— POLL:java.lang.ref.PhantomReference@5db250b4 —
— POLL:java.lang.ref.PhantomReference@223f3642 —
UsedMemory: 219733600 G1 Young Generation GC Count: 99 GCTime = 208 G1 Old Generation GC Count: 0 GCTime = 0
これも15秒で終了しました。
NetBeans のプロファイラで覗いてみます。
こちらのほうが僅かだけど良いかもしれない。
JDK 1.8 と JDK 11 の違いはガーベジ・コレクタが変更されたくらいだと思うのですけど他にも何か変更があるのかもしれません。
ちょっと気になるので JDK 1.8 と JDK 11 のスレッドを確認してみます。
両方とも Reference<T> クラスの poll() メソッドを使用した場合です。
まず、JDK 1.8 から
次は JDK 11です。
JDK 11 のほうに Common-Cleaner という見慣れないスレッドがありますね。
これがパフォーマンスアップの要因の一つなのかな。
GC 関連のことはあまり気にしたことないのでよく解りません。(>_<。)
名前からして勝手にクリーンアップしてくれそうなので finalize() メソッド、Reference<T> クラスの poll() メソッド両方ともコメントアウトして無効にして実行してみました。
OutOfMemoryError でプログラムが止まりまし・・・ あれっ! 止まらない。
プログラムは15秒で終了しました。
Windows 10 10.0 amd64
JDK 11.0.1
UsedMemory: 113246208 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 222298112 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 331350016 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 440401920 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 110281920 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 219333824 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 328385728 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 437437632 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 110325528 G1 Young Generation GC Count: 2 GCTime = 6 G1 Old Generation GC Count: 0 GCTime = 0
略
UsedMemory: 1092084152 G1 Young Generation GC Count: 98 GCTime = 200 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1201136056 G1 Young Generation GC Count: 98 GCTime = 200 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 110617168 G1 Young Generation GC Count: 99 GCTime = 202 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 219669072 G1 Young Generation GC Count: 99 GCTime = 202 G1 Old Generation GC Count: 0 GCTime = 0
BUILD SUCCESSFUL (total time: 15 seconds)
いったいどういうことなんだ?
クリーンアップの際にリソースの解放処理が必要で無いのなら何もしなくてもいいのか?
JDK 1.8 で Reference<T> クラスの poll() メソッドを使う場合は clear() メソッドで参照オブジェクトをクリアして次の GC で解放されるという最低でも GC が二回必要だったのに・・・
ガーベジ・コレクタ凄く賢くなっている。(私が PhantomReference の使い方を間違ってるかもしれない)
このあたりのことを調べ出すとガーベジ・コレクタ沼にはまって苦しむことになるので誰か親切な人が詳しく解りやすい解説記事をネット上にアップしてくれるのを待つことにする。
それでは最後に Cleaner クラス を使ってみます。
この Cleaner クラスもファイナライザ同様にあまり使うのを好ましく思われていません。
詳細は「Effective Java 第3版」の第2章 オブジェクトの生成と消滅 項目 8 ファイナライザとクリーナーを避ける をご覧ください。
それでは Cleaner クラスを使ってみます。
これまでのプログラムを JDK 10 以降の新しいものを使うことを前提にして組み直しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101
|
package jp.yucchi.phantomextermination; import java.lang.management.ManagementFactory; import java.lang.ref.Cleaner; import java.lang.ref.PhantomReference; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.util.ArrayList; import java.util.List; /** * * @author Yucchi */ public class PhantomExtermination { private static final ReferenceQueue<MemoryConsumer> REFEREN_CEQUEU = new ReferenceQueue<>(); public static void main(String[] args) { System.out.println(System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")); System.out.println("JDK " + System.getProperty("java.version") + System.lineSeparator()); List<PhantomReference<MemoryConsumer>> list = new ArrayList<>(); for (int i = 0; i < 1_000; i++) { var memoryConsumer = new MemoryConsumer(); var cleaner = Cleaner.create(); cleaner.register(memoryConsumer, new CleanerRunnable(memoryConsumer.buff, i)); var phantomReference = new PhantomReference<>(memoryConsumer, REFEREN_CEQUEU); list.add(phantomReference); checkStatus(); } } private static void checkStatus() { var memoryBean = ManagementFactory.getMemoryMXBean(); var usage = memoryBean.getHeapMemoryUsage(); var usedMemory = usage.getUsed(); var gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); var gcStatus = new StringBuilder(); gcMXBeans.forEach(e -> { gcStatus .append(e.getName()) .append(" ") .append("GC Count: ") .append(e.getCollectionCount()) .append(" ") .append("GCTime = ") .append(e.getCollectionTime()) .append(" "); }); System.out.print("UsedMemory: " + usedMemory + " "); System.out.println(gcStatus); // Reference<?> ref; // while ((ref = REFEREN_CEQUEU.poll()) != null) { // System.out.println(" --- POLL:" + ref + " ---"); // } } private static class CleanerRunnable implements Runnable { private int ObjectNumber; private byte[] buff; CleanerRunnable(int ObjectNumber) { } private CleanerRunnable(byte[] buff, int ObjectNumber) { this.ObjectNumber = ObjectNumber; this.buff = buff; } @Override public void run() { System.out.println("Cleaning: " + ObjectNumber++); buff = null; } } private static class MemoryConsumer { private final byte[] buff; MemoryConsumer() { this.buff = new byte[100 * 1024 * 1024]; } } <font face="メイリオ"></font> } |
30行目で Cleaner を生成、31行目でファントム到達可能になったときに実行するオブジェクトとクリーニング・アクションを登録します。
var cleaner = Cleaner.create();
cleaner.register(memoryConsumer, new CleanerRunnable(memoryConsumer.buff, i));
クリーニング・アクションの登録でnew CleanerRunnable(memoryConsumer.buff, i) と無駄なことをしています。
これはお遊びですので引数無しでクリーニング・アクションの run() メソッド内のゴニョゴニョしたコードは無くて空でも問題無いです。
84行目の buff = null; もあっても無くてもほぼ変わりはありません。(少しは変わるかもと思って試しただけです)
たったこれだけです!非常に便利です。
これでファイナライザ攻撃の心配はなくなります。
プログラムの実行結果は次のようになります。
Windows 10 10.0 amd64
JDK 11.0.1
UsedMemory: 113246208 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 222298112 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 331350016 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 440401920 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 554883024 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 663934928 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 772986832 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 882038736 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 991090640 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1096034664 G1 Young Generation GC Count: 2 GCTime = 5 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1205086568 G1 Young Generation GC Count: 2 GCTime = 5 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1314138472 G1 Young Generation GC Count: 2 GCTime = 5 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1419045944 G1 Young Generation GC Count: 3 GCTime = 8 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1532292152 G1 Young Generation GC Count: 3 GCTime = 8 G1 Old Generation GC Count: 0 GCTime = 0
Cleaning: 6
Cleaning: 1
Cleaning: 5
Cleaning: 3
Cleaning: 7
Cleaning: 0
Cleaning: 2
Cleaning: 4
Cleaning: 9
UsedMemory: 1645538360 G1 Young Generation GC Count: 3 GCTime = 8 G1 Old Generation GC Count: 0 GCTime = 0
Cleaning: 11
Cleaning: 10
Cleaning: 8
UsedMemory: 1754590264 G1 Young Generation GC Count: 3 GCTime = 8 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1863642168 G1 Young Generation GC Count: 3 GCTime = 8 G1 Old Generation GC Count: 0 GCTime = 0
Cleaning: 16
Cleaning: 15
Cleaning: 13
Cleaning: 14
Cleaning: 12
UsedMemory: 655688552 G1 Young Generation GC Count: 4 GCTime = 12 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 764740456 G1 Young Generation GC Count: 4 GCTime = 12 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 873792360 G1 Young Generation GC Count: 4 GCTime = 12 G1 Old Generation GC Count: 0 GCTime = 0
Cleaning: 19
Cleaning: 17
Cleaning: 18
UsedMemory: 437556936 G1 Young Generation GC Count: 5 GCTime = 15 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 546608840 G1 Young Generation GC Count: 5 GCTime = 15 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 655660744 G1 Young Generation GC Count: 5 GCTime = 15 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 764712648 G1 Young Generation GC Count: 5 GCTime = 15 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 873764552 G1 Young Generation GC Count: 5 GCTime = 15 G1 Old Generation GC Count: 0 GCTime = 0
Cleaning: 20
Cleaning: 21
Cleaning: 24
略
UsedMemory: 1314754576 G1 Young Generation GC Count: 170 GCTime = 285 G1 Old Generation GC Count: 0 GCTime = 0
Cleaning: 993
Cleaning: 992
Cleaning: 997
Cleaning: 995
Cleaning: 996
Cleaning: 991
Cleaning: 994
UsedMemory: 882748320 G1 Young Generation GC Count: 171 GCTime = 287 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 991800224 G1 Young Generation GC Count: 171 GCTime = 287 G1 Old Generation GC Count: 0 GCTime = 0
16秒で終了しました。
Cleaner クラス使うと便利ですね。
ファントム到達可能になったときに実行するオブジェクトとクリーニング・アクションを登録するだけだし、それにクリーニング・アクション用の専用スレッドが起動される。
ファイナライザを使って何かしようとすると自前でスレッドを用意したりしなくちゃいけない場合がある。
ファイナライザ攻撃によるセキュリティ上の問題も無い。
もう、ファイナライザを使う必要は無いような気がする。
どのみち近い将来使えなくなるしね。
パッケージ java.lang.ref を良く解らないままいろいろやってみたけど PhantomReference って扱いが難しいですね。
そもそもファントム到達オブジェクトって強参照、ソフト参照、弱参照のどれから辿っても到達できなくなってファイナライザ呼び出しが完了した状態ってところで使いどころが限定されそうな気がする。
ReferenceQueue に入れられた PhantomReference の get() メソッドは null を常時返すようになっている。
つまり、これって GC 対象ってことですよね。
だめだ、頭痛が痛くなりそうだ。(まさに今こんな感じです。)
PhantomReference ってただのクリーンアップ用にしか思えなくなってきた。(^_^;)
しかも、JDK 1.8 の頃のほうが不思議な動作をせず理解しやすいけど JDK 11 との違いは大きすぎる。
パフォーマンスアップや使い勝手が良くなるのはいいけど解りやすい情報が乏しいのはなんとかしてほしい。
今日でお正月休みも終わってしまうけどもっと Java を楽しめる一年になるようにがんばるぞい!
TAGS: Java,NetBeans |
2019年1月3日6:21 AM |
Java NetBeans
このエントリーは、SoftReference エントリーの続きです。
http://yucchi.jp/blog/?p=2675
SoftReference で組んだプログラムの実行結果が想像していたのと違っていたので今度は WeakReference で遊んでみます。
まず、WeakReference ってどんなものなのか調べてみます。
APIドキュメントには次のように記されてます。
弱参照オブジェクトです。弱参照オブジェクトは、その弱参照オブジェクトのリファレントがファイナライズ可能になり、ファイナライズされ、そして再生されることを阻止することはありません。
弱参照は、ほとんどの場合で正規化マッピングを実装するために使用されます。
ある時点で、オブジェクトが弱到達可能であると、ガベージ・コレクタが判断したとします。
その時点で、ガベージ・コレクタは、そのオブジェクトへの弱参照すべてと、強参照およびソフト参照のチェーンを経由してそのオブジェクトに到達できるような、ほかの弱到達可能なオブジェクトへの弱参照すべてを、原子的にクリアします。
同時に、ガベージ・コレクタは以前に弱到達可能なオブジェクトがすべてファイナライズ可能であることを宣言します。
同時にまたはあとで、ガベージ・コレクタは、参照キューに登録されているそれらの新しくクリアされた弱参照をキューに入れます。
WeakReference では弱到達可能になったら問答無用で何も考えずに GC が働くときに無条件で参照を解放されるってことかな。
SoftReference のように参照の使用頻度やヒープの状況を考慮して参照を解放するのとは大違いですね。
ちなみにこの WeakReference はこの特性をいかして WeakHashMap に使われていますね。
java.util.WeakHashMap: キーが弱可到達になったらエントリごと削除する Map
WeakReference クラスにはコンストラクタが二つあります。
WeakReference(T referent)
指定されたオブジェクトを参照する、新しい弱参照を作成します。新しい参照は、どのキューにも登録されません。
WeakReference(T referent, ReferenceQueue<? super T> q)
指定されたオブジェクトを参照し、指定されたキューに登録されている新しい弱参照を作成します。
パラメータ:referent – 新しい弱参照が参照するオブジェクトq – 参照が登録されるキュー。登録が必要ない場合はnull
メソッドは無いですね。
それではプログラムを組んで試してみます。
前回組んだプログラムと同じように無駄にメモリーを消費するインスタンスを1000個作っているだけです。
今回のプログラムには無駄にメモリーを消費するオブジェクトに finalize() メソッドを実装しました。
特にそれによって何かするわけでなく呼ばれたことを確認するためだけです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
|
package jp.yucchi.weakreference_g1gc; import java.lang.management.ManagementFactory; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; /** * * @author Yucchi */ public class WeakReference_G1GC { private static final ReferenceQueue<MemoryConsumer> REFEREN_CEQUEU = new ReferenceQueue<>(); public static void main(String[] args) { System.out.println(System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")); System.out.println("JDK " + System.getProperty("java.version") + System.lineSeparator()); List<WeakReference<MemoryConsumer>> list = new ArrayList<>(); for (int i = 0; i < 1_000; i++) { var memoryConsumer = new MemoryConsumer(); var weakReference = new WeakReference<>(memoryConsumer, REFEREN_CEQUEU); list.add(weakReference); checkStatus(); } } private static void checkStatus() { var memoryBean = ManagementFactory.getMemoryMXBean(); var usage = memoryBean.getHeapMemoryUsage(); var usedMemory = usage.getUsed(); var gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); var gcStatus = new StringBuilder(); gcMXBeans.forEach(e -> { gcStatus .append(e.getName()) .append(" ") .append("GC Count: ") .append(e.getCollectionCount()) .append(" ") .append("GCTime = ") .append(e.getCollectionTime()) .append(" "); }); System.out.print("UsedMemory: " + usedMemory + " "); System.out.println(gcStatus); Reference<?> ref; while ((ref = REFEREN_CEQUEU.poll()) != null) { System.out.println(" --- POLL:" + ref + " ---"); } } private static class MemoryConsumer { private final byte[] buff; MemoryConsumer() { this.buff = new byte[100 * 1024 * 1024]; } @Override protected void finalize() { System.out.println(" --- finalize() ---"); } } } |
プログラムの実行結果は次のようになりました。
Windows 10 10.0 amd64
JDK 11.0.1
UsedMemory: 113246208 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 222298112 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 331350016 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 440401920 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
— finalize() —
— finalize() —
— finalize() —
— finalize() —
UsedMemory: 546492432 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
— POLL:java.lang.ref.WeakReference@d8355a8 —
— POLL:java.lang.ref.WeakReference@59fa1d9b —
— POLL:java.lang.ref.WeakReference@28d25987 —
— POLL:java.lang.ref.WeakReference@4501b7af —
— finalize() —
UsedMemory: 219385672 G1 Young Generation GC Count: 2 GCTime = 6 G1 Old Generation GC Count: 0 GCTime = 0
— POLL:java.lang.ref.WeakReference@523884b2 —
UsedMemory: 332631880 G1 Young Generation GC Count: 2 GCTime = 6 G1 Old Generation GC Count: 0 GCTime = 0
略
UsedMemory: 1201289840 G1 Young Generation GC Count: 115 GCTime = 252 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1310341744 G1 Young Generation GC Count: 115 GCTime = 252 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 1419393648 G1 Young Generation GC Count: 115 GCTime = 252 G1 Old Generation GC Count: 0 GCTime = 0
SoftReference とは大違いです。
プログラムの処理時間も15秒と非常に短い時間です。
GC 累積時間もぜんぜん違います。
NetBeans のプロファイラで確認してみます。
SoftReference と比べると使用しているヒープメモリーサイズも全然少ないし、GC タイムも雲泥の差がある。
これを見るとなるだけ SoftReference は使いたくないと思ってしまう。
でも、SoftReference だったらソフト到達可能なオブジェクトへのすべてのソフト参照は、仮想マシンが OutOfMemoryError をスローする前にクリアされていることが保証されている。
SoftReference はメモリの容量に応じてスケールするキャッシュ用って感じかな。
WeakReference は弱参照オブジェクトが弱到達可能になったらガベージ・コレクタでクリアしたい時に使う(キャッシュしない)。
API ドキュメントの文章が難しく書かれているのであまり自信はないけどこのような違いがあるようだ。
プログラムの実行結果からもその様子が少しは解ります。
ちなみに SoftReference と WeakReference の違いはプログラムの出力結果のガベージ・コレクタの動作ではっきりと解ります。
WeakReference の弱到達可能なオブジェクトは Young Generation 領域に対するマイナー GC ですぐ回収されています。
それに対して SoftReference のソフト到達可能オブジェクトは G1 Old Generation 領域に移されヒープが不足して OutOfMemoryError をスローしないところまで回収されないようです。
SoftReference: UsedMemory: 6871416088 G1 Young Generation GC Count: 222 GCTime = 332 G1 Old Generation GC Count: 12 GCTime = 8964
WeakReference: UsedMemory: 1419393648 G1 Young Generation GC Count: 115 GCTime = 252 G1 Old Generation GC Count: 0 GCTime = 0
なかなか面白い仕組みになっていますね。
強参照しか普段使わない私にとって新鮮な気分です。
しかし、これって JDK 1.2 から追加された古い API のようです。(^_^;)
いったいどれだけのプログラマが意識して使っているのか、いや・・・使わざるを得なかったのか・・・
さて、次は PhantomReference を試してみよう。
たぶん、続く。
TAGS: Java,NetBeans |
2019年1月2日5:06 AM |
Java NetBeans
あけましておめでとうございます。
とうとう今年の5月で平成が終わってしまいます。
新しい元号がまだ発表されてないので一部の人は困っているようですがどうにかならないものなんでしょうか?
さて、どうにもならないことをどうかしようとすると頭が痛くなるから現実逃避のために Java で遊ぶとしよう。
SoftReference って聞いたことはあるけど実際にプログラムを組む上で使った記憶がないのでこれで遊んでみよう。
java.base モジュールの java.lang.ref パッケージにある public class SoftReference<T> extends Reference<T> クラスです。
API ドキュメントには下記のように記されてます。
メモリー要求に応じてガベージ・コレクタの判断でクリアされるソフト参照オブジェクトです。
ソフト参照は通常、メモリー・センシティブなキャッシュを実装するために使用されます。
ある時点で、オブジェクトがソフト到達可能であると、ガベージ・コレクタが判断したとします。
その時点で、ガベージ・コレクタは、そのオブジェクトへのソフト参照すべてと、強参照のチェーンを経由してそのオブジェクトに到達できるような、ソフト到達可能なほかのオブジェクトへのソフト参照すべてを原子的にクリアすることを選択できます。
同時にまたはあとで、ガベージ・コレクタは、参照キューに登録されているそれらの新しくクリアされたソフト参照をキューに入れます。
ソフト到達可能なオブジェクトへのすべてのソフト参照は、仮想マシンがOutOfMemoryErrorをスローする前にクリアされていることが保証されています。
そうでない場合、ソフト参照がクリアされる時点、またはさまざまなオブジェクトへの一連のソフト参照がクリアされる順序に制約はありません。
ただし、仮想マシンの実装は、最近作成されたソフト参照または最近使用されたソフト参照をクリアしないことが奨励されます。
このクラスの直接のインスタンスは、単純なキャッシュを実装するために使用できます。
このクラスまたは派生したサブクラスは、より洗練されたキャッシュを実装するために、もっと大きなデータ構造でも使用できます。
ソフト参照のリファレントが強到達可能であるかぎり、つまり実際に使用されているかぎり、ソフト参照はクリアされません。
このため、たとえば洗練されたキャッシュは、エントリへの強いリファレントを維持することで、もっとも新しく使用されたエントリが破棄されることを防ぎ、ほかのエントリはガベージ・コレクタの判断で破棄されるようにできます。
つまり、ガベージ・コレクタが参照の使用頻度とヒープの状態をみて参照の解放を勝手にやってくれるので OutOfMemoryError でプログラムが止まってしまうことがない。
ガベージ・コレクタが必死でがんばってくれる!私は感謝するぞ!
で、この SoftReference<T> クラスにはコンストラクタが2種類あります。
SoftReference(T referent)
指定されたオブジェクトを参照する新しいソフト参照を作成します。新しい参照は、どのキューにも登録されません。
パラメータ:referent – 新しいソフト参照が参照するオブジェクト
SoftReference(T referent, ReferenceQueue<? super T> q)
指定されたオブジェクトを参照し、指定されたキューに登録されている新しいソフト参照を作成します。
パラメータ:referent – 新しいソフト参照が参照するオブジェクトq – 参照が登録されるキュー。登録が必要ない場合はnull
メソッドは一つだけあります。
public T get()
参照オブジェクトのリファレントを返します。 プログラムまたはガベージ・コレクタによって、この参照オブジェクトがすでにクリアされている場合、このメソッドはnullを返します。
戻り値:この参照が表すオブジェクト。この参照オブジェクトがクリアされている場合はnull
もの凄く質素なクラスですね。
そう言うことでさっそくお試しプログラムを組んでみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77
|
package jp.yucchi.softreference_g1gc; import java.lang.management.ManagementFactory; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.SoftReference; import java.util.ArrayList; import java.util.List; /** * * @author Yucchi */ public class SoftReference_G1GC { private static final ReferenceQueue<MemoryConsumer> REFEREN_CEQUEU = new ReferenceQueue<>(); public static void main(String[] args) { System.out.println(System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch")); System.out.println("JDK " + System.getProperty("java.version") + System.lineSeparator()); List<SoftReference<MemoryConsumer>> list = new ArrayList<>(); for (int i = 0; i < 1_000; i++) { var memoryConsumer = new MemoryConsumer(); var softReference = new SoftReference<>(memoryConsumer, REFEREN_CEQUEU); list.add(softReference); checkStatus(); } } private static void checkStatus() { var memoryBean = ManagementFactory.getMemoryMXBean(); var usage = memoryBean.getHeapMemoryUsage(); var usedMemory = usage.getUsed(); var gcMXBeans = ManagementFactory.getGarbageCollectorMXBeans(); var gcStatus = new StringBuilder(); gcMXBeans.forEach(e -> { gcStatus .append(e.getName()) .append(" ") .append("GC Count: ") .append(e.getCollectionCount()) .append(" ") .append("GCTime = ") .append(e.getCollectionTime()) .append(" "); }); System.out.print("UsedMemory: " + usedMemory + " "); System.out.println(gcStatus); Reference<?> ref; while ((ref = REFEREN_CEQUEU.poll()) != null) { System.out.println(" --- POLL:" + ref + " ---"); } } private static class MemoryConsumer { private final byte[] buff; MemoryConsumer() { this.buff = new byte[100 * 1024 * 1024]; } } } |
無駄にメモリーを消費するインスタンスを1000個作っているだけです。
普通なら OutOfMemoryError でプログラムが止まってしまうはずですが SoftReference を使って賢いガベージ・コレクタの力で完走させようというものです。
プログラムの実行結果は次のようになりました。
Windows 10 10.0 amd64
JDK 11.0.1
UsedMemory: 113246208 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 222298112 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 331350016 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 440401920 G1 Young Generation GC Count: 0 GCTime = 0 G1 Old Generation GC Count: 0 GCTime = 0
UsedMemory: 546493104 G1 Young Generation GC Count: 1 GCTime = 3 G1 Old Generation GC Count: 0 GCTime = 0
略
UsedMemory: 110148312 G1 Young Generation GC Count: 57 GCTime = 109 G1 Old Generation GC Count: 2 GCTime = 1808
— POLL:java.lang.ref.SoftReference@59fa1d9b —
— POLL:java.lang.ref.SoftReference@28d25987 —
略
— POLL:java.lang.ref.SoftReference@527740a2 —
— POLL:java.lang.ref.SoftReference@13a5fe33 —
UsedMemory: 219243560 G1 Young Generation GC Count: 58 GCTime = 110 G1 Old Generation GC Count: 2 GCTime = 1808
UsedMemory: 328295464 G1 Young Generation GC Count: 58 GCTime = 110 G1 Old Generation GC Count: 2 GCTime = 1808
略
UsedMemory: 6762364184 G1 Young Generation GC Count: 222 GCTime = 332 G1 Old Generation GC Count: 12 GCTime = 8964
UsedMemory: 6871416088 G1 Young Generation GC Count: 222 GCTime = 332 G1 Old Generation GC Count: 12 GCTime = 8964
プログラムが終了するまで1分8秒もかかった。
私の7年前のパソコンが壊れたのかと思ってしまった。(^_^;)
NetBeans のプロファイラでどんなかんじなのか確認してみました。
確かに OutOfMemoryError でプログラムが止まらないようにガベージ・コレクタが頑張っているようだけどGC時間長くない?
プログラムの実行結果からも気のせいか GC の回数、累積時間が長いような気がする。
SoftReference ってこんなものなのかなぁ・・・
あっ、なにかプログラムに問題があるのかもしれない。
これは WeakReference で試して比較してみるしかない。
ということで次回に続く
TAGS: Java,NetBeans |
2019年1月1日4:38 PM |
Java JavaFX NetBeans
このエントリーは、JavaFX Advent Calendar 2017 の 20 日目です。
現時点でもエントリーが無いので急遽先日遭遇した問題に解決策を教えていただいたのでそれを記事にしました。
昨日は @aoetk さんの「Bean ValidationのJavaFX対応」でした。
明日も、この記事を書いている現時点ではまだ空いてます。(^_^; きっと誰かが素敵な記事を投稿してくれると楽しみにしています。
11 月から IBM Cloud (Bluemix)ライト・アカウントが気楽に使えるようになり Watson Personality Insights を利用した人格診断プログラムを組んで楽しんでいました。
その際に Canvas に文字を描くときの位置決めに難儀したので FontMetrics を取得する方法をφ(..)メモメモ
まず、Java で FontMetrics を取得するとしたら java.awt.FontMetrics クラスを使うことができます。
Abstract Window Toolkit を使って下記のようなプログラムを組んでみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116
|
package jp.yucchi.textofcanvas; import java.awt.BorderLayout; import java.awt.Canvas; import java.awt.Color; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Frame; import java.awt.Graphics; import java.awt.GraphicsEnvironment; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.stream.Stream; import jp.yucchi.textofcanvas.TextOfCanvas.Configuration; /** * * @author Yucchi */ public class TextOfCanvas extends Frame { protected enum Configuration { WIDTH(800), HEIGHT(300), FONT_SIZE(100); int settingValue; private Configuration(int settingValue) { this.settingValue = settingValue; } public int getSettingValue() { return settingValue; } } public static void main(String[] args) { TextOfCanvas textOfCanvas = new TextOfCanvas(); // // 使用可能なフォント // Stream.of(GraphicsEnvironment.getLocalGraphicsEnvironment() // .getAvailableFontFamilyNames()) // .forEach(System.out::println); } private Frame frame; TextOfCanvas() { initGUI(); } private void initGUI() { frame = new Frame("Java AWT Text of Canvas"); frame.setSize(Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue); frame.setLayout(new BorderLayout()); frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent we) { System.exit(0); } }); TextCanvas textCanvas = new TextCanvas(); frame.add(textCanvas, BorderLayout.CENTER); frame.setLocationRelativeTo(null); frame.setVisible(true); } } class TextCanvas extends Canvas { @Override public void paint(Graphics g) { g.setColor(Color.LIGHT_GRAY); g.drawLine(0, Configuration.HEIGHT.settingValue / 2, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2); g.drawLine(Configuration.WIDTH.settingValue / 4, 0, Configuration.WIDTH.settingValue / 4, Configuration.HEIGHT.settingValue); // Font font = new java.awt.Font("あくびん", java.awt.Font.BOLD, Configuration.FONT_SIZE.settingValue); // Leading: 0.0 Font font = new java.awt.Font("SansSerif", java.awt.Font.BOLD, Configuration.FONT_SIZE.settingValue); g.setFont(font); g.setColor(Color.RED); g.drawString("Japan 日本", Configuration.WIDTH.settingValue / 4, Configuration.HEIGHT.settingValue / 2); FontMetrics fontmetrics = g.getFontMetrics(); double height = fontmetrics.getHeight(); // テキスト1行の標準の高さ Height = Ascent + Descent + Leading double width = fontmetrics.stringWidth("Japan 日本"); double ascent = fontmetrics.getAscent(); // ベースラインからの高さ double descent = fontmetrics.getDescent(); // ベースラインから下にはみ出る量 // double maxDecent = fontmetrics.getMaxDecent(); // スペルミスによる @Deprecated (^_^; double leading = fontmetrics.getLeading(); // 前の行の descent のラインと次の行の ascent のラインの間に必要な「行間」の量 System.out.println("Height: " + height); System.out.println("Width: " + width); System.out.println("Ascent: " + ascent); System.out.println("Descent: " + descent); System.out.println("Leading: " + leading); Font currentFont = g.getFont(); Font shrinkFont = currentFont.deriveFont(currentFont.getSize() * 0.3F); g.setFont(shrinkFont); g.setColor(Color.GREEN); g.drawLine(0, (int) (Configuration.HEIGHT.settingValue / 2 - ascent), Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2 - ascent)); g.drawString("Ascent: " + ascent, 0, (int) (Configuration.HEIGHT.settingValue / 2 - height + descent)); g.setColor(Color.BLUE); g.drawLine(0, (int) (Configuration.HEIGHT.settingValue / 2 + descent), Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2 + descent)); g.drawString("Descent: " + descent, 0, (int) (Configuration.HEIGHT.settingValue / 2 + descent - leading)); g.setColor(Color.magenta); g.drawLine(0, (int) (Configuration.HEIGHT.settingValue / 2 + leading + descent), Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2 + leading + descent)); g.drawString("Leading: " + leading, 0, (int) (Configuration.HEIGHT.settingValue / 2 + leading * 2 + descent + g.getFontMetrics().getAscent() - g.getFontMetrics().getDescent())); } } |
懐かしいコードですね。
83 行目で FontMetrics を Graphics コンテキストより取得してフォントの構造データをそれぞれの取得メソッドで取得しています。
原点の X, Y 座標に薄いグレーでラインをひいてます。
フォントの高さ関するデータもそれぞれラインをひいてみました。
Abstract Window Toolkit を使って FontMetrics を得ることは簡単にできることが確認できました。
それでは JavaFX 9 ではどうでしょうか?
同じようなプログラムを組んでしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
|
package jp.yucchi.textcoordinates; import com.sun.javafx.tk.FontMetrics; import com.sun.javafx.tk.Toolkit; import java.awt.GraphicsEnvironment; import java.util.stream.Stream; import javafx.application.Application; import javafx.geometry.VPos; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.canvas.Canvas; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.text.Font; import javafx.scene.text.FontSmoothingType; import javafx.scene.text.FontWeight; import javafx.scene.text.Text; import javafx.stage.Stage; /** * * @author Yucchi */ public class TextCoordinates extends Application { private enum Configuration { WIDTH(800), HEIGHT(300), FONT_SIZE(100); private final int settingValue; private Configuration(int settingValue) { this.settingValue = settingValue; } public int getSettingValue() { return settingValue; } } @Override public void start(Stage primaryStage) { // // 使用可能なフォント // Stream.of(GraphicsEnvironment.getLocalGraphicsEnvironment() // .getAvailableFontFamilyNames()) // .forEach(System.out::println); Group root = new Group(); Canvas canvas = new Canvas(Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue); GraphicsContext gc = canvas.getGraphicsContext2D(); drawShapes(gc); root.getChildren().add(canvas); Scene scene = new Scene(root, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue); primaryStage.setTitle("JavaFX Text of Canvas"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } private void drawShapes(GraphicsContext gc) { Text text = new Text("Japan 日本"); // Font font = Font.font("あくびん", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue); // Leading: 0.0 Font font = Font.font("SansSerif", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue); text.setFont(font); double width = text.getBoundsInLocal().getWidth(); double height = text.getBoundsInLocal().getHeight(); double baselineOffset = text.getBaselineOffset(); double minY = text.getLayoutBounds().getMinY(); double maxY = text.getLayoutBounds().getMaxY(); System.out.println("Width: " + width); System.out.println("Height: " + height); // Ascent + Descent System.out.println("BaselineOffset: " + baselineOffset); // Ascent System.out.println("MinY: " + minY); // -Ascent System.out.println("MaxY: " + maxY); // Descent final FontMetrics fontMetrics = Toolkit.getToolkit().getFontLoader().getFontMetrics(font); // float fontMetricsWidth = fontMetrics.computeStringWidth(text.getText()); // JavaFX9 では無くなった? // このフォントでのテキスト行のための最大行高 float fontMetricsLineHeight = fontMetrics.getLineHeight(); // 平均の小文字の最上部へのベースラインから距離 float fontMetricsXHeight = fontMetrics.getXheight(); // 平均の最大文字高さまでベースラインからの距離。 この値は常に正の値です float fontMetricsAscent = fontMetrics.getAscent(); // ベースラインから最大文字高さまでの距離。 この値は常に正の値です float fontMetricsMaxAscent = fontMetrics.getMaxAscent(); // ベースラインは、デセンダーのない文字(例えば、小文字の「a」)が置かれている仮想線です。 // フォントメトリックに関しては、この点から他のすべてのメトリックが導出されます。 この点は暗黙的にゼロとして定義されています。 int fontMetricsBaseline = fontMetrics.getBaseline(); // ベースラインから最低平均値までの距離。 ディセンダー。 この値は常に正の値です float fontMetricsDescent = fontMetrics.getDescent(); // ベースラインから絶対値の最も低いディセンダーまでの距離。 この値は常に正の値です float fontMetricsMaxDescent = fontMetrics.getMaxDescent(); // このフォントのテキスト行間のスペース量。 // これは、1行のmaxDecentと次のmaxAscentの間のスペース量です。 // この数値は、lineHeightに含まれています。 float fontMetricsLeading = fontMetrics.getLeading(); // System.out.println("FontMetricsWidth: " + fontMetricsWidth); System.out.println("FontMetricsLineHeight: " + fontMetricsLineHeight); // Ascent + Descent + Leading System.out.println("FontMetricsXHeight: " + fontMetricsXHeight); System.out.println("FontMetricsAscent: " + fontMetricsAscent); System.out.println("FontMetricsMaxAscent: " + fontMetricsMaxAscent); System.out.println("FontMetricsBaseline: " + fontMetricsBaseline); System.out.println("FontMetricsDescent: " + fontMetricsDescent); System.out.println("FontMetricsMaxDescent: " + fontMetricsMaxDescent); System.out.println("FontMetricsLeading: " + fontMetricsLeading); gc.setFontSmoothingType(FontSmoothingType.LCD); gc.setStroke(Color.LIGHTGRAY); gc.setLineWidth(1.0); gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0); gc.strokeLine(Configuration.WIDTH.settingValue / 4.0, 0.0, Configuration.WIDTH.settingValue / 4.0, Configuration.HEIGHT.settingValue); // テキスト gc.setFont(font); gc.setFill(Color.RED); gc.setTextBaseline(VPos.BASELINE); gc.fillText(text.getText(), Configuration.WIDTH.settingValue / 4.0, Configuration.HEIGHT.settingValue / 2.0); // FontMetrics データ表示用フォント gc.setFont(Font.font("SansSerif", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue * 0.3)); // gc.setFont(Font.font("あくびん", FontWeight.BOLD, Configuration.FONT_SIZE.settingValue * 0.3)); final FontMetrics shrinkFontMetrics = Toolkit.getToolkit().getFontLoader().getFontMetrics(gc.getFont()); // Ascent gc.setStroke(Color.GREEN); gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsAscent, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsAscent); gc.setFill(Color.GREEN); gc.fillText("Ascent: " + String.format("%.1f", fontMetricsAscent), 0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsAscent - fontMetricsLeading); // XHeight gc.setStroke(Color.PURPLE); gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsXHeight, Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsXHeight); gc.setFill(Color.PURPLE); gc.fillText("XHeight: " + String.format("%.1f", fontMetricsXHeight), 0.0, Configuration.HEIGHT.settingValue / 2.0 - fontMetricsXHeight - fontMetricsLeading); // Descent gc.setStroke(Color.BLUE); gc.strokeLine(0.0, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent, Configuration.WIDTH.settingValue, (int) (Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent)); gc.setFill(Color.BLUE); gc.fillText("Descent: " + String.format("%.1f", fontMetricsDescent), 0.0, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent - fontMetricsLeading); // Leading gc.setStroke(Color.MAGENTA); gc.strokeLine(0.0, (Configuration.HEIGHT.settingValue / 2.0 + fontMetricsLeading + fontMetricsDescent), Configuration.WIDTH.settingValue, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsLeading + fontMetricsDescent); gc.setFill(Color.MAGENTA); gc.fillText("Leading: " + String.format("%.1f", fontMetricsLeading), 0.0, Configuration.HEIGHT.settingValue / 2.0 + fontMetricsDescent + shrinkFontMetrics.getAscent() + shrinkFontMetrics.getLeading()); } } |
残念ながらコンパイルエラーです。
「名前のないモジュールにエクスポートされていません」ってなんのことですか?
JavaFX 9 では java.awt.FontMetrics クラスは使えないようなので com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit を使用しました。
確か、JavaFX 8 では使えていたような記憶があるんだけど・・・
そう言えば、Project Jigsaw の影響で com.sun ではじまるパッケージが使えないものがあるようです。
このプログラムで使っている com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit も JDK 内部(モジュール)にちゃんとあるのにデフォルトで使えなくしてあります。
困りました!
Windows 環境なら JavaFX 9 だったら HiDPI 対応の恩恵を享受することが可能なのに。
ちなみに JavaFX 8 だったら問題なく動きました。
さて、どうしたものか。。。
以前 Twitter で 「hoge は fuga ができないからクソッ!」もしくは女子高生を装ってヘルプをつぶやくと優秀なプログラマが解決策を提案してくれるという法則を学んだ。
私には役者の才能も無いし、小賢しいことをするのは面倒なので素直に Twitter でつぶやいたところ心優しい Java プログラマーが助けてくれました。
ありがとうございます!
早速 NetBeans に javacとjavaのオプションを設定してコンパイル、実行をしたところ無事に動きました。(^_^)
ちなみに、テキストの挿入位置の Y 座標の位置はデフォルトでは VPos.BASELINE となっています。
gc.setTextBaseline(VPos.BASELINE);
これは次のように変更することができます。
gc.setTextBaseline(VPos.TOP);
gc.setTextBaseline(VPos.CENTER);
gc.setTextBaseline(VPos.BOTTOM);
ああっ・・・ しまった。(>_<) 左のテキストの位置の修正忘れた。見なかったことにしてください。(ごめんなさい)
ついでに Leading が無いフォントの表示も見ておきます。
AWT と JavaFX では FontMetrics の扱い方に違いがあるので注意が必要ですね。
まとめ
さて、ここで JavaFX で FontMetrics を扱うために com.sun.javafx.tk.FontMetrics, com.sun.javafx.tk.Toolkit を使用します。
これを JavaFX 9 で使うためには javacとjavaのオプションに –add-exports=javafx.graphics/com.sun.javafx.tk=ALL-UNNAMED の設定が必要です。
参考
JEP 261: Module System
カプセル化を破る
モジュールシステムによって定義されたアクセス制御境界に違反し、コンパイラと仮想マシンによって強制されて、
あるモジュールが別のモジュールの一部の非通知タイプにアクセスできるようにする必要があることがあります。
これは、例えば、内部型のホワイトボックステストを可能にするため、
またはサポートされていない内部APIをそれらに依存するようになったコードに公開するために望ましいことがある。
これを行うには、コンパイル時と実行時の両方で–add-exportsオプションを使用できます。
構文は次のとおりです。
–add-exports <source-module>/<package>=<target-module>(,<target-module>)*
<source-module>と<target-module>はモジュール名で、<package>はパッケージ名です。
–add-exportsオプションは、複数回使用できますが、ソースモジュールとパッケージ名の特定の組み合わせに対して最大で1回使用できます。
各インスタンスの効果は、指定されたパッケージの修飾されたエクスポートをソースモジュールからターゲットモジュールに追加することです。
これは基本的に、モジュール宣言内のエクスポート句のコマンドライン形式、またはModule :: addExportsメソッドの無制限な形式の呼び出しです。
結果として、ターゲットモジュールがソースモジュールの名前付きパッケージ内のパブリックタイプにアクセスできるようになります。
ターゲットモジュールはソースモジュールをモジュール宣言のrequires節、Module :: addReadsメソッド、または–add-readsオプションのインスタンスです。
たとえば、jmx.wbtestモジュールに、java.managementモジュールの非エクスポートcom.sun.jmx.remote.internalパッケージのホワイトボックス・テストが含まれている場合、それが必要とするアクセスはオプションを使用して許可することができます。
–add-exports java.management/com.sun.jmx.remote.internal=jmx.wbtest
特殊なケースとして、<target-module>がALL-UNNAMEDの場合、ソースパッケージは、最初に存在するか、後で作成されるかに関係なく、名前のないすべてのモジュールにエクスポートされます。
したがって、java.managementモジュールのsun.managementパッケージへのアクセスは、オプションを介してクラスパス上のすべてのコードに与えることができます。
–add-exports java.management/sun.management=ALL-UNNAMED
–add-exportsオプションを使用すると、指定されたパッケージのパブリックタイプにアクセスできます。
コアリフレクションAPIのsetAccessibleメソッドを使用して、非公開のすべての要素にさらにアクセスしてアクセスできるようにする必要があることがあります。
これを行うには、実行時に–add-opensオプションを使用することができます。
–add-exportsオプションと同じ構文です:
–add-opens <source-module>/<package>=<target-module>(,<target-module>)*
<source-module>と<target-module>はモジュール名で、<package>はパッケージ名です。
–add-opensオプションは複数回使用できますが、ソースモジュールとパッケージ名の特定の組み合わせに対して最大で1回使用できます。
各インスタンスの効果は、名前付きパッケージの修飾されたオープンをソースモジュールからターゲットモジュールに追加することです。
これは基本的に、モジュール宣言のopens節のコマンドライン形式、またはModule :: addOpensメソッドの無制限な形式の呼び出しです。
結果として、ターゲットモジュール内のコードは、ターゲットモジュールがソースモジュールを読み取る限り、
ソースリフレクションAPIを使用して、ソースモジュールの名前付きパッケージ内のパブリックなどのすべてのタイプにアクセスできます。
オープンパッケージは、コンパイル時にエクスポートされていないパッケージと区別できないため、
–add-opensオプションはそのフェーズでは使用できません。
–add-exportsと–add-opensオプションは、細心の注意を払って使用する必要があります。
それらを使用して、ライブラリモジュールの内部API、またはJDK自体のアクセス権を取得することはできますが、自己責任で行ってください。
内部APIが変更または削除された場合、ライブラリまたはアプリケーションは失敗します。
((((;゚Д゚)))))))
I wish you a Merry Christmas.
TAGS: Java,JavaFX,NetBeans |
2017年12月20日10:24 AM |
Java JavaFX NetBeans
このエントリーは、JavaFX Advent Calendar 2017 の 6 日目です。
昨日は @planet-az さんの「簡単なミュージックプレーヤーを作ってみた」でした。
明日はこの記事を書いている現時点ではまだ空いてます。(^_^; きっと誰かが素敵な記事を投稿してくれると楽しみにしています。
今さらですが JavaFX の非同期処理を復習がてら簡単にみていきたいと思います。
では、次のようなプログラムを作ってみます。
long result = 1;
for (int i = 0; i < repeatProcessingNumber; i++) {
result += result;
初期値 1 で10回ループするプログラムです。
計算終了後は 1024 と結果を表示します。
これだけでは時間のかかる処理とはならないので
TimeUnit.MILLISECONDS.sleep(500);
とスレッドをスリープさせています。
プログラムの状態、計算中の値の表示やキャンセルなどのメッセージ表示、プログレスバーによるプログラムの進捗状態の可視化などを実装します。
思考停止状態でギナギナっと作ったプログラムは次のようなものです。
Warning! This code will make you headache.
You do not have to read it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
|
package jp.yucchi.badasynchronousprocessing4javafx; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Application; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.layout.FlowPane; import javafx.stage.Stage; /** * * @author Yucchi */ public class BadAsynchronousProcessing4JavaFX extends Application { private Label statusLabel; private Label messageLabel; private Label interimResult; private Label resultLabel; private ProgressBar progressBar; private Button startButton; private Button cancelButton; private boolean cancelFlag; @Override public void start(Stage primaryStage) { System.out.println(cancelFlag); statusLabel = new Label("STATUS"); statusLabel.setMinWidth(200); messageLabel = new Label("MESSAGE"); messageLabel.setMinWidth(200); interimResult = new Label("INTERIM RESULT"); interimResult.setMinWidth(200); resultLabel = new Label("RESULT"); resultLabel.setMinWidth(200); progressBar = new ProgressBar(0); progressBar.setMinWidth(200); startButton = new Button("START"); startButton.setMinWidth(200); cancelButton = new Button("CANCEL"); cancelButton.setMinWidth(200); cancelButton.setDisable(true); FlowPane root = new FlowPane(); root.setPadding(new Insets(10)); root.setHgap(10); root.setVgap(10); root.getChildren().addAll(statusLabel, messageLabel, interimResult, resultLabel, progressBar, startButton, cancelButton); Scene scene = new Scene(root, 220, 225); primaryStage.setTitle(this.getClass().getSimpleName()); primaryStage.setScene(scene); primaryStage.show(); primaryStage.setOnCloseRequest(we -> { }); startButton.setOnAction(ae -> { resultLabel.setText("RESULT"); executeTask(); }); cancelButton.setOnAction(ae -> { startButton.setDisable(false); cancelButton.setDisable(true); statusLabel.setText("CANCELED"); messageLabel.setText("Cancelled!"); progressBar.setProgress(0.0); interimResult.setText("INTERIM RESULT"); cancelFlag = true; }); } /** * @param args the command line arguments */ public static void main(String[] args) { launch(args); } private void executeTask() { startButton.setDisable(true); cancelButton.setDisable(false); int repeatProcessingNumber = 10; long result = 1; statusLabel.setText("RUNNING"); messageLabel.setText("Start!"); interimResult.setText(String.valueOf(result)); progressBar.setProgress(0); for (int i = 0; i < repeatProcessingNumber; i++) { if (cancelFlag) { System.out.println("This program has been canceled."); break; } try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException ex) { Logger.getLogger(BadAsynchronousProcessing4JavaFX.class.getName()).log(Level.SEVERE, null, ex); if (cancelFlag) { System.out.println("This program has been canceled."); break; } } result += result; messageLabel.setText(String.format("%d/%d", i + 1, repeatProcessingNumber)); interimResult.setText(String.valueOf(result)); progressBar.setProgress(i); } if (!cancelFlag) { statusLabel.setText("SUCCEEDED"); resultLabel.setText(String.valueOf(result)); } else { cancelFlag = false; } startButton.setDisable(false); cancelButton.setDisable(true); } } |
それではこのプログラムを実行してみましょう。
「START」 ボタンを押した瞬間プロブラムがフリーズしたようになりました。
そして暫くすると結果が表示されました。
これは駄目ですね。(>_<。)
プログラムがビジー状態で応答しなくなってしまっては使い物になりません。
進捗状態がわからないのは致命的です。
そして唐突に計算が終了してプログラムが完了する。
そう言えば遠い昔にこのような状態のプログラムが当たり前のように存在していたような気がする。
今の時代こんなのは絶対に許されません。
何がいけないのか?
それは時間のかかる処理を JavaFX アプリケーションスレッド上で行っているからです。
NetBeans のプロファイラで確認してみます。
JavaFX Application Thread タイムラインを見てください。
START ボタンを押した瞬間に時間のかかる処理(スレッドスリープ)が走ってます。(緑色から紫色に変わっているところ)
これでは UI の更新処理などはできませんね。
時間のかかる処理をしている間プログラムの応答は無くなり、ビジー状態のようになってしまいます。
つまり、時間のかかる処理用にスレッドをもう一つ起こせばこの問題は解決するはずです。
この問題を解決するのに最適な方法は javafx.concurrentパッケージを使用することです。
javafx.concurrent パッケージは、Worker インタフェースと、2つの具体的な実装である Task および Service クラスで構成されています。
Worker インタフェースは、バックグラウンドスレッド上の Worker オブジェクトの状態、進捗を監視可能なプロパティで公開しています。
状態は ReadOnlyObjectProperty<Worker.State> stateProperty で確認できます。
次のように Enum Worker.State で定義されています。
- READY Workerがまだ実行されておらず、実行の準備ができているか、またはWorkerが再初期化されたことを示します。
- SCHEDULED Workerの実行がスケジュールされているが、現在は実行中ではないことを示します。
- RUNNING このWorkerが実行中であることを示します。
- SUCCEEDED このWorkerが正常に完了しており、valueプロパティから読み取る準備ができている有効な結果があることを示します。
- CANCELLED このWorkerがWorker.cancel()メソッドによって取り消されたことを示します。
- FAILED 通常は予期しない条件が発生したことによって、このWorkerが失敗したことを示します。
これらは JavaFX アプリケーションスレッドから使用できます。
Worker オブジェクトによって実行される処理の進捗は、totalWork、workDone、progress など、3つの異なるプロパティを通じて取得できます。
Worker オブジェクトの状態が変化するときに発生するイベントは WorkerStateEventクラスによって指定されます。
これは Task クラスと Service クラスの両方に EventTarget インタフェースが実装され状態イベントのリスニングがサポートされているからです。
今回は再利用の必要が無い Worker オブジェクトを生成するので Task クラスを利用します。
上記のように JavaFX の Task クラスは FutureTask クラスの完全に監視可能にした実装となっています。
この Task クラスを拡張しバックグラウンドスレッドで実行するロジックを実装します。
懐かしのSwingWorker クラスを思い出させてくれますね。(もう、ほとんど覚えてないけどね)
SwingWorker では doInBackground() メソッドにてバックグラウンドスレッドロジックを呼び出します。
JavaFX の場合は call() メソッドです。
doInBackground() メソッド、call() メソッドともにどちらも抽象メソッドなのでオーバーライドして使います。
call() メソッドは Task クラスが実行されるときに呼び出され、call() メソッドに実装されたバックグラウンドスレッドロジックを実行します。
つまり、仕組み的には SwingWorker とほとんど変わりはないので馴染みやすいかも知れません。
さて、JavaFX のために用意されたのにこれだけではあまり意味がありません。
この Task クラスの実行にあたり updateProgress() メソッド、updateMessage() メソッド、updateValue() メソッド、updateTitle() メソッドを呼び出すことができます。
これらを使用するとプログラムの状態、進捗状況、実行中の処理の結果の一部を返すなどを可視化できます。
ちょっと updateValue() メソッドの実装をみてみましょう。
|
protected void updateValue(V value) { if (isFxApplicationThread()) { this.value.set(value); } else { // As with the workDone, it might be that the background thread // will update this value quite frequently, and we need // to throttle the updates so as not to completely clobber // the event dispatching system. if (valueUpdate.getAndSet(value) == null) { runLater(() -> Task.this.value.set(valueUpdate.getAndSet(null))); } } } |
呼び出しが JavaFX アプリケーションスレッドかそうでないかで分岐処理をおこなっています。
JavaFX アプリケーションスレッドからだったらそのまま値を更新します。
|
void runLater(Runnable r) { Platform.runLater(r); } |
でなければ runLater()メソッドで javafx.application.Platform クラスの runLater(Runnable runnable) メソッドを呼び出して値を更新します。
何故なら、JavaFX Sceneグラフは、スレッドセーフではなく、JavaFXアプリケーションスレッドのみアクセスおよび変更できます。
これは Swing では javax.swing.SwingUtilities クラスの invokeLater(Runnable doRun) メソッドと同じようなものです。
JDK1.3 からは java.awt.EventQueue.invokeLater() を呼び出すように作られています。
このinvokeLater(Runnable doRun) メソッドはどのスレッドからも呼び出しが可能となっています。
javafx.application.Platform クラスの runLater(Runnable runnable) メソッドも任意のスレッドから呼び出し可能となっています。
updateProgress() メソッド、updateMessage() メソッド、updateValue() メソッド、updateTitle() メソッドの API ドキュメントには「このメソッドは、任意のスレッドから安全に呼び出すことができます。」と記載されています。
ちなみに updateValue() メソッドは JavaFX 8 からなので上記のようにラムダ式を使い綺麗に書かれています。
古くからある updateMessage() メソッドは次のように Java SE 7 の時代のコードのままとなっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
protected void updateMessage(String message) { if (isFxApplicationThread()) { this.message.set(message); } else { // As with the workDone, it might be that the background thread // will update this message quite frequently, and we need // to throttle the updates so as not to completely clobber // the event dispatching system. if (messageUpdate.getAndSet(message) == null) { runLater(new Runnable() { @Override public void run() { final String message = messageUpdate.getAndSet(null); Task.this.message.set(message); } }); } } } |
このようなのを目にすると Java は開発者のために進化し続けている言語なんだなぁって感慨深いものがありますね。
SwingWorker のように publish() メソッド、process() メソッドなんてのは JavaFX には必要なくなってます。
このようにありがたい機能を標準で実装されている Task クラスですがさらに便利なプロパティを持っています。
これから先程のプログラムを改善するのにいくつかの Task クラスのプロパティを使用します。
それらプロパティは JavaFX のバインドという機能により快適に使用することが可能です。
JSR 295 Beans Binding と同じようなものです。
実際にどのように利用するかというと Task クラスの実行状態(Worker オブジェクト)のプロパティを利用して「START」ボタン、「CANCEL」ボタンの活性化、非活性化をおこないます。
また、バックグラウンドスレッド(Worker オブジェクト)の状態、計算結果の一部を返す処理、プログレスバーによるプログラムの進捗状態の表示もおこないます。
これらの対応を施したプログラムは次のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
|
package jp.yucchi.asynchronousprocessing4javafx; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.logging.Level; import java.util.logging.Logger; import javafx.application.Application; import javafx.concurrent.Task; import static javafx.concurrent.Worker.State.RUNNING; import javafx.geometry.Insets; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.control.Label; import javafx.scene.control.ProgressBar; import javafx.scene.layout.FlowPane; import javafx.stage.Stage; /** * * @author Yucchi */ public class AsynchronousProcessing4JavaFX extends Application { private final ExecutorService executorService = Executors.newSingleThreadExecutor(); private Label statusLabel; private Label messageLabel; private Label interimResult; private Label resultLabel; private ProgressBar progressBar; private Button startButton; private Button cancelButton; @Override public void start(Stage primaryStage) { statusLabel = new Label("STATUS"); statusLabel.setMinWidth(200); messageLabel = new Label("MESSAGE"); messageLabel.setMinWidth(200); interimResult = new Label("INTERIM RESULT"); interimResult.setMinWidth(200); resultLabel = new Label("RESULT"); resultLabel.setMinWidth(200); progressBar = new ProgressBar(0); progressBar.setMinWidth(200); startButton = new Button("START"); startButton.setMinWidth(200); cancelButton = new Button("CANCEL"); cancelButton.setMinWidth(200); cancelButton.setDisable(true); FlowPane root = new FlowPane(); root.setPadding(new Insets(10)); root.setHgap(10); root.setVgap(10); root.getChildren().addAll(statusLabel, messageLabel, interimResult, resultLabel, progressBar, startButton, cancelButton); Scene scene = new Scene(root, 220, 225); primaryStage.setTitle(this.getClass().getSimpleName()); primaryStage.setScene(scene); primaryStage.show(); primaryStage.setOnCloseRequest(we -> { executorService.shutdownNow(); }); startButton.setOnAction(ae -> { resultLabel.setText("RESULT"); executeBackgroundTask(); }); } public static void main(String[] args) { launch(args); } private void executeBackgroundTask() { Task<Long> task = new Task<>() { @Override public Long call() { int repeatProcessingNumber = 10; long result = 1; updateMessage("Start!"); updateValue(result); updateProgress(0, repeatProcessingNumber); for (int i = 0; i < repeatProcessingNumber; i++) { if (isCancelled()) { System.out.println("This program has been canceled."); break; } try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException ex) { Logger.getLogger(AsynchronousProcessing4JavaFX.class.getName()).log(Level.SEVERE, null, ex); if (isCancelled()) { System.out.println("Canceled by InterruptedException."); break; } } result += result; updateMessage(String.format("%d/%d", i + 1, repeatProcessingNumber));; updateValue(result); updateProgress(i + 1, repeatProcessingNumber); } return result; } // Workerオブジェクトの状態が変化したときのコールバックメソッド(5種類) @Override protected void scheduled() { super.succeeded(); updateMessage("scheduled!"); System.out.println("Call scheduled()"); } @Override protected void running() { super.running(); updateMessage("running!"); System.out.println("Call running()"); } @Override protected void succeeded() { super.succeeded(); updateMessage("Succeeded!"); System.out.println("Call succeeded()"); resultLabel.setText("RESULT: " + getValue().toString()); } @Override protected void cancelled() { super.cancelled(); updateMessage("Cancelled!"); System.out.println("Call cancelled()"); progressBar.progressProperty().unbind(); progressBar.setProgress(0.0); interimResult.textProperty().unbind(); interimResult.setText("INTERIM RESULT"); } @Override protected void failed() { super.failed(); updateMessage("Failed!"); System.out.println("Call failed()"); } }; // WorkerStateEven を使ってWorkerオブジェクトの状態を監視 task.setOnScheduled(wse -> System.out.println("setOnScheduled")); task.setOnRunning(wse -> System.out.println("setOnRunning")); task.setOnSucceeded(wse -> System.out.println("setOnSucceeded")); task.setOnCancelled(wse -> System.out.println("setOnCancelled")); task.setOnFailed(wse -> System.out.println("setOnFailed")); // textProperty と Workerオブジェクトの stateProperty をバインド statusLabel.textProperty().bind(task.stateProperty().asString()); // textProperty と Workerオブジェクトの messageProperty をバインド messageLabel.textProperty().bind(task.messageProperty()); // textProperty と Workerオブジェクトの valueProperty をバインド interimResult.textProperty().bind(task.valueProperty().asString()); // progressProperty と Workerオブジェクトの progressProperty をバインド progressBar.progressProperty().bind(task.progressProperty()); // disableProperty と Workerオブジェクトの stateProperty をバインド startButton.disableProperty().bind(task.stateProperty().isEqualTo(RUNNING)); cancelButton.disableProperty().bind(task.stateProperty().isNotEqualTo(RUNNING)); cancelButton.setOnAction(ae -> { task.cancel(); }); // ExecutorService を利用してバックグラウンドスレッドを開始 executorService.submit(task); } } |
このプログラムの実行結果を見てみましょう。
ついでに NetBeans のプロファイラでバックグラウンドスレッドが生成されているのを確認しましょう。
pool-2-thread-1 がバックグラウンドスレッドとして起動されています。
これで JavaFX アプリケーションスレッドはイベント処理が可能となります。
画面が固まることなくバックグラウンドスレッドの状態、計算結果の一部(途中経過)、プログレスバーも表示されています。
また、「START」ボタン、「CANCEL」ボタンの活性化、非活性化もちゃんとできています。
まず、Task クラスをみていきましょう。
今回はcall() メソッドを実行するために java.util.concurrent の ExecutorService インタフェースを利用しました。
184行目です。
executorService.submit(task);
タスクをパラメータとして指定したスレッドの開始としては次の方法もあります。
Thread th = new Thread(task);
th.setDaemon(true);
th.start();
call() メソッドに時間のかかる処理を記述します。バックグラウンドスレッドロジックです。
call() メソッドが呼ばれたら初期値をセットします。
updateMessage() メソッド、updateValue() メソッド、updateProgress() メソッドはプロパティバインドを利用します。
バックグラウンドスレッドが開始されたらキャンセル操作がリクエストされたかも監視しなければいけません。
キャンセルは java.util.concurrent.FutureTask クラスの isCancelled() メソッドを使います。
今回、スレッドをスリープさせているので InterruptedException をスローする場合があります。
また InterruptedException は Task のキャンセルの結果として発生する場合があるため、確実に InterruptedException を処理し、キャンセル状態を調べる必要があります。
それに対応するために 102 行目の catch 節のところにも isCancelled() メソッドを使用しています。
このようにバックグラウンドスレッドロジック内にブロッキング・コールがある場合は注意が必要となります。
バックグラウンドスレッドでの処理の結果の一部を返すために 110 行目に updateValue() メソッドを使います。
同様に 109 行目で messageLabel 、111 行目で progressBar の更新も行っています。
これらの更新は JavaFX のバインドという便利な機能を使います。
Task クラスのプロパティと Label のテキストプロパティ、ProgressBar の プログレスプロパティとバインドします。
168 行目から次のようにバインドしています。
// textProperty と Workerオブジェクトの stateProperty をバインド
statusLabel.textProperty().bind(task.stateProperty().asString());
// textProperty と Workerオブジェクトの messageProperty をバインド
messageLabel.textProperty().bind(task.messageProperty());
// textProperty と Workerオブジェクトの valueProperty をバインド
interimResult.textProperty().bind(task.valueProperty().asString());
// progressProperty と Workerオブジェクトの progressProperty をバインド
progressBar.progressProperty().bind(task.progressProperty());
これで自動的に Task クラス(Workerオブジェクト)のプロパティ updateXXX() メソッドで更新されたら Label 、ProgressBar も更新されます。
もし、キャンセル操作が実行され、プログラムの実行結果の一部が表示されたままになるのが嫌なら
141 行目の Worker オブジェクトの状態が変化したときのコールバックメソッドである cancelled() メソッドの処理でいったん unbind() メソッドによりバインドを解除して初期値を設定し直すといいでしょう。
@Override
protected void cancelled() {
super.cancelled();
updateMessage(“Cancelled!”);
System.out.println(“Call cancelled()”);
progressBar.progressProperty().unbind();
progressBar.setProgress(0.0);
interimResult.textProperty().unbind();
interimResult.setText(“INTERIM RESULT”);
}
このようなコールバックメソッドはこの他にもあり、全部で5種類あります。
必要に応じて便利に使えます。
118 行目から 158 行目を参照ください。
バックグラウンドタスクが無事に終了したら 134 行目の succeeded() メソッドがコールバックされます。
@Override
protected void succeeded() {
super.succeeded();
updateMessage(“Succeeded!”);
System.out.println(“Call succeeded()”);
resultLabel.setText(“RESULT: ” + getValue().toString());
}
ここで Task クラスに戻り値がある場合それを取得します。
戻り値の型はジェネリクスの型パラーメターで指定されたものです。
今回のプログラムでは Long となっています。
必然的に Call() メソッドの戻り値も Long です。
計算結果は 138 行目で javafx.concurrent.Task クラスの getValue() メソッドで取得しています。
resultLabel.setText(“RESULT: ” + getValue().toString());
そして、計算結果を Label のテキストとしてセットしています。
これらのコールバックメソッドは JavaFX アプリケションスレッド上で実行されるので RuntimeException も出ません。
コールバックメソッドを利用する以外にも WorkerStateEvent を使ってイベント処理として扱うこともできます。
160 行目から 165 行目のようにイベントハンドラを登録して使用します。
// WorkerStateEven を使ってWorkerオブジェクトの状態を監視
task.setOnScheduled(wse -> System.out.println(“setOnScheduled”));
task.setOnRunning(wse -> System.out.println(“setOnRunning”));
task.setOnSucceeded(wse -> System.out.println(“setOnSucceeded”));
task.setOnCancelled(wse -> System.out.println(“setOnCancelled”));
task.setOnFailed(wse -> System.out.println(“setOnFailed”));
では、このプログラムを動かしたときの Worker オブジェクトの state プロパティ がどのように変化するか確認します。
プログラムが完する場合次のように出力されます。
setOnScheduled
Call scheduled()
setOnRunning
Call running()
setOnSucceeded
Call succeeded()
この出力結果から
SCHEDULED
RUNNING
SUCCEEDED
と遷移しているのが解ります。
プログラムを途中でキャンセルしてみる場合も確認します。
setOnScheduled
Call scheduled()
setOnRunning
Call running()
setOnCancelled
Call cancelled()
Canceled by InterruptedException.
予想通りの結果ですね。
SCHEDULED
RUNNING
CANCELLED
上記のように遷移しています。これらの動作は想像通りでした。
コールバックメソッドより WorkerStateEvent を使ってイベントハンドラを登録したほうが早い結果となっています。
だからといってメリット、デメリットがあるかどうかは私には解りません。
さて、最後に、「START」ボタン、「CANCEL」ボタンの活性化、非活性化をみてみましょう。
175 行目から 177 行目です。
// disableProperty と Workerオブジェクトの stateProperty をバインド
startButton.disableProperty().bind(task.stateProperty().isEqualTo(RUNNING));
cancelButton.disableProperty().bind(task.stateProperty().isNotEqualTo(RUNNING));
Button の disableProperty と Workerオブジェクトの stateProperty をバインドしているだけです。
startButton は Workerオブジェクトの stateProperty が RUNNING なら非活性化となります。
cancelButton は Workerオブジェクトの stateProperty が RUNNING でなければ非活性化となります。
つまり、Workerオブジェクトの stateProperty が RUNNING の場合だけ活性化します。
これでいちいち操作を行うたびに Button を活性化、非活性化処理を記述しなくてすみます。
このように JavaFX には非同期処理を簡単に扱うことができるようになっています。
SwingWorker を使って非同期処理プログラムを組んでいるのなら JavaFX の javafx.concurrentパッケージを使ってみて時代の流れを感じ取ってみてはいかがでしょうか。
お終い!
I wish you a Merry Christmas.
TAGS: Java,JavaFX,NetBeans |
2017年12月6日2:28 AM |
« 古い記事