Blog

Ist die Escape-Analyse aus Java 6 entkommen?

Jeroen Borgers

Aktualisiert Oktober 23, 2025
7 Minuten

Escape Analysis in Java 6? Letzten Monat haben wir unseren Kurs Speeding up Java Applications in den holländischen Wäldern abgehalten. Bei der Vorbereitung habe ich einige der neuen Themen mit meinem Kollegen und Kursleiter Kirk Pepperdine besprochen. Wir erläutern die neuen Funktionen von Java 6 und wie sie zur Verbesserung Ihrer Leistung beitragen können. Eine der ausgefeilteren Funktionen auf VM-Ebene nennt sich Escape-Analyse. Die Frage ist: Funktioniert sie wirklich? Wir erzählen von der Fluchtanalyse nicht nur im Kurs, sondern auch in unserem Performance Top-10 Blog und Podcast sowie in meiner J-Fall Präsentation. Brian Goetz schreibt im September 2005: "Die Escape-Analyse ist eine Optimierung, über die schon lange gesprochen wird, und sie ist endlich da - die aktuellen Builds von Mustang (Java SE 6) können die Escape-Analyse durchführen ..." Außerdem heißt es in Wikipedia: "Die Escape-Analyse ist in Java Standard Edition 6 implementiert." Und mehrere JVM-Schalter für die Escape-Analyse, wie -XX:-DoEscapeAnalysis, sind verfügbar. Wir können also davon ausgehen, dass es funktioniert, oder? Aber lassen Sie uns hier keine Vermutungen anstellen, denn Vermutungen sind die Mutter aller F***-Ups. Und es stellt sich heraus, wie wir sehen werden: Es funktioniert nicht! Wir müssen messen, nicht Vermutungen anstellen. Ich habe ein Interview mit dem Java-Spezialisten Heinz Kabutz gelesen, in dem er tatsächlich misst. Er testet verschiedene Möglichkeiten der String-Verkettung. Er verwendet den thread-sicheren StringBuffer und den thread-unsicheren StringBuilder, wobei sich letzterer unter Java 6 als deutlich schneller erweist als ersterer. Er spricht nicht über die Escape-Analyse, aber wenn die Escape-Analyse richtig funktioniert, wäre die Verwendung von StringBuffer genauso schnell wie die Verwendung von StringBuilder, wie wir in unserem Top 10 Blog behaupten! Die Escape-Analyse funktioniert hier also nicht. Ich werde Ihnen erklären, was los ist. Escape-Analyse erklärt Diese vom Laufzeit-Compiler durchgeführte Analyse kann z.B. zu dem Schluss kommen, dass ein Objekt auf dem Heap nur lokal in einer Methode referenziert wird und keine Referenz aus diesem Bereich entkommen kann. Wenn dies der Fall ist, kann Hotspot Laufzeitoptimierungen anwenden. Es kann das Objekt auf dem Stack oder in Registern statt auf dem Heap allozieren. Oder es kann das Erwerben und Freigeben von Sperren für das Objekt ganz abschaffen (Lock Elision), da kein anderer Thread auf das Objekt zugreifen kann. Beispiel:

public String concatBuffer(String s1, String s2, String s3) {
  StringPuffer sb = new StringPuffer();
  sb.append(s1);
  sb.append(s2);
  sb.append(s3);
  return sb.toString();
}

Hier wird das Ergebnis sb nur lokal in der Methode verwendet und kein Verweis kann aus diesem Bereich entweichen. Es ist ein Kandidat für Stack-Allokation und Lock Elision. Mit Lock Elision haben Sie bei der Verwendung von Objekten, die thread-sicher sind, aber nicht im verwendeten Kontext sein müssen, nicht den Overhead, thread-sicher zu sein. Dies ist also wie "no cure - no pay": keine Threads zum Anhalten - kein Overhead. Der VM-Schalter zur Aktivierung dieser Funktion lautet: -XX:+DoEscapeAnalysis. Biased locking erklärt Biased locking ist eine weitere Threading-Optimierung in Java 6. Da die meisten Objekte während ihrer Lebensdauer von höchstens einem Thread gesperrt werden, ist es sinnvoll, für diesen Fall zu optimieren. Das ist es, was Biased Locking bewirkt. Es erlaubt diesem Thread, ein Objekt für sich selbst zu sperren. Sobald er das Objekt vorgespannt hat, kann er es anschließend sperren und entsperren, ohne auf teure atomare Anweisungen zurückgreifen zu müssen. Der VM-Schalter lautet: -XX:+UseBiasedLocking und ist standardmäßig aktiviert. Vergröberung von Sperren erklärt Eine weitere Threading-Optimierung ist die Vergröberung oder Zusammenführung von Sperren. Benachbarte synchronisierte Blöcke werden zu einem synchronisierten Block zusammengeführt oder mehrere synchronisierte Methoden werden zu einer zusammengefasst. Dies gilt nur, wenn das gleiche Sperrobjekt verwendet wird. Dadurch wird der Sperraufwand reduziert. Beispiel:

public static String concatToBuffer(StringBuffer sb, String s1, String s2, String s3) {
  sb.append(s1);
  sb.append(s2);
  sb.append(s3);
  return sb.toString();
}

