astamuse Lab

astamuse Labとは、アスタミューゼのエンジニアとデザイナーのブログです。アスタミューゼの事業・サービスを支えている知識と舞台裏の今を発信しています。

A/Bテストを繰り返してわかったこと

f:id:astamuse:20180808113111j:plain デザイン部でフロントエンドエンジニアをしているkitoです。 今回は、A/Bテストについて書きたいと思います。
A/Bテストとは、例えば、AパターンとBパターンでそれぞれ色の違うボタンを用意し、どちらか一方のボタンを50%の確率でWebサイトに表示させることで、どちらの色のボタンがユーザーによりクリックされたか、よりコンバージョンに繋がったかを調べることができるテストです。

A/Bテストは多くのWebサイトで行われています。あなたがNetflixを訪れたとき、動画のサムネイル画像が前回の見たときと違っていることに気がつくことがあると思います。それらは進行中のA/Bテストか、もしくはテストの結果として表示されている画像なのです。 また、最近発売された『2万回のA/Bテストからわかった 支持されるWebデザイン事例集』という本には、日本の有名サイトがいかなる課題をもとにA/Bテストを実施して、どのようにサイトを改善してきたのか多数の事例が掲載されています。A/Bテストの効果が一読してわかるのでお薦めします。

弊社でも、自社で運営しているサイトで、NetflixほどではないですがA/Bテストや多変量テストを繰り返し実施し、サイト改善に日々努めています。

A/Bテストの利点と問題点

A/Bテストの利点は、サイトを構成するパーツを改善していくうえで、どちらのパーツがより良いのかを人間の主観で判断するのではなく、実際のユーザの行動データからエビデンスが得られる点です。有無を言わさぬデータなので、サイト改善サイクルのなかで主要な指標になっています。 弊社が運営している転職ナビで、A/Bテストを用いたサイト改善施策が何度も行われた結果、ボタンやフォーム、文言などのパーツが幾度となく修正されてCVRの向上に貢献してきました。

ただ、私がこの改善プロセスを通じて感じたのは、A/Bテストの効果がテストを繰り返すほど低下していくことでした。

A/Bテストの効果が低下していくと、AタイプとBタイプの結果に誤差を超えるほどの差異がでなくなり、より母数を得ようとしてテストが長期化していく傾向にありました。すると改善サイクルが遅延するようになり、他の施策を圧迫することさえありました。しかも、各パーツの改善の積み重ねが、サイト全体の改善を必ずしも約束するわけではありませんでした。どういうことでしょうか?

あえて極端な例えをしますが、A/Bテストを始める前のサイト全体のCVRが2.0%であったとしましょう、そしてあるパーツのA/BテストでCVRが0.5%改善しました。次に違うパーツのA/BテストでさらにCVRが0.3%改善し、また次のA/BテストでCVRが0.2%改善しました。良かった良かったと喜んでA/Bテストをやめ、翌週、サイト全体のCVRを計測してみるとCVRが1.8%に悪化して青くなるということがありえるのです。こうなると何を信じて改善サイクルを回していけばよいのかわからなくなります。A/Bテストの粒度の問題だと考えるむきもあるかもしれませんが、それは問題の本質ではありません。粒度を変えてテストを繰り返していると、同じような不一致が起こりえるからです。

部分最適化と全体最適化

私見では、この問題は、部分をどれほど最適化したとしても、全体の最適化にはならないということを意味していると思います。A/Bテストは部分最適化のための手法であり、全体を最適化するための手法ではありません。全体とは部分の総和を超えていると言っても良いと思います。とはいえ、A/Bテストのような部分最適化が無意味と言いたいのではありません。そうではなくて、それには自ずと限界があるということです。それを理解した上でA/Bテストを行うのであれば、A/Bテストを絶対視することなく、Wbサイトやアプリの改善に取り組めるのではないでしょうか。

デザインの「全体」

