谷本 心 in せろ部屋 このページをアンテナに追加 RSSフィード

2016-12-08

Optimizing JavaというJavaパフォーマンス系の書籍が面白そう

急激な冷え込みのせいで「寒い!」というつぶやきがTLに散見されるこの頃ですが、皆さんお風邪など召していらっしゃらないでしょうか。

否応なしに寒いという言葉に反応してしまう、けなげなエンジニアの @ です。


このエントリーは Java Advent Calendar 2016 の8日目です。

昨日は @ さんの「Java Stream APIでハマったこと」で、

明日は @ さんの「マイクロベンチマークツール、JMHについて」でした。


今日のエントリーでは、Javaのパフォーマンス系書籍を紹介したいと思います。

Optimizing Java - O’Reilly Media

URLを見るにつけ、あのオライリー様のサイトですら拡張子が由緒正しい .do なのですから、日本のSIerStrutsを使うことをどうして否定できましょうか。

いえ、今日はそんな話題ではありません。


紹介したいのは上のリンク先の本、「Optimizing Java - Practical Techniques for Improved Performance Tuning」です。名前の通り、Javaのパフォーマンスに関する書籍です。まだEarly Releaseの段階で、全体の1/3ほどしか書かれていませんが、現状の版を入手したので紹介したいと思います。


ここまでで、「あれ、なんか似たような本がなかったっけ」と思った方がいらっしゃるかも知れません。そう、オライリー社からは2015年に「Javaパフォーマンス」という書籍が出版されています。

Javaパフォーマンス - O’Reilly Japan

こちらの日本語版では、私も監訳者まえがきを書かせて頂き、Java Day Tokyoで寺田佳央さんと共にサイン会を行いました。

当時はきっと「この寺田さんの横にいて本に落書きしてる人、誰なんだろう」と思われていたかも知れませんが、私を誰だと思ってるんでしょう、せろさんだぞ?


この2冊について、比較しながら紹介しましょう。


目次

Javaパフォーマンス」の目次は、次の通りです。

1章イントロダクション
2章パフォーマンステストのアプローチ
3章Javaパフォーマンスのツールボックス
4章JITコンパイラのしくみ
5章ガベージコレクションの基礎
6章ガベージコレクションアルゴリズム
7章ヒープのベストプラクティス
8章ネイティブメモリのベストプラクティス
9章スレッドと同期のパフォーマンス
10章Java EEのパフォーマンス
11章データベースのベストプラクティス
12章Java SEAPIのパフォーマンス

JavaのメモリやGCスレッドに関する紹介から、SE / EEやデータベースのパフォーマンスに広げた話をしています。


一方、「Optimizing Java」の目次は次の通りです。

Chapter 1Optimization and Performance Defined
Chapter 2Overview of the JVM
Chapter 3Hardware and Operating Systems
Chapter 4Performance Testing
Chapter 5Measurement and Bottom-Up Performance
Chapter 6Monitoring and Analysis
Chapter 7Hotspot GC Deep Dive
Chapter 8Garbage Collection Monitoring and Tuning
Chapter 9Hotspot JIT Compilation
Chapter 10Java Language Performance Techniques
Chapter 11Profiling
Chapter 12Concurrent Performance Techniques
Chapter 13The Future

うん、ほとんど一緒やん?


「Optimizing Java」には、「Javaパフォーマンス」では触れられていたSEやEEの話などはないため、そこが差分になりそうにも見えます。ただ正直、「Javaパフォーマンス」の10章以降はちょっと薄口な感じでしたので、そこを飛ばせばほとんど同じ内容を網羅していると言えます。


では、何が違うんでしょうか。


Javaパフォーマンス vs Optimizing Java

僕が見た限りでは「Javaパフォーマンス」は教科書に近い内容、「Optimizing Java」はやや読み物寄りの内容になっています。

「Optimizing Java」は、現在執筆されているChapter 5までしか読めていませんが、「Javaパフォーマンス」には書かれていなかったOSJVM周りのレイヤーの話や、テスト戦略の話など、少し目線が違った内容を書いていました。


たとえば、Javaのクラスファイルが「0xCAFEBABE」から始まっていることは、Javaに詳しい方なら既にご存じかと思います。ただ、その先はどうなっているのか。

