Finalizerの動作

こないだfinalizerでハマったので記録として残しときます。

javaにはfinalizerと呼ばれる仕組みがあり、Object#finalize()をオーバーライドしたクラスのインスタンスガベージコレクションされる際、finalize()メソッドが呼び出される、と言う四組です。

これをつかうことでリソース後始末をしたりすることができます。
ただし、finalize()メソッドは呼び出されないこともあり、確実なリソースの回収手段ではない、ということに注意が必要です。

でもまぁ、nativeなリソース(JNIとかJavaの外でつかわれるリソースとか)の管理にはつかわれたりしていたりします。

で、自分が作ったのはnativeの正規表現ライブラリを呼び出すやつでした*1
いちいちJNIでスタブメソッドを書きたくなかったのでJNA(Java Native Access: https://jna.dev.java.net/)をつかって楽をすることにしました。

で、出来上がったのかこれ > OneDrive

で、こんな風につかいます。

public class RegexTest {
    public static void main(String[] args) {
        
        RegexT regex = new RegexT();

        int result =
            PosixRegex.INSTANCE.regcomp(regex, "[0-9]+([a-z]+)[0-9]+", 1);

        if (result != 0)
            throw new RuntimeException("regex compile failed.");

        RegMatchT[] matches = (RegMatchT[])new RegMatchT().toArray(2);
        result =
            PosixRegex.INSTANCE.regexec(regex,
                                        "abcd0123456jfdaskl3493210rewio",
                                        matches.length, matches, 0);
        if (result != 0)
            System.out.println("not matched.");
        else {
            System.out.println("matched.");
            System.out.println("all match position: " +
                               matches[0].rm_so + " to " +
                               matches[0].rm_eo);
            System.out.println("group operator match opsition: " +
                               matches[1].rm_so + " to " +
                               matches[1].rm_eo);
        }

        //PosixRegex.INSTANCE.regfree(reg);
    }
}

動作結果。

cd /home/alfeim/source/jde/
/usr/lib/jvm/java-6-sun/bin/java -classpath /home/alfeim/source/jde:/home/alfeim/java/lib/jna/linux-i386.jar:/home/alfeim/java/lib/jna/jna.jar RegexTest

matched.
all match position: 4 to 25
group operator match opsition: 11 to 18

Process RegexTest finished


これだけなら、めでたしめでたし、なんですが、実は問題があります。

public class RegexTest {
    public static void main(String[] args) {
        
        for (int i = 0; i < 100; ++i) {

            RegexT regex = new RegexT();

            int result =
                PosixRegex.INSTANCE.regcomp(regex, "[0-9]+([a-z]+)[0-9]+", 1);

            if (result != 0)
                throw new RuntimeException("regex compile failed.");

            RegMatchT[] matches = (RegMatchT[])new RegMatchT().toArray(2);
            result =
                PosixRegex.INSTANCE.regexec(regex,
                                            "abcd0123456jfdaskl3493210rewio",
                                            matches.length, matches, 0);
            if (result != 0)
                System.out.println("not matched.");
            else {
                System.out.println("matched.");
                System.out.println("all match position: " +
                                   matches[0].rm_so + " to " +
                                   matches[0].rm_eo);
                System.out.println("group operator match opsition: " +
                                   matches[1].rm_so + " to " +
                                   matches[1].rm_eo);
            }

            //PosixRegex.INSTANCE.regfree(reg);
        }
    }
}

ループの中に入れて実行してみます。

cd /home/alfeim/source/jde/
/usr/lib/jvm/java-6-sun/bin/java -classpath /home/alfeim/source/jde:/home/alfeim/java/lib/jna/linux-i386.jar:/home/alfeim/java/lib/jna/jna.jar RegexTest

matched.
all match position: 4 to 25
group operator match opsition: 11 to 18
matched.

...snip...

all match position: 4 to 25
group operator match opsition: 11 to 18
matched.
all match position: 4 to 25
group operator match opsition: 11 to 18
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGSEGV (0xb) at pc=0xb4e04ee5, pid=5445, tid=3040222064
#
# JRE version: 6.0_15-b03
# Java VM: Java HotSpot(TM) Client VM (14.1-b02 mixed mode, sharing linux-x86 )
# Problematic frame:
# C  [jna1250405488460372990.tmp+0x4ee5]  Java_com_sun_jna_Pointer__1setPointer+0x21
#
matched.
# An error report file with more information is saved as:
# /home/alfeim/source/jde/hs_err_pid5445.log
all match position: 4 to 25
group operator match opsition: 11 to 18
matched.
all match position: 4 to 25
group operator match opsition: 11 to 18
matched.

... snip ...

all match position: 4 to 25
group operator match opsition: 11 to 18
#
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
# The crash happened outside the Java Virtual Machine in native code.
# See problematic frame for where to report the bug.
#

Process RegexTest aborted (core dumped)

JVMごと落ちてしまいました。

原因はクラスRegexTのfinalize()メソッドにあります。

    @Override
    protected void finalize() {
    PosixRegex.INSTANCE.regfree(this);
    }

RegexTのインスタンスがファイナライズされるときにregfree()を呼び出して正規表現処理につかったリソースを開放しているのですが、何がまずかったのでしょうか?

JNAではnativeの関数を呼び出すときに、引数として特定のJavaオブジェクトを渡し、それをメモリ上にC互換のデータとして展開してから関数に渡しています。
そして、その展開先のメモリを管理しているのがcom.sun.jna.Memoryです。このクラスでは、要求されたときにmallocを呼び出してメモリを確保し、finalize()でそのメモリを開放しています。

そしてCの構造体のJNA版であるところのcom.sun.jna.Structureのインスタンスが自己のシリアライズ用としてメンバにMemoryのインスタンスを持っているのです。

JNAでの関数呼び出しのシーケンスは次のようになります。

1) 引数となるJavaオブジェクトをMemoryなどに変換
2) 変換したMemoryが持っているメモリのポインタを引数として関数を呼び出す
3) Memory上のデータをJavaオブジェクトに書き戻す*2