さて話はここで終わりません。私は、Webサイトやアプリの「全体」を最適化するとはいかなることなのか、ということに興味をもちました。というのも、「全体」を考察することで、AIや自動化で代替できないデザインの領域がはっきりしてくると思うからです。

さらに、部分最適化に拘泥せず、もっと自由な観点からサイト改善を考えるには、「全体」について理解しておく必要があるように思います。 ただ、全体といっても、それはあくまで「Webサイトやアプリ」のという留保がつくのであって、全体最適化を考えるあまり抽象度を上げ、会社の社長が事業間のリソース最適化を計るような観点で全体を考えていくと、そもそもなぜ全体最適化について考えているのかを見失います。そういう意味では、「」つきの全体です。

私が言わんとしている「全体」とは、マーケティングで言うところのグロースハックに近いのではないかと思いますが、グロースハックはマーケティングの文脈上で考えられており、サイトを成長させるダイナミズムの中から「全体」を捉えようとしています。デザイン部のフロントエンドエンジニアとしては、全体最適化をデザインの文脈で考えたいと思います。

「全体」とAIとデザイナー

Webサイトやアプリの部分とは、ボタンとかバナーなどの各パーツであることは自明ではありますが、「全体」となるとそれほど自明ではないと思います。なぜなら、あるサイトのビジュアルの総計をデザインの「全体」だと考えるのは、早合点だと思うからです。

車のデザインで例えるなら、マツダ車のデザインを理解しようと思うなら、デミオの各パーツを熱心に眺めるだけでは不十分で、他のマツダ車にも一貫して流れているデザインフィロソフィーを掴まなければならないでしょう。

しかもそのデザインフィロソフィーは、「ヨーロッパで車を売りたい」とか「ブランド価値を上げたい」などの経営戦略を鑑みて、デザイナーが磨き上げたものです。これらは車のビジュアルだけからは導けない、より抽象度の高い視点でしょう。車のビジュアルをデザインのすべてだと思うのは、完成したプロダクトにしか目がいかない消費者の視点ですが、デザインの「全体」はこれに還元できません。 Webサイトもこれと同じで、そのビジュアル的な側面から伺えるのは、限定的な部分です。

つまり、ビジュアル全体がデザインの「全体」というわけではないのです。 「全体」はビジュアルではないので、当たり前ですがA/Bテスト出来るようなものではありません。AIや自動化がデザイナーの仕事を奪うのではないかと懸念される時代には、こんな当たり前を再確認しておいた方が良いように思います。

A/Bテストは、あるパーツの良し悪しの判断を自動化することでデザイナーの判断を奪うので、脅威に感じるのは無理からぬことではあるのですが、ただ、それはデザインの価値を矮小化しているのであって、今のところAIや自動化技術は、部分を最適化しているだけでしょう。この先、AIが「全体」を思考できるほどの抽象的思考を身に着ける可能性がないとは言えませんが、かなりのブレイクスルーと時間が必要だと思います。  

また、AIがデザイナーに容易にとって代わることができない理由は、実際のプロダクトを作成する段階において顕著だと思います。

デザインが生成される過程において、Webデザイナーは、プロジェクトマネージャー、ディレクターといった関係者たちと、泥臭い調整や対話が必要になります。ときには喧嘩になることもあるでしょう。しかし、そういった調整や対話のなかでこそ新しいデザインが生まれてきます。デザインの「全体」には、こういった現場の泥臭い生成プロセスも含まれていると考えなければなりません。そうでなければ定義上、デザインの「全体」とは呼べません。

A/Bテストは厄介な調整をキャンセルできることもあるでしょうが、それは部分最適化においてのみであり、新しいサイトのデザインを丸ごと決定することはできないです。 AIとデザインの関係について、『東大准教授に教わる「人工知能って、そんなことまでできるんですか?』という本で、人工知能研究者の松尾豊教授と実業家の塩野誠氏がこう述べています。少し引用してみます。

SHIONO: 気づきや仮説を設定する行為は、人間の大きな能力だと思っています。そしてその先には、クリエイティビティ、広告の分野ならデザインとかキャッチコピーを作るような話につながると思うのですが、人工知能はいつかキャッチコピーを作れるようになるのでしょうか?

