torutkのブログ

ソフトウェア・エンジニアのブログ

Java読書会BOF「Javaパフォーマンス」を読む会(第5回)を開催して #javareading

昨日は、Java読書会BOF主催の「Javaパフォーマンス」を読む会(第5回)を開催しました。
http://www.javareading.com/bof/

今日は内容に関してのメモです。

弱い参照(WeakReference)

オブザーバーパターンを使って通知先リスト(オブザーバーのリスト)を管理する際に、登録したオブザーバーが不要になってもリストから削除する操作をしないため、オブザーバーへの強い参照が通知先リストに残り続けてメモリリークの原因になるという話題を振って、WeakReferenceを使えば回避できるのでは?と話題を振りましたが、即効「オブザーバーへの参照がオブザーバブルの通知先リストしかないのですぐGC対象となって死亡」と是正されました。

※ ここで、オブザーバーを無名内部クラスで実装すると、この無名内部クラスのインスタンスへの強参照を誰も保持しないのですぐGCされてしまいます。ラムダ式の場合、ラムダ式が何らかの変数をキャプチャしていると同様GCされますが、キャプチャしない場合はGCされないようです(変数のキャプチャがあるとインスタンスが生成されるようです)。

リスナーリストを弱参照で保持する

public class SampleModel {
    List<WeakReference<Consumer<String>>> listeners = new ArrayList<>();
    
    void addListener(Consumer<String> listener) {
        WeakReference<Consumer<String>> weakRef = new WeakReference<>(listener);
        listeners.add(weakRef);
    }

    :
}

リスナーにラムダ式を登録(キャプチャあり)

    model.addListener(s -> System.out.println(message + s));

このラムダ式(で生成されるであろうインスタンス)は、modelからのみ参照されるので、それが弱い参照(WeakReference)だとGCされてしまうという状況です。
そのため、モデルから通知する段階で、WeakReferenceのgetがnullを返し、NullPointerExceptionが発生します。

この問題について調べていたところ、次の掲示板サイトに解決のアイデアが掲載されていました。
http://java.javawebdevelop.com/134_22796301/

ラムダ式インスタンスと、その所有インスタンスを対で管理するHolderを作成し、所有インスタンスをWeakReferenceとして保持するというものでした。

その対で管理するクラスを次に引用します。

class Holder<T> extends WeakReference<T> {
    private final Consumer<T> action;
    Holder(Consumer<T> action, T target) {
        super(target);
        this.action=action;
    }
    public void performAction() {
        T t=get();
        if(t!=null) action.accept(t);
        else System.out.println("target collected");
    }
}

このアプローチならできなくはないかもしれませんが・・・
ちょっと無理やり感があります。

32ビットOS上でのJVMは4GBフルには使えない

この本では、32bitJVMについて、64ビットOS上での制約を記載している節があります。
Windows OS 32ビット版の場合、通常のプロセスは4GBの仮想メモリ空間のうち2GBがユーザー空間として利用可能で、残り2GBはカーネル空間となります。つまり、JVMのコード、ヒープ、スタック、その他はすべて2GBに収まる必要があります。この状況ではJavaのヒープとしては1GB少々が最大となります。

なお、Linux OS 32ビット版だとプロセスの仮想メモリ空間のうちユーザー空間は3GB、カーネル空間が1GBとなっています。

なお、Windows OS 32ビット版では、3GBスイッチという設定で、Linuxと同様ユーザー空間を3GBに拡張し、カーネル空間を1GBにする方法があります。これはWindows OSに対する設定(bcdedit)と、各プロセスの実行ファイルのヘッダーにLARGE_ADDRESS_AWARE設定の両方が必要になります。
LARGE_ADDRESS_AWAREはリンク時にオプションを指定して設定するものですが,
java.exe(javaw.exe)は設定されていません。ただし、後からバイナリエディタ等でjava.exe(javaw.exe)のヘッダを修正することは可能です。

Windows OS 64ビットOS上で32bitアプリケーションを実行するときは、デフォルトではユーザー空間は2GBですが、上述のLARGE_ADDRESS_AWARE設定をした実行ファイルから動くプロセスは、4GBをフルに利用できます。

NMT(Native Memory Tracking)

Java SE 8からネイティブメモリの使用状況(割り当て)を見ることができるようになりました。
https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/tooldescr007.html

ラージページ

WindowsLinuxのメモリページ管理単位はデフォルトでは4KBです。
このページの大きさは、マシン起動時に固定ではなく、プロセス単位で設定できるそうです。これは知りませんでした。

また、最近のLinuxではtransparent huge pageが搭載されており、この場合Javaで明示的にオプションを指定しなくてもラージページが使われます。
RHEL 6(CentOS 6)ではデフォルトで有効(always)になっています。

ラージページは性能向上が大きいとのことなので、知っておくとよさそうです。

スレッドのスタックサイズ

Windows 32bitではスレッドごとに320KBがデフォルトです。スレッドが30個あれば10MBを喰ってしまいます。書籍によると一般的には32bitでは128KBあればいいよということなので、ここも指定しておくとよさそうです。