書籍では次のように紹介されています。

  • Magic Number (0xCAFEBABE)
  • Version of Class File Format
  • Constant Pool
  • Access Flags
  • This Class Name
  • Super Class Name
  • Interfaces
  • Fields
  • Methods
  • Attributes

この先頭を取って

M V C A T S I F M A、

語呂合わせして

My Very Cute Animal Turn Savage In Full Moon Areas

なんて紹介されています。


「僕のとってもかわいい猫は、満月のエリアで凶暴になる」

・・・覚えやすいんですかね、これ?


あ、なんかふざけた本だなと思ったかも知れませんが、もちろん技術的な面もきちんと紹介されています。

あくまで上に書いたようなウィット(?)も挟みながら、Javaの領域だけでなく、必要に応じて低レイヤーにも触れて紹介する本となっているわけです。そのため、「Javaパフォーマンス」を読んだ方でも楽しめる本になるのではないかと思います。


で、いつ出るの? 日本語版は?

この本は2017年3月に出版予定となっています。


また、皆さん気になる日本語版ですが、残念ながらまだ翻訳されることは決まっていないようです。

ただ原著の人気が高かったり、この後に公開される6章以降の内容が「Javaパフォーマンス」とはまた違った切り口であり楽しめるのであれば、翻訳される可能性も十分にあるんじゃないかなと思っています。


そんなわけで、日本語版が出ることを祈りながら、このエントリーを書きました。

Stay tuned, see you!

2016-01-06

[]AWS Lambda + Javaは、なぜ1回目と3回目の処理が重いのか?

以前のエントリーで、AWS LambdaでJavaを使ってDynamoDBを呼び出した際に、初回起動にとても時間が掛かったという話を書きました。

http://d.hatena.ne.jp/cero-t/20160101/1451665326


今回は、この辺りの原因をもう少し追求してみます。


なぜ1回目と3回目のアクセスが遅いのか?

AWS Lambdaの中身はよく知りませんが、おそらく、アップロードしたモジュールTomcatみたいなコンテナとして起動させて、外部からコールしているんだろうと予想しました。それであれば、2回目以降のアクセスが早くなることは理解ができます。

ただ、1回目と3回目だけが極端に遅くて、2回目、4回目以降は早くなるというところは腑に落ちません。


その辺りを調べるべく、staticなカウンタを使って、値がどんな風に変化するかを調べてみました。

こんなソースコードです。

public class Pid {
    static AtomicLong counter = new AtomicLong();

    public String myHandler() {
        long count = counter.incrementAndGet();
        String name = ManagementFactory.getRuntimeMXBean().getName();
        System.out.println("Name: " + name);
        System.out.println("Count: " + count);
        return "SUCCESS";
    }
}

出力された結果は、次のようになりました。

回数NameCount
1回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal1
2回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal2
3回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal1
4回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal2
5回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal3
6回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal4
7回目1@ip-10-0-aaa-bbb.ap-northeast-1.compute.internal5
8回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal3
9回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal4
10回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal5
11回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal6
12回目1@ip-10-0-xxx-yyy.ap-northeast-1.compute.internal7

サーバIPアドレスが2種類あり、それぞれのサーバで、1から順番にカウントアップしていることが分かります。

なるほど、2台のサーバでロードバランシングしているのだと。そのため、それぞれのサーバの初回起動である、1回目と3回目の処理に時間が掛かるのですね。なかなか納得いく結果でした。

ちなみにロードバランシングは毎回このような結果になるわけではなく、1回目と2回目がそれぞれ別のサーバに行く(=処理に時間が掛かる)こともあります。


どんなコンテナを使っているのか?

先ほど「Tomcatみたいなコンテナ」を使っているんじゃないかと推測しましたが、実際、どんなコンテナを使っているのでしょうか。スレッドダンプを取って、確かめてみました。


こんなコードです。

public class StackTrace {
    public String myHandler() {
        new Exception("For stack trace").printStackTrace();
        Arrays.stream(ManagementFactory.getThreadMXBean().dumpAllThreads(true, true))
                .forEach(System.out::println);
        return "SUCCESS";
    }
}

結果、こうなりました。