MATSUO: 作れると思います。あるキャッチコピーがどれくらいの人に受けそうかは、いまはいろいろな形でテストできます。ウェブの場合、「A/Bテスト」とか「タイムライン解析」などの手法がありますが、あるバージョンを置いて反応を見ていくやり方ですね。キャッチコピーくらいでしたら、それほど深い知識はなくとも言葉の組合せで作っていけますから、それをいったん提示し、反応を見て手直ししていくような処理は、人工知能にもできるようになるでしょう。ただ、広告の全体デザインを決めるとか、映画の宣伝文、短編小説など長めのコンテンツにどこまでこの手法が使えるかは、これからの研究次第ですね。ここはいまから面白くなっていく分野だと思います。

現在のAIの実力からすれば、より限定された部分ならばA/Bテストで最適化することはできるが、より大きな部分の最適化は現状難しいようです。人間のデザイナーのように、デザインの「全体」に関与するのはまだまだほど遠いでしょう。 逆に考えるなら、これからのデザイナーは、A/Bテストで最適化できないより大きな部分や全体性を志向することが求められるのではないでしょうか。先に上げた 『2万回のA/Bテストからわかった 支持されるWebデザイン事例集』で、A/Bテストの役割を再確認してみるのも良いかと思います。

最後に

実は、デザインの「全体」がなんであるのか今でも明確に言うことができません。しかし、少なくともデザインが生成されるプロセスを抜きにして、成果物の外観だけをデザインの「全体」であるという見方では、AIや自動化技術がデザインに介入してくる時代に、定義として賢くはないだろうと思います。

アスタミューゼでは、エンジニア・デザイナーを募集中です。ご興味のある方は遠慮なく採用サイトからご応募ください。お待ちしています。

Excelとscalaのケースクラス間で入出力ができるライブラリをgithubに公開しました

f:id:astamuse:20180731192645j:plain scalaでバックエンドを開発しているaxtstar(@axtstart)です。

今回は、Excelをプログラムから扱うにあたって、面倒だなと感じていた部分をバインド変数の様なアイデアで少し楽にするライブラリを作成、公開しましたのでそちらの紹介をします。

面倒な話

Excelからデータを取得するのってなくなりそうで無くならないですよね。

例えばこんなExcelシートからデータを取得したいとします。

↓こんなの(data.xlsxとします)

f:id:astamuse:20180727120440p:plain

apache-poiを使ったプログラムの場合、下記のようなコードになると思います。

import java.io.File
import org.apache.poi.ss.usermodel._

val workbook = WorkbookFactory.create(new File("data.xlsx"))

val sheet = workbook.getSheetAt(0)

val numeric = sheet.getRow(0).getCell(1).getNumericCellValue // numeric: Double = 111.0
val stringRow = sheet.getRow(1).getCell(1).getStringCellValue // stringRow: String = 111
val dateRow = sheet.getRow(2).getCell(1).getDateCellValue // dateRow: java.util.Date = Thu Jan 01 00:00:00 JST 1970
val formula = sheet.getRow(3).getCell(1).getStringCellValue // formula: String = 111
val bool = sheet.getRow(4).getCell(1).getBooleanCellValue// bool: Boolean = true
val time = sheet.getRow(5).getCell(1).getDateCellValue // time: java.util.Date = Sun Dec 31 17:25:47 JST 1899
val userDate = sheet.getRow(6).getCell(1).getDateCellValue // userDate: java.util.Date = Mon Jul 02 22:35:54 JST 2018

このようにロケーションの指定や、データ型毎の変換など、正確にデータを取得するのは結構大変な作業です。

また、Excelとの突き合わせが、行、列番号とのマッピングになるため、間違いやすいです。

場所がずれるなんてことも結構おこります。

Asta4e

