Java SE 8とJMHを使ってインスタンス生成の性能を計測した
megascusさんの「Java SE 8時点ではリフレクションを使ったインスタンス生成はほぼ問題にならない程度に早いらしい」を見かけたので、もがみもJava SE 8(8u40)でマイクロベンチマークのフレームワークJMHを使って測定してみました。
コード
元の記事ではStringだけ測定してましたが、Stringは内部的にchar[]やハッシュコードを事前に計算したintなどのフィールドが初期化されます。これを除外するため、フィールド無しのTooSimpleClassも定義して比較してみました。
またStringの場合、普通はコンストラクタを使わずにリテラルを使うと思うので、そちらも計測。リテラルだと、プールされたインスタンスがよしなに使われるので、高速になるはず。Stringプールに対して、TooSimpleClassではあらかじめ生成したインスタンス(要はシングルトンみたいなものですね)取得を比較します。
ウォームアップは50回、実際の測定は50回行って平均スループットを算出しています。
package net.exoego;
import java.io.IOException;
import org.openjdk.jmh.Main;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.runner.RunnerException;
public class NewInstanceBench {
public static void main(final String... args) throws ReflectiveOperationException, IOException, RunnerException {
Main.main(new String[]{"-i", "50", "-wi", "50", "-f", "5"});
}
public static class TooSimpleClass {
public static final TooSimpleClass INSTANCE = new TooSimpleClass();
}
private static final Class<String> STRING_CLASS = String.class;
private static final Class<TooSimpleClass> SIMPLE_CLASS = TooSimpleClass.class;
@Benchmark
public static void stringPool() {
String s = "";
}
@Benchmark
public static void stringConstructor() {
String s = new String();
}
@Benchmark
public static void stringReflectionCached() throws ReflectiveOperationException {
String s = STRING_CLASS.newInstance();
}
@Benchmark
public static void stringReflectionNotCached() throws ReflectiveOperationException {
String s = (String) Class.forName("java.lang.String").newInstance();
}
@Benchmark
public static void simpleSingleton() {
final TooSimpleClass instance = TooSimpleClass.INSTANCE;
}
@Benchmark
public static void simpleConstructor() {
TooSimpleClass s = new TooSimpleClass();
}
@Benchmark
public static void simpleReflectionCached() throws ReflectiveOperationException {
TooSimpleClass s = SIMPLE_CLASS.newInstance();
}
@Benchmark
public static void simpleReflectionNotCached() throws ReflectiveOperationException {
TooSimpleClass s = (TooSimpleClass) Class.forName("net.exoego.NewInstanceBench$TooSimpleClass").newInstance();
}
}
実際はmainメソッドは使わず、作ったjarファイルをコマンドラインで実行しました。
JMHのセットアップとかをやってくれるpom.xmlはこちら。
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.exoego</groupId>
<version>0.1-SNAPSHOT</version>
<artifactId>constructor-benchmark</artifactId>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jmh.version>1.6.3</jmh.version>
<javac.target>1.8</javac.target>
<uberjar.name>benchmarks</uberjar.name>
</properties>
<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<compilerVersion>${javac.target}</compilerVersion>
<source>${javac.target}</source>
<target>${javac.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.2</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${uberjar.name}</finalName>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
測定結果
スループット(1秒間に実行できる回数)はこちら。値が大きいほど高速です。
| TooSimpleClass | String | ||
|---|---|---|---|
| instance pool | 3,124,042,755 times/sec | 3,126,290,518 times/sec | |
| constructor | 3,198,891,172 times/sec | 322,034,032 times/sec | |
| klass.newInstance(cached) | 148,542,913 times/sec | 94,572,902 times/sec | |
| klass.newInstance | 1,071,358 times/sec | 1,859,832 times/sec |
チャートにするとこちら。

スループットを逆にして、1実行あたりの処理時間(ナノ秒)はこちら。値が小さいほど高速です。
| TooSimpleClass | String | ||
|---|---|---|---|
| instance pool | 0.32 ns | 0.32 ns | |
| constructor | 0.31 ns | 3.11 ns | |
| klass.newInstance(cached) | 6.73 ns | 10.57 ns | |
| klass.newInstance | 933.39 ns | 537.68 |
StringリテラルでのStringプールからの取得は、1秒で31億回実行できています。これに対し、Stringコンストラクタは約10分の1の3億2千万回です。StringのコンストラクタでなくStringリテラルを使うべき理由は、こういう性能向上なんですね。
通常のコンストラクタを見ると、new String()はnew TooSimpleClass()の約10倍遅かったです。フィールドがちょっと増えただけで、こんなにも違うとは(といっても0.31ナノ秒が3.11ナノ秒に増えた程度ですが)。
今回の測定ではキャッシュされたClassリテラルでのClass#newInstance()呼出は、new String()より3.4倍遅くなりました(元記事では2倍)。実際のクラスではコンストラクタ自体の実行にかかる時間(引数のバリデーションやフィールドの初期化など)がもっと増えてくると思いますので、そこまで目くじらを立てる程の開きではありませんね。
キャッシュしてない場合は2985倍遅くなりましたが、元記事では200倍なので、こちらはかなり開きがあります。JMHでウォームアップを増やし、測定回数も多くしているので正確になってると思いますが、こんなに開くものなのかな。