といった動作をしています。

で、ここで問題になったのが 1) の部分です。
RegexT#finalize()時点ではStructureが持っているMemoryのインスタンスは、ファイナライズ処理されているRegexTのインスタンスから到達可能なので、まだファイナライズされてないだろう、とおもってたのですが、実はそれが間違いでした*3

ファイナライズ自体の動作順序は不定だそうで、さらにガベージコレクションの対象となるのは、ユーザコードから到達不可能になったものすべてがなるようです*4



実際に循環構造をもつオブジェクトにファイナライズ処理をつけてみて動きをみてみます。

class CircularRef {

    static class Circular {
        public int id;
        public Circular next;

        public Circular(int id) {
            this.id = id;
        }

        @Override
        protected void finalize() {
            synchronized(System.out) {
                System.out.println("finalize: " + id);
            }
        }
    }

    public static void main(String[] args) {

        for (int i = 0; i < 100; i += 2) {

            Circular head = new Circular(i + 1);
            Circular next = new Circular(i + 2);
            head.next = next;
            next.next = head;

            System.gc();
            System.runFinalization();
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
cd /home/alfeim/source/jde/
/usr/lib/jvm/java-6-sun/bin/java -classpath /home/alfeim/source/jde:/home/alfeim/java/lib/jna/linux-i386.jar:/home/alfeim/java/lib/jna/jna.jar CircularRef

finalize: 2
finalize: 1
finalize: 4
finalize: 3
... snip ...
finalize: 96
finalize: 95
finalize: 98
finalize: 97

Process CircularRef finished

ということでちゃんと循環していても回収されてファイナライズされました。

つぎに通常のプログラムコードから見えなくなったオブジェクトのファイナライズが順不定で呼び出されることを確認しておきます。

class FinalizeTest {

    public static boolean isChildFinalized[] = null;

    static class Finalizable {
        int id;
        Finalizable next;
    
        public Finalizable(int id, Finalizable next) {
            this.id = id;
            this.next = next;
        }
    
        @Override
        protected void finalize() {

            synchronized (System.out) {
                if (next == null)
                    isChildFinalized[id] = true;
            }
        }
    }

    public static void main(String[] args) {

        FinalizeTest.isChildFinalized = new boolean[1000];
    
        Finalizable f = null;
        for (int i = 0; i < 1000; ++i) {
            FinalizeTest.isChildFinalized[i] = false;
            new Finalizable(i, new Finalizable(i, null));
            System.gc();
        }
        System.runFinalization();

        for (int i = 0; i < 1000; ++i) {
            if (FinalizeTest.isChildFinalized[i])
                System.out.println("child finalize when parent first at id = " + i);
        }
    }
}

結果

cd /home/alfeim/source/jde/
/usr/lib/jvm/java-6-sun/bin/java -classpath /home/alfeim/source/jde:/home/alfeim/java/lib/jna/linux-i386.jar:/home/alfeim/java/lib/jna/jna.jar FinalizeTest

child finalize when parent first at id = 0
child finalize when parent first at id = 1
child finalize when parent first at id = 2
child finalize when parent first at id = 3
child finalize when parent first at id = 4
child finalize when parent first at id = 5
child finalize when parent first at id = 6
...snip...
child finalize when parent first at id = 994
child finalize when parent first at id = 995
child finalize when parent first at id = 996
child finalize when parent first at id = 997
child finalize when parent first at id = 998
child finalize when parent first at id = 999

Process FinalizeTest finished

というようにガベージコレクションの対象内になっているオブジェクト同士でのファイナライズ順序も不定です。

これらの事実より RegexTのインスタンスのfinalize()が呼び出されたとき、"運良く" 親クラスStructureのMemoryインスタンスがfinalize()されていない場合はちゃんと動きますが、"運悪く" さきにMemoryインスタンスのfinalize()が呼び出された場合、さされているメモリ位置はすでにfree()されてしまった場所なのでAccess violationでJVMごと落っこちることになるのでした。

なので複合的なリソースをfinalize()で開放したい場合は一つのfinalize()でやるか、nativeなリソースだとnative側に押しやってjava側ではハンドルだけで管理するとかしないと後々面倒にことになるようです。

*1:Javaにある正規表現をつかえない理由があったんです

*2:設定によっては自動的には行われないようにすることもできます

*3:Java SE Specifications の12.6.2 で書いてあります

*4:参照が循環しているリンクドリスト等があることを考えれば当然でした。コード書いてる最中は気がつきませんでしたが