そこで、弊社製Webフレームワークであるasta4dを参考にして、似たような*1テンプレート入出力の機能をapache-poiでExcelを使って行うライブラリを作成しました。

ソースはgithubで公開し、Maven Centralにおいてますのでdependencyに下記のように追加すれば試すことができます。

build.sbtの場合(scala2.11 scala2.12 共通)

libraryDependencies += "com.axtstar" %% "asta4e" % "0.0.6"

mavenの場合

scala2.11

<dependency>
    <groupId>com.axtstar</groupId>
    <artifactId>asta4e_2.11</artifactId>
    <version>0.0.6</version>
</dependency>

scala2.12

<dependency>
    <groupId>com.axtstar</groupId>
    <artifactId>asta4e_2.12</artifactId>
    <version>0.0.6</version>
</dependency>

テンプレートになるExcelシートに、変数となる値を${変数名}で記載します。

↓こんなかんじ(template.xlsxとします)

f:id:astamuse:20180727120001p:plain

データを取得したいExcelを指定します、先ほどの変数の場所に実際の値が入っているもの(つまりdata.xlsx)を、以下の要領で取得できます。

import com.axtstar.asta4e.ExcelMapper

val result = ExcelMapper.getData("template.xlsx" // 変数テンプレート(バインドする変数名を記載したExcel)
  ,"data.xlsx" // 入力用データ(読み込みたいExcel)
  ,List() // 無視するシート名(読み込まないシート名のリスト)
)

resultは下記のようになっています。

result: IndexedSeq[(String, Map[String,Any])] = 
Vector((
Sheet1,
Map(
userDate -> Mon Jul 02 22:35:54 JST 2018, 
string -> 111, 
bool -> true, 
date -> Thu Jan 01 00:00:00 JST 1970, 
formula -> 111, 
numeric -> 111.0, 
time -> Sun Dec 31 17:25:47 JST 1899)))

${変数名}で指定した変数名に対して、その位置上の値を取得格納し、 { シート名 ->Map(バインド変数名、値)} を返します。

こうすることで、セル位置の指定をコードから排除し、位置の指定をExcel上で行うようにして、生産性を向上することができます。

テンプレートエンジン機能

Excelを出力したい場合は、上記の考え方と同様にテンプレートのExcel上のバインド変数の位置にデータを出力できる関数を用意しました。(テンプレートエンジン機能)

ただ、出力はレイアウトのことを考えて、出力用フォーマットのExcelを別途指定して、そちらからコピーして出力を行います。*2

↓出力用フォーマット(レイアウト)

f:id:astamuse:20180731185046p:plain

import com.axtstar.asta4e.ExcelMapper

ExcelMapper.setData(
  "template.xlsx" // 変数テンプレートパス
  ,"layout.xlsx" // 出力用フォーマットExcelパス
  ,"output.xlsx" // 出力先のパス
  , result :_* // 出力用データ
)

↓出力結果

f:id:astamuse:20180731195036p:plain

ケースクラス変換

scalaではエンティティの定義をケースクラスで行うことがほとんどだと思いますので、Excelとケースクラスの相互変換の関数も用意しておきました。

テンプレートExcelと同じ変数名*3を持つケースクラスを作成します。

import java.util.Date

case class Data(
              numeric: Double,
              string:String,
              date:Date,
              formula:String,
              bool:Boolean,
              time:Date,
              userDate:Date
)

Excel → ケースクラス

下記の方法で先程のExcelからケースクラスを取得できます。

val data = ExcelMapper.by[Data].getDataAsAny("template.xlsx" // バインド変数テンプレート
  ,"data.xlsx" // 入力用データ
  ,List() // 無視するシート名
)

結果は下記です

data: IndexedSeq[(String, Option[Data])] = Vector((Sheet1,
Some(
Data(111.0,
111,
Thu Jan 01 00:00:00 JST 1970,
111,
true,
Sun Dec 31 17:25:47 JST 1899,
Mon Jul 02 22:35:54 JST 2018)
)
))

少しわかりにくいですが構造的に

