Hello World! コンテンツ・メディア第1事業部の、jyukutyoこと阪田です。JavaOneではランチボックスが配られるのですが、聞いていたほどおいしくないという感じではなかったです。味はイマイチだけど食べたくないほどではないな、という感じでした。外で食べるとなると確実に$20は超えてしまうので、私はほぼこのランチボックスを食べていました。
私はランチボックスの写真を撮っていなかったので、JavaOneのランチボックス写真をflickrからお借りしました(さくらばさんありがとうございます)。 2014年のものですが、2015年でも同じ感じでした。
Protecting Java Bytecode from Hackers with the InvokeDynamic Instruction
Javaのバイトコードは高レベルのJVM命令セットです。スタック指向であり、命令フォーマットがあります。
ラムダ式を使った以下のコードがあるとします。
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello JavaOne 2015");
r.run();
}
これはバイトコードでは次のように表現されます。
0: invokedynamic #2,0 // InvokeDynamic #0:run:()Ljava/lang/Runnable;
5: astore_1
6: aload_1
7: invokeinterface #3, 1 // InterfaceMethod java/lang/Runnable.run:()V
12: return
Procyon(https://bitbucket.org/mstrobel/procyon/wiki/Java%20Decompiler)というでコンパイラを使って起動してみます。$java –jar procyon.jar HelloJavaOneWithLamdas.class
出力は次のようになります。
public static void main(String[] args) {
Runnable r = () -> System.out.println("Hello JavaOne 2015");
r.run();
}
Javaのバイトコードへの攻撃としてはリバースエンジニアリングや、重要なルーティンへのバイパス、デコンパイルと変更が容易であることなどです。有名なJavaのデコンパイラは次にようなものです。Procyon + LuytenやCFR、JD、Fernflower、Krakatau、Candleです。 Javaのバイトコード保護としては、コードフロー難読化や名前の難読化、呼び出しの隠蔽などです。ここでObfuscatorという製品をデモします。
次にコンスタントプールとバイトコードについて考えます。コンスタントプールはすべてのシンボル情報を含んでいます。invoke*という命令はメソッド呼び出しに使われます。JVMはメソッド実行の前にスタックが矛盾しないことを要求します。
リフレクションを使って呼び出しを隠蔽することができます。
// Hide Fieldref #2
Object out = Class.forName("java.lang.System").getField("out").get(null);
// Hide Methodref #4
Class.forName("java.io.PrintStream").getMethod("println",new Class[]{String.class}).invoke(System.out, new Object[]{"Hello JavaOne 2015"})
そうすると、メリットとしてコンスタントプールからMethodRefとFieldRefを取り除けます。デメリットとしてはパフォーマンス、バイトコードサイズのオーバーヘッド、引数や戻り値でボクシング/アンボクシングが必要となること、スーパークラスのメソッドを呼び出せないこと、プライベートメソッドやフィールドへアクセスするときにセキュリティ違反になること、などがあります。
そこで、JSR-292のInvokeDynamicです。Java 7から導入されています。InvokeDynamicを使って呼び出しを隠蔽します。プランとしては、ブートストラップメソッドを生成し、invokevirtual/invokeinterface/invokestaticの呼び出しをinvokedynamicに書き換えます。それにはASMを使います。ASMは低レベルのバイトコード操作のためのライブラリです。SAXのようなVisitor APIとDOMのようなTree APIがあります。ASMでHello Worldすると次のようになります。
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
mv.visitCode();
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitLdcInsn("Hello JavaOne 2015");
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V");
mv.visitInsn(RETURN);
mv.visitMaxs(2, 1);
mv.visitEnd();
ソースコードを読み込み、ASMを使ったコードに書き換えるわけです。ブートストラップを生成するコードはこのようになります。
String bootstrapMethodName = "bootstrap$0";
String bootstrapMethodSignature = "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;ILjava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;";
MethodVisitor mv = cv.visitMethod(ACC_PRIVATE + ACC_STATIC, bootstrapMethodName, bootstrapSignature, null, null, true, true);
mv.visitCode();
Label l0 = new Label();
Label l1 = new Label();
mv.visitTryCatchBlock(l0, l1, l0, "java/lang/Exception");
mv.visitInsn(ICONST_0);
mv.visitVarInsn(ISTORE, 13);
mv.visitInsn(ACONST_NULL);
次に、invoke*の置き換えですが、MethodVisitorを実装します。
class MethodIndyProtector extends MethodVisitor implements Opcodes {
Handle bootsrapMethodHandle = null;
public MethodIndyProtector(MethodVisitor mv, String className) {
super(ASM4, mv);
bootsrapMethodHandle = new Handle(Opcodes.H_INVOKESTATIC, className, bootstrapMethodName, bootstrapMethodSignature);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc) {
// replace invokestatice/invokevirtual, invokeinterface instructions
}
}
visitMethodInsn()メソッドは次のような実装となります。
@Override
public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf ) {
// generate a newName and a generic newSig (String.class->Object.class, ..)
// …
switch(opcode){
case INVOKESTATIC:
case INVOKEVIRTUAL:
case INVOKESINTERFACE:
mv.visitInvokeDynamicInsn(newName, newSig, bootsrapMethodHandle, opcode, owner, name, desc);
default:
mv.visitMethodInsn(opcode, owner, name, desc, itf);
}
}
結果として、たとえばJDででコンパイルするとエラーになります。他のデコンパイラでも読めません。結論としては、invoke*命令はすべて保護できました。さらにパフォーマンスのインパクトはありません。JVMがInvokeDynamicを最適化してくれます。
Having Fun with Javassist
JRebleなどを作っているZEROTURNAROUNDの方のセッション。 XRebelやJRebelでJavassistを多く活用しています。バイトコード操作はいたるところで行われています。Hibernateではプロキシ生成に使われています。主要なユースケースとして、バイトコード操作はJavaのフレームワークにおいてプロキシを生成するために使われています、がその内容だと退屈ですね。 JavassistはリフレクションAPIのような感じです。ClassPoolにCtClassがあり、CtClassにはCtFieldやCtMethod、CtConstがあります。CtMethodにはinsertBefore、insertAfter、instrumentがあります。
public static void main(String[] args) throws Exception {
ClassPool cp = ClassPool.getDefault();
CtClass ct = cp.makeClass("com.zt.A", cp.get("com.zt.Clazz"));
CtMethod[] methods = ct.getMethods();
for(CtMethod method : methods) {
//...
}
ct.writeFile("/output");
}
ビルド時にメタデータからクラスを生成できます。またはコンパイルしたクラスに対してあとから処理することができます。
CtMethod foo = ctClass.getMethod("foo", "()V");
CtMethod foo = ctClass.getMethod("foo", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V");
foo.insertBefore("System.out.println($1)");
// $1, $2, $3 — local variables
// $0 — this for non‐static methods
Class c = ctClass.toClass();
A a = (A) c.newInstance();
a.foo("Hello");
こうして、トレーシングを実装したりログを追加したり、AOPを実装したりできます。不要な呼び出しを削除できます。フィールドに直接アクセスしていたところをsetter呼び出しに変えることができます。
次にJava Agentについて。$java –javaagent:agent.jar application.Main
でAgentをつけて実行できます。Agentの実装はこうなります。
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
public class Agent {
public static void premain(String args, Instrumentation inst) throws Exception {
inst.addTransformer(new ClassFileTransformer {
// here be code
});
}
}
さらに、META‐INF/MANIFEST.MFにこう書きます。
Premain-Class: Agent
ClassFileTransformerの実装としては、たとえばこんな感じです。
new ClassFileTransformer() {
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer){
ClassPool cp = ClassPool.getDefault();
CtClass ct = cp.makeClass(new ByteArrayInputStream(classfileBuffer));
// here we can do all the things to ‘ct’
return ct.toBytecode();
}
}
JRebleプラグインでもこの仕組みを使っています。JRebleプラグインでは、JRebleのコアとSpringやHibernate、EJBといった個別のプラグインがあります。jrebel.jarはすべてのプラグインを含んでいます。クラスをリロードすると、JRebleコアが各プラグインに通知します。そして、実際に設定が更新されたりします。Javassistは各プラグインで使っています。
Beyond top: Command-Line Monitoring on the JVM
サーバがたびたび遅くなる問題がありました。チームはアプリケーションサーバを手動で再起動していました。レスポンスタイムが5分ほどかかることがあありました。何かが起こっています。
事実に基づいてみると、すべてのリクエストが影響を受けていました。JVMは落ちていませんでした。JVMを再起動するとすべてがうまくいきました。何ができるのでしょう?さらなる事実として、フルGCが頻発していました。
ヒープに何があるか、どのアプリケーションコードを実行しているかを確認するために、いいツールがあります。まずvmstat。システムレベルでCPUやメモリ、ディスク、コンテキストスイッチが見れます。topも。プロセスごとのCPUとメモリが見れます。jpsでPIDがわかります。jstackでスレッドの状態がわかります。jcmdは何でもできます。jstatでGCやクラスローダ、コンパイラがわかります。
これで謎は解けます。たとえば、リークを除去する、キャッシュを除去する、機能を削除する、全文検索エンジン…”遅い”には意味がたくさんあります。”CPU負荷が高い”には意味がたくさんあります。データを集めることは危機的状況において極めて重要です。
ほかの役立つツールとしては、ヒープアナライザやプロファイラ、絶え間ないモニタリングやアラート、動的なトレースなどがあります。
Java Community Keynote
Javaコミュニティ(JUG)によるキーノート。これだけ別の大きな会場(Marriott Marquis)でした。
今回は、Java仕様に関わった人やJUGリーダなどが寸劇をするという衝撃的なものでした。ストーリは、JavaのマスコットであるDukeが、未来の世界で歯が生えて(!?)暴れまわっているため、世界を救うために過去にタイムスリップしながら破滅を避けるキーワードを探す、という内容でした。
キーノートのMCはStephen Chinさんで、去年私もJUGリーダとして少し話したことがあり親近感がありました。劇ではジェームズ・ゴスリン本人が登場したり、10年前くらいの世界で「Project Jigsawは完成しているんだろ!?(実際はJava 9なのでまだ)」という少し皮肉ったセリフが出たりと、とても楽しいものでした。
キーワード自体は”Kids are future(未来は子どもたちである)”というものでした。技術的な内容はまったくなく、純粋にJavaのお祭りを楽しむ、そういうセッションでした。
The Rise of 1.0: How Reactive Streams and Akka Streams Change the JVM Ecosystem
リアクティブ・ストリームのお話でした。リアクティブ・ストリームは.NET 3.5で導入されたのが最初です。その後、PlayフレームワークがIterateesを導入しました。AkkaはAkka-IOを、BenがRxJavaを開始しました。ただ2013年時点ではこの3つには違いがありました。Iterateesはpull back-pressureであり、APIが複雑です。Akka-IOはNACK back-pressureであり、低レベルIO、メッセージングAPIです。RxJavaはno back-pressureでいいAPIです。そこでエキスパートグループが創設されました。ゴールは、非同期で、ブロックせず、安全で、純粋に局所抽象化され、同期型も許容するということです。リアクティブ・ストリームには仕様とTCKがあります。バージョン1.0は2015/04/28にリリースされました。
なぜback-pressureなのか?負荷でクラッシュしないようにするためです。back-pressureとは何か? Publisher-Subscriberモデルで考えます。まず、Publisherのpushが早くてSubscriberが遅い場合、Subscriberにはバッファできる何かが必要です。でももしバッファがあふれたら?Subscriberはメッセージを捨てて、Publisherはメッセージを再送する必要があります。カーネルやルータはこうしています。メモリがあるときはバッファを増やせますが、いずれOutOfMemoryになります。Negative ACKnowledgementではバッファオーバーフローは差し迫ったものです。Publisherにペースを落とすか送信を停止してもらわなければなりません。しかしNACKではそれは間に合いません。そうしている間にもメッセージが来るからです。Publisherの速度よりSubscriberの速度が上回っているとき、back-pressureは必要ありません。
リアクティブ・ストリームは動的なPush/Pullです。pushだけでは遅いSubscriberのときに効果がなく、pullだけでは早いSubdcriberのときには遅すぎるのです。解決策としては動的な調整です。遅いSubscriberでは、3つの要素だけバッファに取ります。Publisherはたとえもっと多く遅れる場合でも多くて3つだけにします。これがpull-based-backpressureです。早いSubscriberはもっと要求します。Publisherは要求をためておきます。Publisherは各Subscriberの総要求数をためておき、オーバーフローしないように送ります。
Inter OPでは、異なる実装でもお互いにうまくやれるようにします。RxJavaとAkkaなど。リアクティブ・ストリームがプロトコルとなります。リアクティブ・ストリームはデイリーユースのエンドユーザAPIではありません。これはSPIです。Service Provider Interfaceです。SPIはサードパーティによって実装または拡張されることを意図したAPIです。
Play 2.5はAkka Streamsを使います。ScalaでもJavaでもDSLで同じパワーを得られます。Akka 2.4は2.3とバイナリ互換です。
カニパーティー
これはJavaOneとは直接関係がありませんが、JavaOneの日本人参加者で最終日の夜にカニを食べに行くというイベントが毎年あります。30名以上の方が来られていました。
カニはダンジネスクラブというサンフランシスコでは有名なカニです。ガーリックソースでローストしており、日本では味わえないおいしさです。手はドロドロになりますので、汚れてもいい服の方がよいです。
話は自然とJavaのこと、いろいろな方と知り合うことができました!
JavaOne全体をふりかえって
最高でした!学べる内容としては、サンフランシスコまで行かなくてもプレゼンテーションの動画を見ても同じです。ただ、刺激という面では、やはり現地で受ける刺激はディスプレイの前で動画を見るよりも何十倍、何百倍も多いです。
英語のセッションということで心配になる方も多いかと思います。ふだん英語の技術文書を読まれている方なら、リスニングに不安があっても大丈夫です!スライドが補助となり、セッションが全部理解できないということはありません。迷うより飛び込んでしまうのが楽です!
私は今年もJavaOneに参加しようと計画しています。2016年のJavaOneは9/18〜22です。日本はちょうどシルバーウィークとなり、2日休暇を取るだけでJavaOneに参加できます!ぜひいっしょにJavaOneに参加しましょう!