java.lang.Exception: For stack trace
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:8)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:497)
	at lambdainternal.EventHandlerLoader$PojoMethodRequestHandler.handleRequest(EventHandlerLoader.java:434)
	at lambdainternal.EventHandlerLoader$PojoHandlerAsStreamHandler.handleRequest(EventHandlerLoader.java:365)
	at lambdainternal.EventHandlerLoader$2.call(EventHandlerLoader.java:967)
	at lambdainternal.AWSLambda.startRuntime(AWSLambda.java:231)
	at lambdainternal.AWSLambda.<clinit>(AWSLambda.java:59)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:348)
	at lambdainternal.LambdaRTEntry.main(LambdaRTEntry.java:93)

"Signal Dispatcher" Id=4 RUNNABLE

"Finalizer" Id=3 WAITING on java.lang.ref.ReferenceQueue$Lock@19bb089b
	at java.lang.Object.wait(Native Method)
(略)

"Reference Handler" Id=2 WAITING on java.lang.ref.Reference$Lock@4563e9ab
	at java.lang.Object.wait(Native Method)
(略)

"main" Id=1 RUNNABLE
	at sun.management.ThreadImpl.dumpThreads0(Native Method)
	at sun.management.ThreadImpl.dumpAllThreads(ThreadImpl.java:446)
	at cero.ninja.aws.analyze.StackTrace.myHandler(StackTrace.java:9)
(略)

何やらシンプルな独自コンテナを使っているみたいです。何度か実行してみても結果は同じでした。

ソースコードがないので推測になりますが、アップロードされたzipをロードして起動する独自コンテナがあり、外部からAPIコールされた際にAWSLambdaクラスあたりが処理を受け取って、zip内のハンドラを呼び出しているのでしょう。


なぜコンストラクタで処理すると早いの?

そういえば、もう一つ、謎な挙動がありました。

それは、Credentialsを取るという重めの処理をコンストラクタで実行すれば、処理時間がかなり短くなるというものです。


ハンドラの中でCredentialsを取ると、実測値で24秒ぐらい、課金対象値で22秒ぐらいでした。

コンストラクタでCredentialsを取っておくと、実測値で8秒ぐらい、課金対象値で6秒ぐらいでした。

ここでいう実測値とは、手元のストップウォッチを使って計測したという意味です。


ここから推測できることは、コンストラクタは事前に処理されていて、そこは課金対象外になるのかも知れません。


・・・あれ、それなら、コンストラクタで重い処理をがっつり走らせて、ハンドラでその結果を取り出せば、課金額を抑えられるじゃないですか?

ということで、ハンドラの中で10秒スリープする場合と、コンストラクタスリープした場合の比較をしてみました。


こんな2つのクラスで試してみます。

public class Wait1 {
    static long origin = System.currentTimeMillis();

    public String myHandler() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "SUCCESS";
    }
}
public class Wait2 {
    static long origin = System.currentTimeMillis();