Sheet名 --+ Option(ケースクラス) のリスト構造を持っています。

ケースクラス → Excel

こちらは逆変換、ケースクラスを含むIndexedSeq[(String, Option[ケースクラス])]を指定してExcelファイルの出力を行います。

ExcelMapper.By[Data].setData4cc("template.xlsx" // バインド変数テンプレート
  ,"layout.xlsx" // 出力用フォーマットExcelパス
  ,"output.xlsx" // 出力パス
  , data
)

上記でExcelファイルの作成ができます。

全体イメージ

f:id:astamuse:20180801023957p:plain

まとめ

Excelでの開発がだいぶ楽になりました。

いかがでしたでしょうか?

アスタミューゼでは現在、エンジニア・デザイナーを募集中です。 興味のある方はぜひ下記バナーからご応募ください。

*1:Asta4dでは、CSSセレクタを使って、HTML要素にアクセスすることができます。

*2:変数テンプレートは位置だけを指定、実際のフォーマットはレイアウトである出力用フォーマットで指定する考え方

*3:今の実装だとケースクラスの変数が過剰にある場合は動作せずNoneが帰ります。不足している場合は動作します。

Java9でDeprecatedになったfinalize()をjava.lang.ref.Cleanerで代替する

f:id:astamuse:20180722232243p:plain
はじめまして、お初にお目にかかります。
4月に入社したomiと申します。現在開発部で唯一の女子部員です。
優しく愉快な先輩方に囲まれて、日々感謝の気持ちで胸がいっぱいになりながら楽しく過ごしています。

さて、Javaの話をします。
弊社のJavaシステムの中で、Java9でDeprecated対象となったjava.lang.Object.finalize()を使用している箇所があり、同じくJava9で追加されたjava.lang.ref.Cleanerを用いて代替する方法を検討しましたので、その内容をご紹介します。

そもそもfinalizeのような機構のものを使うべきでない、という話は置いときます。
いろいろな事情でしょうがなく・もしくは意図的にfinalize()を使ってきた人たちのための代替方法です。

finalize()について

java.lang.Object.finalize()は、このオブジェクトへの参照はもうないとガベージ・コレクションによって判断されたときに、ガベージ・コレクタによって呼び出されるメソッドです。(公式API参照)
finalize()内にリソースのクローズ処理を記述しておくことで、GCのタイミングでリソースの破棄前にクローズ処理を実行させることができるものです。(とは言ってもご存知の通りfinalize()は呼ばれるタイミングが不定、確実に呼ばれる保証はない、など多くの問題があります)

 実際に動きを確認してみます。

public class TestFinalize {
    public static void main (String[] args) throws Exception {
        Finalize s = new Finalize();
        // using resource
        s = null; // 参照の削除
        System.out.println("System.gc()-start");
        System.gc(); // 強制GC
        System.out.println("System.gc()-end");
    }
}

class Finalize {
    public Finalize() {
        System.out.println("constructor");
    }

    public void close() {
        System.out.println("close");
    }
    
    @Override
    protected void finalize() throws Throwable{
        try{
            System.out.println("finalize()");
            this.close();
        } finally {
            super.finalize();
        }
    }
}

結果はこのようになりました。

$ java TestFinalize
constructor
System.gc()-start
System.gc()-end
finalize()
close

GCが走ったタイミングでfinalize()が呼ばれ、記述しておいたclose処理が実行されていることがわかります。

Cleanerについて

java.lang.ref.Cleanerは、クローズ対象のオブジェクトと対応するクリーニングアクション(Runnable実装クラス)を登録しておくことで、オブジェクトがルートセットから参照されなくなった時(PhantomReachableになった時)に該当クリーニングアクションを呼び出してくれるものです。(公式API参照)

CleanerによるRunnable実行(クリーニングアクション実行)タイミングには2通りあります。

  • Cleaner.Cleanable.clean()メソッドが明示的に呼び出された時
  • 上記clean()の呼び出しがされておらず、オブジェクトがPhantomReachableになった時