In diesem Beispiel ist die StringBuffer-Sperre kein Kandidat für die Lock Elision, da sie außerhalb der Methode verwendet wird. Die dreimalige Erfassung und Freigabe der Sperre kann jedoch auf eine einzige reduziert werden, nachdem die Append-Methoden eingefügt wurden. Der VM-Schalter lautet: -XX:+EliminateLocks und ist standardmäßig aktiviert. Benchmarking - Messen, ob es funktioniert Um das, was wir predigen, in die Praxis umzusetzen, habe ich einen sperrintensiven LockTest-Benchmark erstellt, um diese drei VM-Optionen zu testen. Der Code ist unten abgebildet. Ich möchte ihn zunächst mit deaktivierten Optionen auf meinem Vista-Laptop ausführen: java -XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:-UseBiasedLocking LockTest Ich verstehe: Unrecognized VM option '-DoEscapeAnalysis' Could not create the Java virtual machine. Hmmm, seltsam. Ich bin sicher, dass dies die richtige Option ist. Nach einigem Nachforschen haben wir herausgefunden, dass dies nur für die Server-VM funktioniert, nicht aber für die Client-VM, die standardmäßig auf 32-Bit-Windows läuft. Diese VMs sind derzeit zwei separate Binärdateien. Nachdem ich mich mit meiner wertvollen Quelle des Performance-Teams bei Sun in Verbindung gesetzt hatte, erfuhr ich, dass diese Option im Gegensatz zu anderen Locking-Optimierungen nicht standardmäßig aktiviert ist. Eine weitere überraschende Information war, dass die Allokationsoptimierung (unter Verwendung der Escape-Analyse) noch nicht im JDK enthalten ist. Sie arbeiten noch daran und erwarten, dass sie mit dem JDK-Update im Frühjahr 2008 verfügbar sein wird. Das ist also enttäuschend, es gab eine 'kleine' Verzögerung seit der Aussage von Brian Goetz im Jahr 2005. Er sagte mir jedoch auch, dass die Elision von Escape-Analysen-Locks ab der letzten JDK-Version (6_03) tatsächlich verfügbar ist. Lassen Sie uns den Test machen. Hier sind meine Ergebnisse: >java -server -XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:-UseBiasedLocking LockTest StringBuffer: 6553 ms. StringBuilder: 1836 ms. Thread-Sicherheits-Overhead von StringBuffer: 256% .

java -server -XX:+DoEscapeAnalysis -XX:-EliminateLocks -XX:-UseBiasedLocking LockTest StringBuffer: 6546 ms. StringBuilder: 1872 ms. Thread-Sicherheits-Overhead von StringBuffer: 249% . java -server -XX:-DoEscapeAnalysis -XX:+EliminateLocks -XX:-UseBiasedLocking LockTest StringBuffer: 3101 ms. StringBuilder: 1836 ms. Thread-Sicherheits-Overhead von StringBuffer: 68% . java -server -XX:-DoEscapeAnalysis -XX:-EliminateLocks -XX:+UseBiasedLocking LockTest StringBuffer: 2852 ms. StringBuilder: 1855 ms. Thread-Sicherheits-Overhead von StringBuffer: 53% . java -server -XX:-DoEscapeAnalysis -XX:+EliminateLocks -XX:+UseBiasedLocking LockTest StringBuffer: 2645 ms. StringBuilder: 1823 ms. Thread-Sicherheits-Overhead von StringBuffer: 45% Schlussfolgerungen Wir sehen also deutlich, dass für diesen offensichtlichen Fall die Optimierungen der Escape-Analyse aus Java 6 entkommen sind. Sie ist für die Client-VM nicht verfügbar, sie ist standardmäßig deaktiviert und wenn Sie sie für die Server-VM aktivieren, hilft sie nicht wesentlich: der Thread-Sicherheits-Overhead bleibt hier bei etwa 250%. Idealerweise sollte der Threadsicherheits-Overhead mit der Escape-Analyse Lock Elision auf 0% sinken. Glücklicherweise sehen wir jedoch, dass Lock Coarsening und Biased Locking sehr hilfreich sind und den Overhead auf etwa 50% senken. Wir sehen bei dieser Übung erneut, dass wir Annahmen in Frage stellen, die richtigen Fragen stellen und tatsächlich messen sollten, um Beweise zu erhalten. Hoffentlich bringt das Frühjahrs-Update 2008 von Java 6 die richtigen Optimierungen für die Escape-Analyse, die schon so lange versprochen wurden! Wir werden Sie auf dem Laufenden halten. LockTest Code:

public class LockTest {
private static final int MAX = 20000000; // 20 Millionen
.
public static void main(String[] args) throws InterruptedException {
// Wärmen Sie den Methoden-Cache auf
concatBuffer("Josh", "James", "Duke");
concatBuilder("Josh", "James", "Duke");
.
System.gc();
Thread.sleep(1000);
.
long start = System.currentTimeMillis();
for (int i = 0; i  <  MAX; i++) {
concatBuffer("Josh", "James", "Duke");
}
long bufferCost = System.currentTimeMillis() - start;
System.out.println("StringPuffer: " + bufferCost + " ms.");
.
System.gc();
Thread.sleep(1000);
.
start = System.currentTimeMillis();
for (int i = 0; i  <  MAX; i++) {
concatBuilder("Josh", "James", "Duke");
}
long builderCost = System.currentTimeMillis() - start;
System.out.println("StringBuilder: " + builderCost + " ms.");
System.out.println("Thread-Sicherheits-Overhead von StringBuffer: "
+ ((bufferCost * 10000 / (builderCost * 100)) - 100) + "%n");
}
.
public static String concatBuffer(String s1, String s2, String s3) {
StringPuffer sb = new StringPuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
.
public static String concatBuilder(String s1, String s2, String s3) {
StringBuilder sb = new StringBuilder();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
}

Verfasst von

Jeroen Borgers

Contact

Let’s discuss how we can support your journey.