"Hello World!" を出す Java のクラスファイルってどれくらい小さくなるんだろう?
まずは慣れてる Scala で。
object S {
def main(args: Array[String]) {
println("Hello World!");
}
}
scalac でコンパイルして
rw-r--r-- 1 kawachi staff 571 Mar 3 23:02 S$.class
-rw-r--r-- 1 kawachi staff 530 Mar 3 23:02 S.class
571 + 530 = 1101 bytes.
javap で中身を確認。
$ javap -c S.class
Compiled from "A.scala"
public final class S {
public static void main(java.lang.String[]);
Code:
0: getstatic #16 // Field S$.MODULE$:LS$;
3: aload_0
4: invokevirtual #18 // Method S$.main:([Ljava/lang/String;)V
7: return
}
$ javap -c S$.class
Compiled from "A.scala"
public final class S$ {
public static final S$ MODULE$;
public static {};
Code:
0: new #2 // class S$
3: invokespecial #12 // Method "<init>":()V
6: return
public void main(java.lang.String[]);
Code:
0: getstatic #19 // Field scala/Predef$.MODULE$:Lscala/Predef$;
3: ldc #21 // String Hello World!
5: invokevirtual #25 // Method scala/Predef$.println:(Ljava/lang/Object;)V
8: return
}
S.main から S$.main を呼んでる様子。無駄が多い。
つづいて Java 最初から Java でやれという感がある。
class J {
public static void main(String args[]) {
System.out.println("Hello World!");
}
}
javac すると
-rw-r--r-- 1 kawachi staff 408 Mar 3 23:07 J.class
408 bytes. Scala の半分以下だ。
javap してみる。
$ javap -c J.class
Compiled from "A.java"
class J {
J();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello World!
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
Hello World を印字するだけなら J();
の部分は要らないのではないか?
.class public M
.super java/lang/Object
.method public static main([Ljava/lang/String;)V
.limit stack 2
getstatic java/lang/System/out Ljava/io/PrintStream;
ldc "Hello World!"
invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V
return
.end method
とりあえず main だけあればいいよね。
コンパイルしてサイズは
-rw-r--r-- 1 kawachi staff 303 Mar 3 23:13 M.class
303 bytes.
javap して
$ javap -c M.class
Compiled from "M.j"
public class M {
public static void main(java.lang.String[]);
Code:
0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String Hello World!
5: invokevirtual #19 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
Jasmine で生成されたクラスファイルを見てみる。
$ hexdump -C M.class
00000000 ca fe ba be 00 03 00 2d 00 18 0c 00 14 00 17 01 |.......-........|
00000010 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 |..([Ljava/lang/S|
00000020 74 72 69 6e 67 3b 29 56 01 00 10 6a 61 76 61 2f |tring;)V...java/|
00000030 6c 61 6e 67 2f 4f 62 6a 65 63 74 07 00 03 01 00 |lang/Object.....|
00000040 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 07 00 0f |.Hello World!...|
00000050 08 00 05 07 00 12 01 00 04 43 6f 64 65 01 00 04 |.........Code...|
00000060 6d 61 69 6e 09 00 08 00 01 01 00 01 4d 01 00 0a |main........M...|
00000070 53 6f 75 72 63 65 46 69 6c 65 0c 00 10 00 15 01 |SourceFile......|
00000080 00 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 |..java/io/PrintS|
00000090 74 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 07 |tream...println.|
000000a0 00 0c 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 |.....java/lang/S|
000000b0 79 73 74 65 6d 0a 00 06 00 0e 01 00 03 6f 75 74 |ystem........out|
000000c0 01 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 |...(Ljava/lang/S|
000000d0 74 72 69 6e 67 3b 29 56 01 00 03 4d 2e 6a 01 00 |tring;)V...M.j..|
000000e0 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 |.Ljava/io/PrintS|
000000f0 74 72 65 61 6d 3b 00 21 00 11 00 04 00 00 00 00 |tream;.!........|
00000100 00 01 00 09 00 0a 00 02 00 01 00 09 00 00 00 15 |................|
00000110 00 02 00 01 00 00 00 09 b2 00 0b 12 07 b6 00 13 |................|
00000120 b1 00 00 00 00 00 01 00 0d 00 00 00 02 00 16 |...............|
0000012f
- 末尾の
00 0d 00 00 00 02 00 16
は SourceFile 属性を表しているので削れそう。- constant pool の "SourceFile" (0x0d), "M.j" (0x16) も削れる。
- クラス名を M としていたが、 Code という名前にすれば Code 属性で利用している constant pool が使いまわせる。
目視で 1 byte ずつ丁寧に書き換えたのがこれ。 constant pool の index が変わると変更箇所がいろいろ出てきてめんどい。
$ hexdump -C Code.class
00000000 ca fe ba be 00 03 00 2d 00 15 0c 00 12 00 14 01 |.......-........|
00000010 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 |..([Ljava/lang/S|
00000020 74 72 69 6e 67 3b 29 56 01 00 10 6a 61 76 61 2f |tring;)V...java/|
00000030 6c 61 6e 67 2f 4f 62 6a 65 63 74 07 00 03 01 00 |lang/Object.....|
00000040 0c 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 07 00 0d |.Hello World!...|
00000050 08 00 05 07 00 10 01 00 04 43 6f 64 65 01 00 04 |.........Code...|
00000060 6d 61 69 6e 09 00 08 00 01 0c 00 0e 00 13 01 00 |main............|
00000070 13 6a 61 76 61 2f 69 6f 2f 50 72 69 6e 74 53 74 |.java/io/PrintSt|
00000080 72 65 61 6d 01 00 07 70 72 69 6e 74 6c 6e 07 00 |ream...println..|
00000090 09 01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 53 79 |....java/lang/Sy|
000000a0 73 74 65 6d 0a 00 06 00 0c 01 00 03 6f 75 74 01 |stem........out.|
000000b0 00 15 28 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 |..(Ljava/lang/St|
000000c0 72 69 6e 67 3b 29 56 01 00 15 4c 6a 61 76 61 2f |ring;)V...Ljava/|
000000d0 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 3b 00 |io/PrintStream;.|
000000e0 21 00 0f 00 04 00 00 00 00 00 01 00 09 00 0a 00 |!...............|
000000f0 02 00 01 00 09 00 00 00 15 00 02 00 01 00 00 00 |................|
00000100 09 b2 00 0b 12 07 b6 00 11 b1 00 00 00 00 00 00 |................|
00000110
272 bytes まで減った。 実行もできる。
$ java Code
Hello World!
$ javap -c Code.class
public class Code {
public static void main(java.lang.String[]);
Code:
0: getstatic #11 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #7 // String Hello World!
5: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
親クラスで参照している java.lang.Object
が無駄(文字列的に)という示唆をいただいた。
https://twitter.com/kazzna/status/573409436124905472
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1 の super_class をみると
For a class, the value of the super_class item either must be zero or must be a valid index into the constant_pool table.
とかいており、 0x00 にしておけば java.lang.Object を constant pool から削れて短くなるのでは!?と思ったが
Exception in thread "main" java.lang.ClassFormatError: Invalid superclass index
0 in class file Code
というエラーに。。。 よく読んだら
If the value of the super_class item is zero, then this class file must represent the class Object, ...
と書いてあった。
というわけで、既に constant pool に存在している PrintStream を super_class に変更して、 java/lang/Object の Utf8_info と Class_info を削除。 バイナリエディタで丁寧に削っていく。
$ hexdump -C Code.class
00000000 ca fe ba be 00 03 00 2d 00 13 0c 00 10 00 12 01 |.......-........|
00000010 00 16 28 5b 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 |..([Ljava/lang/S|
00000020 74 72 69 6e 67 3b 29 56 01 00 0c 48 65 6c 6c 6f |tring;)V...Hello|
00000030 20 57 6f 72 6c 64 21 07 00 0b 08 00 03 07 00 0e | World!.........|
00000040 01 00 04 43 6f 64 65 01 00 04 6d 61 69 6e 09 00 |...Code...main..|
00000050 06 00 01 0c 00 0c 00 11 01 00 13 6a 61 76 61 2f |...........java/|
00000060 69 6f 2f 50 72 69 6e 74 53 74 72 65 61 6d 01 00 |io/PrintStream..|
00000070 07 70 72 69 6e 74 6c 6e 07 00 07 01 00 10 6a 61 |.println......ja|
00000080 76 61 2f 6c 61 6e 67 2f 53 79 73 74 65 6d 0a 00 |va/lang/System..|
00000090 04 00 0a 01 00 03 6f 75 74 01 00 15 28 4c 6a 61 |......out...(Lja|
000000a0 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b 29 |va/lang/String;)|
000000b0 56 01 00 15 4c 6a 61 76 61 2f 69 6f 2f 50 72 69 |V...Ljava/io/Pri|
000000c0 6e 74 53 74 72 65 61 6d 3b 00 21 00 0d 00 04 00 |ntStream;.!.....|
000000d0 00 00 00 00 01 00 09 00 08 00 02 00 01 00 07 00 |................|
000000e0 00 00 15 00 02 00 01 00 00 00 09 b2 00 09 12 05 |................|
000000f0 b6 00 0f b1 00 00 00 00 00 00 |..........|
000000fa
$ java Code
Hello World!
$ javap -c Code
public class Code extends java.io.PrintStream {
public static void main(java.lang.String[]);
Code:
0: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String Hello World!
5: invokevirtual #15 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
}
$ ls -l Code.class
-rw-r--r-- 1 kawachi staff 250 Mar 5 19:02 Code.class
250 bytes まできた!
https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.10 SourceFile attribute は削れそう。