前者はAutoCloseableを継承しオーバーライドしたclose()メソッドにCleanable.clean()呼び出しを記述しておけば、実質try-with-resourcesにおいてリソースがスコープから外れた際に自動でRunnable実行しクローズを行うよう記述することができます。(実装は公式API参照)これは「最も効率的な方法」と書かれています。
(しかし従来通り、オーバーライドしたclose()メソッドに直接クローズ処理を記載しておけば、わざわざCleanerを実装する必要がないように思います。。??私には需要がわかりませぬ。。だれか教えてください。。)

今回はfinalize()の代替として同じ動きを再現したいので、後者のタイミングでのクローズ処理について動きを確認してみます。
クローズ処理を記述するRunnable実装クラスを作っておき、コンストラクタでCleaner.registerメソッドを呼び出し、オブジェクトと対応Runnableを登録しておきます。

import java.lang.ref.Cleaner;

public class TestClean {
    public static void main (String[] args) throws Exception {
        Clean s = new Clean();
        // using resource
        s = null; // 参照の削除
        System.out.println("System.gc()-start");
        System.gc(); // 強制GC
        System.out.println("System.gc()-end");
    }
}

class Clean {
    private CloseObject obj = new CloseObject();
    private static final Cleaner cleaner = Cleaner.create();
    private final CloseRunnable closeRunnable;
    private final Cleaner.Cleanable cleanable;

    public Clean() {
        System.out.println("constructor");
        this.closeRunnable = new CloseRunnable(obj);
        this.cleanable = cleaner.register(this, closeRunnable);
    }
}

class CloseObject {
    public void close() {
        System.out.println("close");
    }
}

class CloseRunnable implements Runnable{
    CloseObject obj;
    CloseRunnable (CloseObject obj) {
        this.obj = obj;
    }
    public void run() {
        System.out.println("Runnable.run()");
        obj.close();
    }
}

結果はこのようになりました。

$ java TestClean
constructor
System.gc()-start
System.gc()-end
Runnable.run()
close

finalize()同様、GCが走ったタイミングで登録しておいたRunnableが呼ばれ、記述しておいたclose処理が行われていることがわかります。

finalize()とCleanerとメモリ解放について

finalize()やCleanerを使用する上で留意しておかなければならないことがあります。
finalize()、Cleanerによるメモリ解放は、Full GCが少なくとも2回以上発生しないと実現しない、ということです。
finalize()、Cleanerのメモリ解放の流れは以下の通りです。

  1. ルートセットからオブジェクト参照がなくなる。
  2. Full GCが実行される。(オブジェクトがPhantomReachableになる)
  3. (PhantomReachableになったのを検知し、)FinalizerやCleanerの処理が実行される。
  4. (オブジェクトがReferenceQueに格納され、)オブジェクトが削除対象となる。
  5. 次に起こるFull GCでオブジェクト削除され、メモリ解放される。

※()内は、少し難しいので読み飛ばして頂いて結構です。
GCによるクリーンアップはjava.lang.refパッケージの各種Reference達やReferenceQueを用い制御することができます。
PhantomReachable:オブジェクトの参照レベルが、一番弱い参照レベルのPhantomReferenceのみになった状態。
参照レベルによってオブジェクトがReferenceQueに格納されるタイミングが異なります。Phantomの場合はオブジェクトがfinalize()処理された後にReferenceQueに入ります。Queに格納対象となったオブジェクトは次GCで削除されます。

実際に確認してみます。まずfinalize()実行時のメモリ解放。
メモリの増減が分かりやすいようにインスタンスを大量生産しています。

import java.util.concurrent.TimeUnit;
import java.lang.management.MemoryMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.MemoryUsage;

public class TestFinalizeMemoryCheck {
    public static void main (String[] args) throws Exception {
        check("初期値");
        // using resource
        for(int i = 0 ; i < 100000 ; i++) {
            Finalize s = new Finalize();
            s = null; // 参照の削除
        }
        check("インスタンスnew後");
        System.out.println("System.gc()-1回目");
        System.gc(); // 強制GC
        check("GC-1回目後");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("System.gc()-2回目");
        System.gc(); // 強制GC
        check("GC-2回目後");
    }