    public Wait2() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(10000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果は、、、


Wait1(ハンドラの中でsleep)

回数Before waitAftre wait課金対象値実測値
1回目3901039010100ms10秒
2回目3871038710100ms10秒
3回目209173091710100ms10秒
4回目688527885210100ms10秒

きっちり10秒sleepして、課金対象値もそのオーバーヘッド分ぐらい。ストップウォッチで計測した値も同じく10秒ぐらいになりました。


Wait2(コンストラクタでsleep)

回数Before waitAftre wait課金対象値実測値
1回目3381033814800ms25秒
2回目--100ms1秒以下
3回目2611035814800ms25秒
4回目--100ms1秒以下

えーっ、sleepは10秒だったのに、なぜか15秒分ぐらい課金されてしまい、ストップウォッチで計測すると25秒と、えらく時間が掛かりました。これは謎な挙動です。

2回目や4回目ではインスタンス生成が終わっているので、Before waitやAfter waitが出力されず、処理がすぐに終わるというのは納得ですが。


どうして、こんなことが起きるんでしょうか。

不思議に思って、CloudWatch Logsのログを確認してみると・・・

Before wait: 17 
START RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f Version: $LATEST 
Before wait: 249 
After wait: 10250 
Called: 14408
END RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f 
REPORT RequestId: 34ae18c3-b47b-11e5-858f-272ee689265f	Duration: 14531.57 ms	Billed Duration: 14600 ms Memory Size: 128 MB Max Memory Used: 27 MB	

最初のBefore waitの後にAfter waitがなく、Lambdaの処理がSTARTした後に再度Before waitが呼ばれ、After waitした後に、4秒ほど待ってから、ハンドラ処理が実行されてCalledが呼ばれていました。

なるほど、つまりこういうことでしょうか。


1. コンストラクタが実行され、10秒sleepの途中でタイムアウトして強制的に処理が止められ、インスタンス生成を諦めた(プロセスごと破棄された?)

2. 改めてコンストラクタが実行され、10秒sleepした。

3. AWS Lambda内の処理か何かで4秒ぐらい処理が掛かった。

4. ハンドラが実行された。

5. 2〜4の間が課金対象となり、15秒弱となった。

6. ストップウォッチで計測した時間は1から5までの間なので、25秒弱となった。


要するに、コンストラクタで重い処理を行うような悪いことを考える人への対策として、コンストラクタは一定時間で(おそらく10秒きっかりで)タイムアウトして、いったんプロセスは破棄される。

その後、改めてコンストラクタの処理がタイムアウト関係なく実行されたうえで、AWS Lambdaの内部処理と、ハンドラ処理が行われ、すべての処理が課金対象となる、ということころでしょうか。


コンストラクタの処理が短い場合は、どうなるの?

ということで、sleepの時間を短くして、再挑戦してみます。

public class Wait3 {
    static long origin = System.currentTimeMillis();

    public Wait3() {
        try {
            System.out.println("Before wait: " + (System.currentTimeMillis() - origin));
            Thread.sleep(2000);
            System.out.println("After wait: " + (System.currentTimeMillis() - origin));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public String myHandler() {
        System.out.println("Called: " + (System.currentTimeMillis() - origin));
        return "SUCCESS";
    }
}

結果。

回数Before WaitAfter wait課金対象値実測値
1回目--100ms3秒
2回目--100ms1秒以内
3回目--100ms1秒以内

CloudWatch Logsでの出力

Before wait: 25 
After wait: 2026 
START RequestId: xxx Version: $LATEST 
Called: 2389 
END RequestId: xxx 
REPORT RequestId: xxx	Duration: 97.52 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	 
START RequestId: yyy Version: $LATEST 
Called: 16385 
END RequestId: yyy 
REPORT RequestId: yyy	Duration: 6.04 ms	Billed Duration: 100 ms Memory Size: 128 MB	Max Memory Used: 27 MB	

今度はSTARTの前に、きちんとBefore waitもAfter waitも出力され、ハンドラ処理のみが課金対象となっていました。


まとめ

ここまでの話をまとめると、AWS Lambdaは・・・


1. 複数台のサーバで処理されるため、それぞれのサーバでの初回起動時には処理時間が掛かる。

2. 独自のコンテナを利用して、モジュールデプロイしている。

3. コンストラクタの処理が軽い場合は、ハンドラ内の処理だけが課金対象となる。

4. コンストラクタの処理が重い場合は、コンストラクタの処理 + 5秒弱 + ハンドラ内の処理が課金対象となる。


ということですね。


・・・とは言え、Credentialsの処理をコンストラクタで行った場合に、実測値まで早くなる辺りは、少しだけ不可解です。というか、Credentialsの取得処理が重いこと自体が不可解なのですが。

この辺りはもう少し追試験をしてみれば解析できそうですが、長くなるので、今回はこの辺りまでにしたいと思います。


いやー、Lambdaさん、なかなかよく考えられてますね!

2013-09-30

[]俺様とJavaOne 2013(中編)

JavaOne 3日目、自分のセッション当日は

やっぱり直前まで資料準備&練習でバタバタしていました。

この性格、死ぬまで治らない予感!


Day 3 : 解析ツールのセッションは人気

3日目、僕のセッションは夜7時半からなので、

朝イチにあった面白そうなセッションに参加していました。


[CON5092] Diagnosing Your Application on the JVM

元BEAのStaffan Larsenのセッション。

朝イチにも関わらず、満席になる人気セッションでした。


内容は、診断・解析ツールについて、デモを交えて次々と紹介するセッションで

主に7u40から使えるようになった(7u4から使えてたものもあるけど)

「jcmd」(旧jrcmd)を中心に紹介されていました。


ちょっと列挙しますと・・・

jps : Javaプロセスの一覧を列挙する

jcmd : 引数なしならjpsと同じ

jcmd <pid> VM.uptime : Javaプロセスの起動後の経過時間

jcmd <pid> Thread.print : スレッドダンプ。jstackと同じ。

jcmd <pid> GC.heap_dump : ヒープダンプ。jmapと同じ。

jcmd -gcnew <pid> 1s : 毎秒のGC領域のサイズを見る

jcmd <pid> PerfCounter.print : JVM内部で保持している様々なカウンタを取得

などなど。


自分的に衝撃だった事と言えば、jcmdコマンドの引数で pid に 0 を指定すると

全てのJavaプロセスの情報をまとめて取れる、というところ。

実際に使う機会があるかどうかは分かりませんが、良い事を知った感がありますね(笑


またセッション後半は、このような解析ツールをリモートから実行するために、

jstatdやJMX Remoteを利用するという話や、その裏側の仕組みが説明されました。


正直、他の作業をしながら聞いていたので、かなり聞き逃してしまったのですが

リモートから診断・解析をする時に、何ができるか・できないかを判断するための

背景となる知識が得られる良い内容でした。まさにJavaOneらしい内容だったと言えます。


特に解析する機会が多いとか、開発ツールを作る立場であるとか、

そういう人は、このセッションの資料をきちんと読むべきだと思いますね、

っていうか、私も、きちんと読み直します!


ところで、セッションの後に

こんな風にスピーカーのStaffanさんに、お礼なのか挑発なのか分からないツイートをして

私のセッションに来て頂きました。我ながら、強引なことをしたもんです。


そんなわけで、夜には自分のセッションがあったわけですが

それについては、前後の話も含めて、またきちんと別エントリとして投稿します。


Day 4 - Lambda、Lambda、JFR

おはよう世界。

自分のセッションが終わった開放感からか、倒れ込むように寝てしまい

これは昼まで寝るかなと思ったら、意外と5時間睡眠ぐらいで目が覚めてしまい

時差ボケの威力を実感した早朝でした。


そんなわけで、朝から元気にセッションに参加します。

そう、僕のJavaOne参加はここから始まったわけです。


[CON2055] Programming with Lambda Expressions in Java

Agile Developer, Inc.の社長、Venkat Subramaniamによる

軽妙でウィットに溢れたLambdaのセッションでした。


内容的には、外部イテレーターから内部イテレーターの書き方の移り変わり、

Lambdaの文法やstreamの使い方や効果などを紹介するという

比較的、初級者向けのセッションなのですが、その語り口調が面白すぎて

本当に笑いの絶えないセッションでした。


直接的な表現よりも、間接的な表現を軽妙に語る事で面白さを増す感じでしたね、たとえば

  • 汚い → とても子供に見せられない。しっ見ちゃいけません!
  • 危険 → 何をやろうとしているんだ、家に帰って考え直せ!
  • 素敵 → これは食欲をそそる!

などなど。

って私はJavaOneに来て何を学んでるんですかね。


もう少し実用的なところをフィードバックすると、

やはりLambda時代にはAPIデザインが少し変わるということでしょうか。


たとえば自分で比較するユーティリティメソッドを書く際には、

isPriceLessThan(500, value) と書けるようなAPIを提供するのではなく、

isPriceLessThan(500).test(value) と書けるようなAPIを提供することで

Lambda式として利用できるようにしていました。


ちょっとこの辺り、自分でも消化しきれていないので

日本に帰ったら資料を見ながら復習しようと思います。


ってよく考えたら、このセッション、

テキストエディタだけで話してたから、資料ないんだった (^^;;


[CON7942] Java 8 Streams: Lambda in Top Gear

続けてのLambdaセッション。

Paul Sandozと、Lambdaの神Brian Goetzのセッションです。


streamのAPIは、集計処理などにおいて、

うまくparallel化ができるもの、できないものがあったり、

処理を途中で中断しても良いもの、全ての要素を走査するものがあるなど、

APIは、いくつかのカテゴリで「分類」することができます。


この分類次第で、parallel化した時のパフォーマンスなども変わってくるため

streamを使う際には、この分類をきちんと押さえておかなければいけない、

ということが説明されていました。ちょっと自分にはなかった視点でした。


この辺りは、資料をダウンロードして学び直す必要があるので、

ボロが出ないうちに、説明をこの辺で切り上げましょう (^^;


[CON5091] Java Flight Recorder Behind the Scenes

3日の朝イチに解析ツールの紹介していた、Staffan Larsenのセッション。

前半こそFlight Recorderの紹介だったのですが、

後半はFlight Recorderの設計の話が展開され、かなり興味深かったですね。

そんな後半の話だけピックアップして紹介します。


1. Thread buffers

Flight Recorderが取得した情報は、スレッドローカルのThread buffersに貯めてから、

共有のGlobal buffersに書き出します。

こうすることで、Global buffersへの書き込みが衝突することを抑えています。

この辺りは、Flight Recorderがメイン処理に影響を与えないようにするために

欠かせない、いわば当たり前の設計でしょう。


2. Flight Recorderは永久には情報を取り続けない。

メモリリークへの対策として、情報は一定期間かサイズごとに消すか上書きしています。

これも当たり前のことですが、私は過去にちょっとやらかした事があります (^^;


3. Flight Recorderは、アプリケーションのクラスやオブジェクトへの参照を持たない。

これも、メモリリークを防ぐうえで不可欠のポリシーです。


4. クラス名はIDに変換する。

クラス名(文字列)を、int程度の数値に変換することで、

ファイルやメモリの空間効率を向上させます。

また、そのクラス名と数値のマップをFlight Recorderの出力ファイルに

持たせておくことで、互換性や移植性にも配慮しています。

こういう細かいところも、きちんと工夫しているんですね。


5. クラス一覧自体も定期的にリセットする。

クラス一覧がメモリリークの原因にならないよう、

一定期間ごとにファイルに出力して、クラス一覧をクリアしてしまいます。

(このタイミングを「チェックポイント」と読んでいました)

最後にクラス一覧をマージするかどうかは、ちょっと分かりませんでした。

マージしないと重複が出てしまって、ファイル効率がよくない気がします。


6. スタックトレースのpoolを作る。

同じスタックトレースが何度も表れることが多いため、

スタックトレースのプールを作っておいて、(全く)同じスタックトレースが

発生した場合には、前のスタックトレースへの参照を使うだけにします。

なるほど、勉強になります。


というような、ENdoSnipeの開発者的にありがたい情報がたくさんありました。

もちろんアプリケーション開発をするうえでも、このようなメモリやデータの

効率化の仕組みを「発想」することは、とても大切だと思います。

まだまだやるべき事があるのだなと、改めて思い知らされた感じです。


[CON2959] Modular JavaScript

Luminis Technologies社のSander Mak、Paul Bakkerのセッション。

なんかJavaとの連携もありそうな感じのアジェンダが提示されていたんですが

実際には、JavaScriptのライブラリやフレームワークを使ったときの

packageやclassの可視性なんかを、延々延々とJavaScriptのソースで説明するセッションで、

Javaコードは全く出てきませんでした。


ここJavaOneやぞ!


4日目終わり

この後の時間帯に、kotlinのセッションがあったり、

あの #てらだよしお さんのJavaEEのセッションがあったのですが、

どうしても眠かったため、ホテルに戻って休んでいました。


聞くところによると、kotlinのセッションは10人いなかったそうです。

kotlinの過疎感ハンパない!(><)


そして寺田さんのセッションは「質問はTwitterでお願い」と言っていたにも関わらず、

バンバン質問が出て、大変だったそうです(そして、きちんと回答したそうです!)

そりゃOracleのエンジニアがJavaEEについて話したら、質問出るって!


ちなみに4日目の夜には、トレジャーアイランドで

Maroon5というバンドのライブなどあったのですが、

上にも書いた通り、ホテルに戻ってお休みしていました。


Folder5が来るんだったら、無理してでも行ったと思うんですけどね。

#行かねーよ。


そんなわけで、JavaOneも後半戦に差し掛かってきました。

2012-08-30

[]volatileとか使うなと怒られた話

JJUG Night Seminar ~ Java VM<&納涼会 ~

http://kokucheese.com/event/index/48437/

に参加してきました。


「スタックマシンとしてのJavaVM」なんて言う

一見さんお断りみたいなタイトルに集まった人たちはもちろんレベルが高く

参加者のjavap経験率が20%ぐらいになるなど、なかなか偏った集客具合でした。


そんな中、私もLTでBTraceの話をしようと、45枚のスライドと、2種類のデモを用意していました。

ただ客層を考えるに、最近Twitterでちょっと呟いていた

「longに複数スレッドから値を代入すると、想定外の値になる」話をした方が良いんじゃないかと思い、

観客にどちらが聞きたいか尋ねてみたところ、longの方が圧倒的人気。


そんなわけで、45枚のスライドをドブに捨てて(いつか再利用するよ!)

スライドなしで、longの話をしてきました。

longに1と-1を入れ続けると、4294967295や-4294967295になる

論より証拠、まずはWindowsXP 32bitなど、

普通の32bit OSで、以下のコードを実行してみてください。

public class VolatileTest {
	private static long longValue = 0;

	public static void main(String[] args) throws Exception {
		final int LOOP = 1000 * 1000 * 1000;

		Thread th1 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < LOOP; i++) {
					longValue = 1;
					check(longValue);
				}
			}
		});

		Thread th2 = new Thread(new Runnable() {
			public void run() {
				for (int i = 0; i < LOOP; i++) {
					longValue = -1;
					check(longValue);
				}
			}
		});

		th1.start();
		th2.start();

		th1.join();
		th2.join();

		System.out.println("Finished");
	}

	private static void check(long value) {
		if (value != 1 && value != -1) {
			throw new RuntimeException(String.valueOf(value));
		}
	}
}

2スレッドからlongValueに1と-1を入れ続け、

もし値が1と-1以外になった場合には例外を発生させる、というプログラムです。


素直に考えると「1」と「-1」以外になることはあり得ないのですが、

実際には「4294967295」や「-4294967295」になることがあります。

64bitなlongへの代入は、上位32bitと下位32bitに分けて行なう

なぜそのような事が起こるのでしょうか?

それは、longへの代入がアトミック(分割できない最小単位)ではないためです。


見出しに書いた通りですが、Javaのlongは64bitであり、

特に32bit版のJavaVMでは、上位32bitと下位32bitの代入は別々に行なわれるのです。


つまり上に書いたプログラムの代入部分は、こんな風に読み替えることができます。

// Thread1側 (longValue = 1)
longValue_upper32 = 0x00000000;
longValue_lower32 = 0x00000001;

// Thread2側 (longValue = -1)
longValue_upper32 = 0xFFFFFFFF;
longValue_lower32 = 0xFFFFFFFF;

この処理をマルチスレッドで動かすわけですから、値の状態として

 longValue_upper32 = 0x00000000;

 longValue_lower32 = 0xFFFFFFFF;

とか

 longValue_upper32 = 0xFFFFFFFF;

 longValue_lower32 = 0x00000001;

とかになることは、容易に想像ができます。


その結果、

 前者の0x00000000FFFFFFFFが「4294967295」

 後者の0xFFFFFFFF00000001が「-4294967295」

になるのです。


ただしこの問題、64bitなMac OSJavaでは発生しません。

少なくとも私のMacBook Air(Lion)では、代入を上位下位に分割せず

64bit分を一度に代入するため、常に1か-1になります。

そこでvolatileの登場です

「ここで出てくるのがvolatileです」

って言ったら櫻庭さんから「あり得ない!」っていきなり怒られたのですが(><)

めげずに説明を続けますと

private volatile static long longValue = 0;

と、volatileで宣言すれば32bit Javaでも問題は起きません。


volatileには「値の参照の際に、常に最新の値を見に行く」という性質があり、

ざっくり言えば、volatileの代入や参照はロックされているような振る舞いをします。


そのため、longValueの値を上位32bit、下位32bitともきちんと処理し終わってから

他のスレッドからlongValueの参照ができるようになります。


なんだかよく分からない修飾子ナンバーワンのvolatileですが、

この例を通して見て頂ければ、その振る舞いがよく分かるのではないかと思います。

でもホントはAtomicLongを使う。

とは言え、

実際にどう振る舞うかが保証(規定)されていないvolatileを

このような場所で使うのは、必ずしも安全とは言えません。


また、よく分からない修飾子ナンバーワンなのですから、

よく分からないままコピーされたり、よく分からないまま削除されたり(!)しかねません。


では代わりに何を使うべきかというと、AtomicLongです。

private static AtomicLong longValue = new AtomicLong(0);

AtomicLongのgetやsetは、その名前の通りアトミックに行なわれるため

マルチスレッドで操作しても問題は発生しません。

分かりやすい名前のため、可読性も高いと言えるでしょう。


ところで、

変数をvolatileで宣言しても、インクリメント処理はアトミックにならないため、

複数スレッドからインクリメント処理を行なうと、値が壊れる可能性があります。

(これはlongだけでなく、shortやintなどでも発生します)

private volatile static int intValue = 0;

// 複数スレッドから実行すると値を更新しないことがある。
public void increment() {
	intValue++;
}

そのような場合でも、

AtomicIntegerやAtomicLongのincrementAndGetメソッドを使うことで

インクリメント処理をアトミックに行なうことができるのです。

volatileの使いどころ

そんなわけで使いにくいvolatileですが、

ステートフラグとか、ダブルチェックロッキングとかでは使えますよね!

まとめ

  • 変数への代入が、いつもアトミックだと過信してはいけない。
  • volatile! volatile!
  • 普通にAtomicIntegerやAtomicLongを使ってください。

2011-10-04 Day 2 - Java for iOS!? と Twitter

[]JRockit Mission ControlはVisualVMの凄いヤツ

What is New in Mission Control

どうやらMacで動くようになるそうです。


HotRockitのエントリーでも書きましたが、僕はJRockitの方が好きでして、

そんなJRockitの一番の売り、JRockit Mission Control(以下、JRMC)のセッションです。


JRMCをご存知ない方は、VisualVMの凄い版だと思ってください。

VisualVMもご存知ない方は、このエントリーを読み飛ばしてください。

え? Mission ControlがMac対応?

バージョン4.1から、JRMCがMacで動作するようになるそうです。

一瞬、JRockit for Macの登場かと思いましたが、

どうやらJRMCがMac版のJavaでも動くようになるようですね。


何を言っているかというと・・・

これまで、JRMCを動かすためには、

解析対象(サーバ側)がJRockitで動く必要があったのはもちろんの事、

JRMC(クライアント側)自体もJRockitで実行する必要がありました。


それがバージョン4.1からは、JRMCクライアントはHotspotで動くようになり、

Mac版のJava上でも動かせるようになった、という事のようです。


でもやっぱり、Mac版のJavaで動くアプリをJRMCで解析したいですよね?

それはHotRockit登場まで、お預けです。


ちなみにそれ以外には、

Nativeメモリも見えるようになったとか、

スレッドごとのCPU使用率、メモリ使用量などが見えるようになったとか、

DTraceプラグインも出るぞとか、

そんな些細な(?)アップデートが紹介されていました。

Flight Recorderは地味めのアップデート

続いてJRockit Flight Recorder (JRFR) のアップデート。

一応説明しておくと、JRFCというのはJRockitに組み込まれている

プロファイラ、あるいはプローブのようなもので、

JVMの状態を延々と取り続けるというツールです。


ただ私自身、元のバージョンを詳しく知っているわけではなく

アップデートと言われても、あんまりピンと来ないのですが。


JRFR 4.1の売りは、JRMCと連携したGUIビルダー。

自分の好きなように解析画面をカスタマイズできたり、

それをエクスポートできるようになる、という所が売りのようです。


とまぁそんなアップデートよりも、

Flight Recorderはこんなに凄いんだぜ、というセッションを

小一時間やって欲しいのですけどねぇ。

最後に、Java Mission Control 5.0

HotRockitの所で、「JRockitほげほげ」は「Javaほげほげ」に変わると書きましたが

JRMCの次期バージョンは、Java Mission Control 5.0になる予定のようです。


JMC5はHotSpot版のみが提供され(JRockitは4.xのみ)

Flight Recorderも使える状態でリリースされるようです。


この辺り、HotRockitの進捗次第で大きくスケジュールが変わる所でしょうから

今はこれぐらいのビジョン・・・だと思っておいた方が良いでしょう。