    // 使用メモリを計測
    public static void check(String str){
        StringBuilder buff = new StringBuilder();
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memoryBean.getHeapMemoryUsage();
        buff.append("usedmemory=");
        buff.append(usage.getUsed());
        System.out.println(buff.toString()+ " : " + str);
    }
}

class Finalize {
    public Finalize() {
        // System.out.println("constructor");
    }

    public void close() {
        // System.out.println("close");
    }

    @Override
    protected void finalize() throws Throwable{
        try{
            this.close();
        } finally {
            super.finalize();
        }
    }
}

結果はこうなりました。

$ java TestFinalizeMemoryCheck
usedmemory=2684392 : 初期値
usedmemory=8053248 : インスタンスnew後
System.gc()-1回目
usedmemory=7273832 : GC-1回目後
System.gc()-2回目
usedmemory=1674040 : GC-2回目後

2回目のGCでメモリ解放されています。
同様に、Cleanerの場合は

import java.util.concurrent.TimeUnit;
import java.lang.ref.Cleaner;
import java.lang.management.MemoryMXBean;
import java.lang.management.ManagementFactory;
import java.lang.management.GarbageCollectorMXBean;
import java.lang.management.MemoryUsage;

public class TestCleanerMemoryCheck {
    public static void main (String[] args) throws Exception {
        check("初期値");
        // using resource
        for(int i = 0 ; i < 100000 ; i++) {
            Clean s = new Clean();
            s = null; // 参照の削除
        }
        check("インスタンスnew後");
        System.out.println("System.gc()-1回目");
        System.gc(); // 強制GC
        check("GC-1回目後");
        TimeUnit.SECONDS.sleep(3);
        System.out.println("System.gc()-2回目");
        System.gc(); // 強制GC
        check("GC-2回目後");
    }

    // 使用メモリを計測
    public static void check(String str){
        StringBuilder buff = new StringBuilder();
        MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean();
        MemoryUsage usage = memoryBean.getHeapMemoryUsage();
        buff.append("usedmemory=");
        buff.append(usage.getUsed());
        System.out.println(buff.toString()+ " : " + str);
    }
}

class Clean {
    private CloseObject obj = new CloseObject();
    private static final Cleaner cleaner = Cleaner.create();
    private final CloseRunnable closeRunnable;
    private final Cleaner.Cleanable cleanable;

    public Clean() {
        // System.out.println("constructor");
        this.closeRunnable = new CloseRunnable(obj);
        this.cleanable = cleaner.register(this, closeRunnable);
    }
}

class CloseObject {
    public void close() {
        // System.out.println("close");
    }
}

class CloseRunnable implements Runnable{
    CloseObject obj;
    CloseRunnable (CloseObject obj) {
        this.obj = obj;
    }
    public void run() {
        // System.out.println("Runnable.run()");
        obj.close();
    }
}

結果は

$ java TestCleanerMemoryCheck
usedmemory=2821744 : 初期値
usedmemory=13617704 : インスタンスnew後
System.gc()-1回目
usedmemory=9668736 : GC-1回目後
System.gc()-2回目
usedmemory=2118416 : GC-2回目後

同じく2回目のGCでメモリ解放されています。
Full GCが起こる頻度にも気をつけなければいけないということですね。

おわりに

とうとう死亡フラグがたったfinalize()の代替方法について検討してみました。
何らかの理由でfinalize()を使っていた方のとりあえずの回避策として参考になれば嬉しいです。

最後に、アスタミューゼではエンジニア・デザイナーを大募集しています!
少しでも気になった方は下のバナーから是非採用サイトをご覧下さい!優しくて愉快な先輩方と私があなたをお待ちしてます!わくわく!
では。サリュ!

Copyright © astamuse company, ltd. all rights reserved.