Bytecode-Funktionen sind in der Java-Sprache nicht verfügbar

146

Gibt es derzeit (Java 6) Dinge, die Sie in Java-Bytecode tun können, die Sie in der Java-Sprache nicht tun können?

Ich weiß, dass beide Turing vollständig sind, also lesen Sie "kann" als "kann wesentlich schneller / besser oder nur auf andere Weise".

Ich denke an zusätzliche Bytecodes wie invokedynamic, die nicht mit Java generiert werden können, außer dass dieser für eine zukünftige Version vorgesehen ist.

Bart van Heukelom
quelle
3
Definieren Sie "Dinge". Am Ende sind sowohl die Java-Sprache als auch der Java-Bytecode vollständig ...
Michael Borgwardt
2
Ist die eigentliche Frage; Gibt es einen Vorteil bei der Programmierung von Bytecode, z. B. mit Jasmin anstelle von Java?
Peter Lawrey
2
Wie rolim Assembler, den Sie in C ++ nicht schreiben können.
Martijn Courteaux
1
Es ist ein sehr schlechter Optimierungs-Compiler, der nicht (x<<n)|(x>>(32-n))zu einer rolAnweisung kompiliert werden kann .
Random832

Antworten:

62

Soweit ich weiß, gibt es in den von Java 6 unterstützten Bytecodes keine wesentlichen Funktionen, auf die nicht auch über Java-Quellcode zugegriffen werden kann. Der Hauptgrund dafür ist offensichtlich, dass der Java-Bytecode unter Berücksichtigung der Java-Sprache entwickelt wurde.

Es gibt jedoch einige Funktionen, die von modernen Java-Compilern nicht erstellt werden:

  • Die ACC_SUPERFlagge :

    Dies ist ein Flag, das für eine Klasse gesetzt werden kann und angibt, wie ein bestimmter Eckfall des invokespecialBytecodes für diese Klasse behandelt wird. Es wird von allen modernen Java-Compilern festgelegt (wobei "modern"> = Java 1.1 ist, wenn ich mich richtig erinnere), und nur alte Java-Compiler haben Klassendateien erstellt, bei denen dies nicht festgelegt wurde. Dieses Flag existiert nur aus Gründen der Abwärtskompatibilität. Beachten Sie, dass ACC_SUPER ab Java 7u51 aus Sicherheitsgründen vollständig ignoriert wird.

  • Die jsr/ retBytecodes.

    Diese Bytecodes wurden verwendet, um Unterroutinen zu implementieren (hauptsächlich zum Implementieren von finallyBlöcken). Sie werden seit Java 6 nicht mehr produziert . Der Grund für ihre Ablehnung ist, dass sie die statische Überprüfung ohne großen Gewinn erheblich erschweren (dh Code, der verwendet wird, kann fast immer mit normalen Sprüngen mit sehr geringem Overhead neu implementiert werden).

  • Zwei Methoden in einer Klasse, die sich nur im Rückgabetyp unterscheiden.

    Die Java-Sprachspezifikation erlaubt nicht zwei Methoden in derselben Klasse, wenn sie sich nur in ihrem Rückgabetyp unterscheiden (dh gleichen Namen, gleiche Argumentliste, ...). Die JVM - Spezifikation hat jedoch keine solche Beschränkung, so dass eine Klassendatei kann zwei solche Methoden enthalten, gibt es einfach keine Möglichkeit , eine solche Klasse - Datei mit dem normalen Java - Compiler zu erzeugen. Diese Antwort enthält ein schönes Beispiel / eine Erklärung .

Joachim Sauer
quelle
5
Ich könnte eine andere Antwort hinzufügen, aber wir könnten genauso gut Ihre zur kanonischen Antwort machen. Möglicherweise möchten Sie erwähnen, dass die Signatur einer Methode im Bytecode den Rückgabetyp enthält . Das heißt, Sie können zwei Methoden mit genau denselben Parametertypen, aber unterschiedlichen Rückgabetypen verwenden. Siehe diese Diskussion: stackoverflow.com/questions/3110014/is-this-valid-java/…
Adam Paynter
8
Sie können Klassen-, Methoden- und Feldnamen mit nahezu jedem Zeichen haben. Ich habe an einem Projekt gearbeitet, bei dem die "Felder" Leerzeichen und Bindestriche in ihren Namen hatten. : P
Peter Lawrey
3
@Peter: Apropos Dateisystemzeichen: Ich bin auf einen Obfuscator gestoßen, der eine Klasse in aund eine andere in Ader JAR-Datei umbenannt hat. Es dauerte ungefähr eine halbe Stunde, bis ich auf einem Windows-Computer entpackt hatte , bis mir klar wurde, wo sich die fehlenden Klassen befanden. :)
Adam Paynter
3
@JoachimSauer: paraphrasierten JVM - Spezifikation, Seite 75: Klassennamen, Methoden, Felder und lokale Variablen enthalten , können beliebige Zeichen außer '.', ';', '[', oder '/'. Methodennamen sind identisch, können aber auch kein '<'oder enthalten '>'. (Mit den bemerkenswerten Ausnahmen von <init>und <clinit>zum Beispiel und statischen Konstruktoren.) Ich möchte darauf hinweisen, dass die Klassennamen, wenn Sie die Spezifikation genau befolgen, tatsächlich viel eingeschränkter sind, aber die Einschränkungen nicht erzwungen werden.
Leviathanbadger
3
@JoachimSauer: auch eine undokumentierte Ergänzung meiner eigenen: Die Java-Sprache enthält die "throws ex1, ex2, ..., exn"als Teil der Methode Signaturen; Sie können überschriebenen Methoden keine Ausnahmeklauseln hinzufügen. ABER die JVM könnte es nicht weniger interessieren. finalDie JVM garantiert also, dass nur Methoden wirklich ausnahmefrei sind - abgesehen von RuntimeExceptions und Errors natürlich. Soviel zur geprüften Ausnahmebehandlung: D
Leviathanbadger
401

Nachdem ich einige Zeit mit Java-Bytecode gearbeitet und einige zusätzliche Untersuchungen zu diesem Thema durchgeführt habe, finden Sie hier eine Zusammenfassung meiner Ergebnisse:

Führen Sie Code in einem Konstruktor aus, bevor Sie einen Superkonstruktor oder Hilfskonstruktor aufrufen

In der Java-Programmiersprache (JPL) muss die erste Anweisung eines Konstruktors ein Aufruf eines Superkonstruktors oder eines anderen Konstruktors derselben Klasse sein. Dies gilt nicht für Java-Bytecode (JBC). Innerhalb von Bytecode ist es absolut legitim, Code vor einem Konstruktor auszuführen, solange:

  • Ein anderer kompatibler Konstruktor wird irgendwann nach diesem Codeblock aufgerufen.
  • Dieser Aufruf befindet sich nicht in einer bedingten Anweisung.
  • Vor diesem Konstruktoraufruf wird kein Feld der konstruierten Instanz gelesen und keine ihrer Methoden aufgerufen. Dies impliziert den nächsten Punkt.

Legen Sie Instanzfelder fest, bevor Sie einen Superkonstruktor oder Hilfskonstruktor aufrufen

Wie bereits erwähnt, ist es völlig legal, einen Feldwert einer Instanz festzulegen, bevor ein anderer Konstruktor aufgerufen wird. Es gibt sogar einen Legacy-Hack, der es ermöglicht, diese "Funktion" in Java-Versionen vor 6 auszunutzen:

class Foo {
  public String s;
  public Foo() {
    System.out.println(s);
  }
}

class Bar extends Foo {
  public Bar() {
    this(s = "Hello World!");
  }
  private Bar(String helper) {
    super();
  }
}

Auf diese Weise könnte ein Feld gesetzt werden, bevor der Superkonstruktor aufgerufen wird, was jedoch nicht mehr möglich ist. In JBC kann dieses Verhalten weiterhin implementiert werden.

Verzweigen Sie einen Super-Konstruktor-Aufruf

In Java ist es nicht möglich, einen Konstruktoraufruf wie zu definieren

class Foo {
  Foo() { }
  Foo(Void v) { }
}

class Bar() {
  if(System.currentTimeMillis() % 2 == 0) {
    super();
  } else {
    super(null);
  }
}

Bis Java 7u23 hat der Verifizierer der HotSpot-VM diese Prüfung jedoch verpasst, weshalb dies möglich war. Dies wurde von mehreren Tools zur Codegenerierung als eine Art Hack verwendet, aber es ist nicht mehr legal, eine solche Klasse zu implementieren.

Letzteres war lediglich ein Fehler in dieser Compilerversion. In neueren Compilerversionen ist dies wieder möglich.

Definieren Sie eine Klasse ohne Konstruktor

Der Java-Compiler implementiert immer mindestens einen Konstruktor für eine Klasse. Im Java-Bytecode ist dies nicht erforderlich. Dies ermöglicht die Erstellung von Klassen, die auch bei Verwendung von Reflektion nicht erstellt werden können. Die Verwendung sun.misc.Unsafeermöglicht jedoch weiterhin die Erstellung solcher Instanzen.

Definieren Sie Methoden mit identischer Signatur, aber unterschiedlichem Rückgabetyp

In der JPL wird eine Methode durch ihren Namen und ihre Rohparametertypen als eindeutig identifiziert. In JBC wird zusätzlich der Rohergabetyp berücksichtigt.

Definieren Sie Felder, die sich nicht nach Namen, sondern nur nach Typ unterscheiden

Eine Klassendatei kann mehrere Felder mit demselben Namen enthalten, sofern sie einen anderen Feldtyp deklarieren. Die JVM bezieht sich immer auf ein Feld als Tupel aus Name und Typ.

Wirf nicht deklarierte geprüfte Ausnahmen, ohne sie zu fangen

Die Java-Laufzeit und der Java-Bytecode kennen das Konzept der geprüften Ausnahmen nicht. Nur der Java-Compiler überprüft, ob geprüfte Ausnahmen immer entweder abgefangen oder deklariert werden, wenn sie ausgelöst werden.

Verwenden Sie den dynamischen Methodenaufruf außerhalb von Lambda-Ausdrücken

Der sogenannte dynamische Methodenaufruf kann für alles verwendet werden, nicht nur für Javas Lambda-Ausdrücke. Mit dieser Funktion können Sie beispielsweise die Ausführungslogik zur Laufzeit ausschalten. Viele dynamische Programmiersprachen, die auf JBC hinauslaufen, haben ihre Leistung mithilfe dieser Anweisung verbessert . In Java-Bytecode können Sie auch Lambda-Ausdrücke in Java 7 emulieren, bei denen der Compiler die Verwendung eines dynamischen Methodenaufrufs noch nicht zugelassen hat, während die JVM die Anweisung bereits verstanden hat.

Verwenden Sie Bezeichner, die normalerweise nicht als legal angesehen werden

Haben Sie schon einmal Lust gehabt, Leerzeichen und einen Zeilenumbruch im Namen Ihrer Methode zu verwenden? Erstellen Sie Ihre eigene JBC und viel Glück bei der Codeüberprüfung. Die einzigen ungültigen Zeichen für Bezeichner sind ., ;, [und /. Darüber hinaus Methoden, die nicht benannt sind <init>oder <clinit>nicht <und enthalten können >.

Ordnen Sie die finalParameter oder die thisReferenz neu zu

finalParameter sind in JBC nicht vorhanden und können daher neu zugewiesen werden. Jeder Parameter, einschließlich der thisReferenz, wird nur in einem einfachen Array innerhalb der JVM gespeichert, wodurch die thisReferenz am Index 0innerhalb eines einzelnen Methodenrahmens neu zugewiesen werden kann.

finalFelder neu zuweisen

Solange ein letztes Feld innerhalb eines Konstruktors zugewiesen ist, ist es zulässig, diesen Wert neu zuzuweisen oder gar keinen Wert zuzuweisen. Daher sind die folgenden zwei Konstruktoren zulässig:

class Foo {
  final int bar;
  Foo() { } // bar == 0
  Foo(Void v) { // bar == 2
    bar = 1;
    bar = 2;
  }
}

Für static finalFelder ist es sogar zulässig, die Felder außerhalb des Klasseninitialisierers neu zuzuweisen.

Behandeln Sie Konstruktoren und den Klasseninitialisierer so, als wären sie Methoden

Dies ist eher ein konzeptionelles Merkmal, aber Konstruktoren werden in JBC nicht anders behandelt als normale Methoden. Nur der Verifizierer der JVM stellt sicher, dass Konstruktoren einen anderen legalen Konstruktor aufrufen. Davon abgesehen ist es lediglich eine Java-Namenskonvention, dass Konstruktoren aufgerufen werden müssen <init>und dass der Klasseninitialisierer aufgerufen wird <clinit>. Abgesehen von diesem Unterschied ist die Darstellung von Methoden und Konstruktoren identisch. Wie Holger in einem Kommentar hervorhob, können Sie sogar Konstruktoren mit anderen Rückgabetypen als voidoder einen Klasseninitialisierer mit Argumenten definieren, obwohl es nicht möglich ist, diese Methoden aufzurufen.

Erstellen Sie asymmetrische Datensätze * .

Beim Erstellen eines Datensatzes

record Foo(Object bar) { }

javac generiert eine Klassendatei mit einem einzelnen Feld namens bar, einer Accessormethode namens bar()und einem Konstruktor, der ein einzelnes Feld verwendet Object. Zusätzlich wird ein Datensatzattribut für barhinzugefügt. Durch manuelles Generieren eines Datensatzes ist es möglich, eine andere Konstruktorform zu erstellen, das Feld zu überspringen und den Accessor anders zu implementieren. Gleichzeitig ist es weiterhin möglich, die Reflection-API davon zu überzeugen, dass die Klasse einen tatsächlichen Datensatz darstellt.

Rufen Sie eine beliebige Supermethode auf (bis Java 1.1)

Dies ist jedoch nur für Java-Versionen 1 und 1.1 möglich. In JBC werden Methoden immer für einen expliziten Zieltyp gesendet. Dies bedeutet, dass für

class Foo {
  void baz() { System.out.println("Foo"); }
}

class Bar extends Foo {
  @Override
  void baz() { System.out.println("Bar"); }
}

class Qux extends Bar {
  @Override
  void baz() { System.out.println("Qux"); }
}

Es war möglich zu implementieren Qux#baz, um Foo#bazbeim Überspringen aufzurufen Bar#baz. Während es weiterhin möglich ist, einen expliziten Aufruf zum Aufrufen einer anderen Implementierung der Supermethode als der der direkten Superklasse zu definieren, hat dies in Java-Versionen nach 1.1 keine Auswirkungen mehr. In Java 1.1 wurde dieses Verhalten durch Setzen des ACC_SUPERFlags gesteuert, das dasselbe Verhalten ermöglicht, das nur die Implementierung der direkten Superklasse aufruft.

Definieren Sie einen nicht virtuellen Aufruf einer Methode, die in derselben Klasse deklariert ist

In Java ist es nicht möglich, eine Klasse zu definieren

class Foo {
  void foo() {
    bar();
  }
  void bar() { }
}

class Bar extends Foo {
  @Override void bar() {
    throw new RuntimeException();
  }
}

Der obige Code führt immer zu einem RuntimeExceptionWann, foodas für eine Instanz von aufgerufen wird Bar. Es ist nicht möglich, die Foo::fooMethode zu definieren , um eine eigene bar Methode aufzurufen , die in definiert ist Foo. Wie barbei einer nicht privaten Instanzmethode ist der Aufruf immer virtuell. Mit Byte - Code, kann man jedoch den Aufruf definieren die verwenden INVOKESPECIALOpcode , die direkt über die Links barMethodenaufruf in Foo::foozu Foos Version‘. Dieser Opcode wird normalerweise zum Implementieren von Super-Methodenaufrufen verwendet. Sie können den Opcode jedoch wiederverwenden, um das beschriebene Verhalten zu implementieren.

Feinkörnige Anmerkungen

In Java werden Anmerkungen entsprechend ihrer @TargetDeklaration in den Anmerkungen angewendet . Mithilfe der Bytecode-Manipulation können Anmerkungen unabhängig von diesem Steuerelement definiert werden. Es ist beispielsweise auch möglich, einen Parametertyp zu kommentieren, ohne den Parameter zu kommentieren, selbst wenn die @TargetAnnotation für beide Elemente gilt.

Definieren Sie ein Attribut für einen Typ oder seine Mitglieder

In der Java-Sprache können nur Anmerkungen für Felder, Methoden oder Klassen definiert werden. In JBC können Sie grundsätzlich alle Informationen in die Java-Klassen einbetten. Um diese Informationen nutzen zu können, können Sie sich jedoch nicht mehr auf den Lademechanismus der Java-Klasse verlassen, sondern müssen die Metainformationen selbst extrahieren.

Überlauf und implizit assign byte, short, charund booleanWerte

Die letzteren primitiven Typen sind in JBC normalerweise nicht bekannt, sondern nur für Array-Typen oder für Feld- und Methodendeskriptoren definiert. Innerhalb von Bytecode-Anweisungen nehmen alle benannten Typen das 32-Bit-Leerzeichen ein, mit dem sie dargestellt werden können int. Offiziell nur die int, float, longund doubleTypen innerhalb Byte - Code existieren , die alle brauchen explizite Konvertierung durch die Regel des Verifizierer JVM.

Kein Monitor freigeben

Ein synchronizedBlock besteht eigentlich aus zwei Anweisungen, eine zum Erfassen und eine zum Freigeben eines Monitors. In JBC können Sie eine erwerben, ohne sie freizugeben.

Hinweis : In neueren Implementierungen von HotSpot führt dies stattdessen zu einem IllegalMonitorStateExceptionam Ende einer Methode oder zu einer impliziten Freigabe, wenn die Methode durch eine Ausnahme selbst beendet wird.

Fügen Sie einem Typinitialisierer mehr als eine returnAnweisung hinzu

In Java kann sogar ein trivialer Typinitialisierer wie

class Foo {
  static {
    return;
  }
}

ist illegal. Im Bytecode wird der Typinitialisierer wie jede andere Methode behandelt, dh Rückgabeanweisungen können überall definiert werden.

Erstellen Sie irreduzible Schleifen

Der Java-Compiler konvertiert Schleifen in goto-Anweisungen im Java-Bytecode. Solche Anweisungen können verwendet werden, um irreduzible Schleifen zu erstellen, was der Java-Compiler niemals tut.

Definieren Sie einen rekursiven Catch-Block

Im Java-Bytecode können Sie einen Block definieren:

try {
  throw new Exception();
} catch (Exception e) {
  <goto on exception>
  throw Exception();
}

Eine ähnliche Anweisung wird implizit erstellt, wenn ein synchronizedBlock in Java verwendet wird, bei dem eine Ausnahme beim Freigeben eines Monitors zu der Anweisung zum Freigeben dieses Monitors zurückkehrt. Normalerweise sollte bei einer solchen Anweisung keine Ausnahme auftreten, aber wenn dies ThreadDeathder Fall wäre (z. B. die veraltete Anweisung ), wird der Monitor trotzdem freigegeben.

Rufen Sie eine Standardmethode auf

Der Java-Compiler erfordert, dass mehrere Bedingungen erfüllt sind, um den Aufruf einer Standardmethode zu ermöglichen:

  1. Die Methode muss die spezifischste sein (darf nicht von einer Subschnittstelle überschrieben werden, die von einem beliebigen Typ implementiert wird , einschließlich Supertypen).
  2. Der Schnittstellentyp der Standardmethode muss direkt von der Klasse implementiert werden, die die Standardmethode aufruft. Wenn die Schnittstelle die Schnittstelle Berweitert, Aaber eine Methode in nicht überschreibt A, kann die Methode trotzdem aufgerufen werden.

Für Java-Bytecode zählt nur die zweite Bedingung. Der erste ist jedoch irrelevant.

Rufen Sie eine Supermethode für eine Instanz auf, die dies nicht ist this

Der Java-Compiler erlaubt nur das Aufrufen einer Super- (oder Schnittstellenstandard-) Methode für Instanzen von this. Im Bytecode ist es jedoch auch möglich, die Super-Methode für eine Instanz desselben Typs aufzurufen, die der folgenden ähnelt:

class Foo {
  void m(Foo f) {
    f.super.toString(); // calls Object::toString
  }
  public String toString() {
    return "foo";
  }
}

Zugriff auf synthetische Elemente

Im Java-Bytecode ist es möglich, direkt auf synthetische Mitglieder zuzugreifen. Überlegen Sie beispielsweise, wie im folgenden Beispiel auf die äußere Instanz einer anderen BarInstanz zugegriffen wird:

class Foo {
  class Bar { 
    void bar(Bar bar) {
      Foo foo = bar.Foo.this;
    }
  }
}

Dies gilt im Allgemeinen für jedes synthetische Feld, jede Klasse oder Methode.

Definieren Sie nicht synchronisierte generische Typinformationen

Während die Java-Laufzeit keine generischen Typen verarbeitet (nachdem der Java-Compiler die Typlöschung angewendet hat), werden diese Informationen weiterhin als Metainformationen an eine kompilierte Klasse weitergeleitet und über die Reflection-API zugänglich gemacht.

Der Prüfer überprüft nicht die Konsistenz dieser mit Metadaten Stringkodierten Werte. Es ist daher möglich, Informationen zu generischen Typen zu definieren, die nicht mit der Löschung übereinstimmen. Als Konsequenz können die folgenden Behauptungen wahr sein:

Method method = ...
assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());

Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);

Die Signatur kann auch als ungültig definiert werden, sodass eine Laufzeitausnahme ausgelöst wird. Diese Ausnahme wird ausgelöst, wenn zum ersten Mal auf die Informationen zugegriffen wird, da diese träge ausgewertet werden. (Ähnlich wie Anmerkungswerte mit einem Fehler.)

Fügen Sie Parameter-Metainformationen nur für bestimmte Methoden hinzu

Der Java-Compiler ermöglicht das Einbetten von Parameternamen und Modifikatorinformationen beim Kompilieren einer Klasse mit parameteraktiviertem Flag. Im Java-Klassendateiformat werden diese Informationen jedoch pro Methode gespeichert, sodass nur solche Methodeninformationen für bestimmte Methoden eingebettet werden können.

Verwirren Sie die Dinge und bringen Sie Ihre JVM zum Absturz

In Java-Bytecode können Sie beispielsweise festlegen, dass eine beliebige Methode für einen beliebigen Typ aufgerufen werden soll. Normalerweise beschwert sich der Prüfer, wenn einem Typ eine solche Methode nicht bekannt ist. Wenn Sie jedoch eine unbekannte Methode für ein Array aufrufen, habe ich in einer JVM-Version einen Fehler gefunden, bei dem der Prüfer dies übersieht und Ihre JVM beendet wird, sobald die Anweisung aufgerufen wird. Dies ist zwar kaum eine Funktion, aber technisch gesehen ist dies mit javac kompiliertem Java nicht möglich . Java hat eine Art doppelte Validierung. Die erste Validierung wird vom Java-Compiler angewendet, die zweite von der JVM, wenn eine Klasse geladen wird. Wenn Sie den Compiler überspringen, finden Sie möglicherweise eine Schwachstelle in der Validierung des Überprüfers. Dies ist jedoch eher eine allgemeine Aussage als ein Merkmal.

Kommentieren Sie den Empfängertyp eines Konstruktors, wenn keine äußere Klasse vorhanden ist

Seit Java 8 können nicht statische Methoden und Konstruktoren innerer Klassen einen Empfängertyp deklarieren und diese Typen mit Anmerkungen versehen. Konstruktoren von Klassen der obersten Ebene können ihren Empfängertyp nicht mit Anmerkungen versehen, da sie meistens keinen deklarieren.

class Foo {
  class Bar {
    Bar(@TypeAnnotation Foo Foo.this) { }
  }
  Foo() { } // Must not declare a receiver type
}

Da Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()jedoch eine AnnotatedTypeDarstellung zurückgegeben wird Foo, ist es möglich, Typanmerkungen für Fooden Konstruktor direkt in die Klassendatei aufzunehmen, in der diese Anmerkungen später von der Reflection-API gelesen werden.

Verwenden Sie nicht verwendete / Legacy-Bytecode-Anweisungen

Da andere es benannt haben, werde ich es auch aufnehmen. Java verwendete früher Unterprogramme der Anweisungen JSRund RET. JBC kannte zu diesem Zweck sogar eine eigene Art von Absenderadresse. Die Verwendung von Unterprogrammen hat jedoch die statische Code-Analyse zu kompliziert gemacht, weshalb diese Anweisungen nicht mehr verwendet werden. Stattdessen dupliziert der Java-Compiler den von ihm kompilierten Code. Dies schafft jedoch im Grunde eine identische Logik, weshalb ich es nicht wirklich für etwas anderes halte. Ebenso können Sie beispielsweise die hinzufügenNOOPBytecode-Anweisung, die auch vom Java-Compiler nicht verwendet wird, aber dies würde es Ihnen auch nicht wirklich ermöglichen, etwas Neues zu erreichen. Wie im Zusammenhang ausgeführt, werden diese erwähnten "Funktionsanweisungen" jetzt aus dem Satz legaler Opcodes entfernt, wodurch sie noch weniger zu einem Merkmal werden.

Rafael Winterhalter
quelle
3
In Bezug auf Methodennamen können Sie mehrere <clinit>Methoden verwenden, indem Sie Methoden mit dem Namen definieren, <clinit>aber Parameter akzeptieren oder einen voidTyp ohne Rückgabe haben. Diese Methoden sind jedoch nicht sehr nützlich. Die JVM ignoriert sie und der Bytecode kann sie nicht aufrufen. Die einzige Verwendung wäre, die Leser zu verwirren.
Holger
2
Ich habe gerade festgestellt, dass die JVM von Oracle beim Beenden der Methode einen unveröffentlichten Monitor erkennt und eine auslöst, IllegalMonitorStateExceptionwenn Sie die monitorexitAnweisung weggelassen haben . Und im Falle eines außergewöhnlichen Methoden-Exits, bei dem a nicht ausgeführt wurde monitorexit, wird der Monitor unbeaufsichtigt zurückgesetzt.
Holger
1
@Holger - wusste das nicht, ich weiß, dass dies zumindest in früheren JVMs möglich war, JRockit hat sogar einen eigenen Handler für diese Art der Implementierung. Ich werde den Eintrag aktualisieren.
Rafael Winterhalter
1
Nun, die JVM-Spezifikation schreibt ein solches Verhalten nicht vor. Ich habe es gerade entdeckt, weil ich versucht habe, eine baumelnde intrinsische Sperre mit einem solchen nicht standardmäßigen Bytecode zu erstellen.
Holger
3
Ok, ich habe die relevante Spezifikation gefunden : „ Strukturiertes Sperren ist die Situation, in der während eines Methodenaufrufs jeder Exit auf einem bestimmten Monitor mit einem vorhergehenden Eintrag auf diesem Monitor übereinstimmt. Da nicht garantiert werden kann, dass der gesamte an die Java Virtual Machine gesendete Code eine strukturierte Sperrung ausführt, sind Implementierungen der Java Virtual Machine zulässig, jedoch nicht erforderlich, um die beiden folgenden Regeln durchzusetzen, die eine strukturierte Sperrung gewährleisten. … ”
Holger
14

Hier sind einige Funktionen, die im Java-Bytecode, jedoch nicht im Java-Quellcode ausgeführt werden können:

  • Auslösen einer aktivierten Ausnahme aus einer Methode, ohne zu deklarieren, dass die Methode sie auslöst. Die aktivierten und nicht aktivierten Ausnahmen werden nur vom Java-Compiler und nicht von der JVM überprüft. Aus diesem Grund kann Scala beispielsweise geprüfte Ausnahmen von Methoden auslösen, ohne sie zu deklarieren. Bei Java-Generika gibt es jedoch eine Problemumgehung, die als hinterhältiger Wurf bezeichnet wird .

  • Zwei Methoden in einer Klasse, die sich nur im Rückgabetyp unterscheiden, wie bereits in Joachims Antwort erwähnt : Die Java-Sprachspezifikation erlaubt nicht zwei Methoden in derselben Klasse, wenn sie sich nur in ihrem Rückgabetyp unterscheiden (dh gleicher Name, gleiche Argumentliste, ...). Die JVM - Spezifikation hat jedoch keine solche Beschränkung, so dass eine Klassendatei kann zwei solche Methoden enthalten, gibt es einfach keine Möglichkeit , eine solche Klasse - Datei mit dem normalen Java - Compiler zu erzeugen. Diese Antwort enthält ein schönes Beispiel / eine Erklärung .

Esko Luontola
quelle
4
Beachten Sie, dass es ist eine Möglichkeit , das erste , was in Java zu tun. Es wird manchmal ein hinterhältiger Wurf genannt .
Joachim Sauer
Das ist hinterhältig! : D Danke fürs Teilen.
Esko Luontola
Ich denke, Sie können auch Thread.stop(Throwable)für einen hinterhältigen Wurf verwenden. Ich gehe davon aus, dass der bereits verknüpfte schneller ist.
Bart van Heukelom
2
Sie können keine Instanz erstellen, ohne einen Konstruktor im Java-Bytecode aufzurufen. Der Prüfer lehnt jeden Code ab, der versucht, eine nicht initialisierte Instanz zu verwenden. Die Implementierung der Objektdeserialisierung verwendet native Code-Helfer zum Erstellen von Instanzen ohne Konstruktoraufruf.
Holger
Für ein Klasse-Foo-Erweiterungsobjekt konnten Sie Foo nicht instanziieren, indem Sie einen Konstruktor aufrufen, der in Object deklariert ist. Der Prüfer würde es ablehnen. Sie könnten einen solchen Konstruktor mit Javas ReflectionFactory erstellen, aber dies ist kaum eine Bytecode-Funktion, die jedoch von Jni realisiert wird. Ihre Antwort ist falsch und Holger ist richtig.
Rafael Winterhalter
8
  • GOTOkann mit Beschriftungen verwendet werden, um eigene Kontrollstrukturen zu erstellen (außer for whileusw.)
  • Sie können die thislokale Variable innerhalb einer Methode überschreiben
  • Wenn Sie beide kombinieren, können Sie einen für Tail Call optimierten Bytecode erstellen (ich mache das in JCompilo ).

Als verwandten Punkt können Sie den Parameternamen für Methoden erhalten, wenn diese mit Debug kompiliert wurden ( Paranamer tut dies durch Lesen des Bytecodes

Daniel Worthington-Bodart
quelle
Wie macht man overridediese lokale Variable?
Michael
2
@ Michael Overriding ist ein zu starkes Wort. Auf der Bytecode-Ebene wird auf alle lokalen Variablen über einen numerischen Index zugegriffen, und es gibt keinen Unterschied zwischen dem Schreiben in eine vorhandene Variable oder dem Initialisieren einer neuen Variablen (mit einem nicht zusammenhängenden Bereich). In beiden Fällen handelt es sich lediglich um ein Schreiben in eine lokale Variable. Die thisVariable hat den Index Null, ist jedoch nicht nur bei der thisEingabe einer Instanzmethode mit der Referenz vorinitialisiert, sondern nur eine lokale Variable. Sie können also einen anderen Wert schreiben, der sich je nach Verwendung wie das Beenden thisdes Gültigkeitsbereichs oder das Ändern der thisVariablen verhält.
Holger
Aha! Es ist also wirklich so, dass thises neu zugewiesen werden kann? Ich denke, es war nur das Wort Override, das mich fragte, was es genau bedeutete.
Michael
5

Vielleicht ist Abschnitt 7A in diesem Dokument von Interesse, obwohl es eher um Bytecode- Fallstricke als um Bytecode- Funktionen geht .

Eljenso
quelle
Interessante Lektüre, aber es sieht nicht so aus, als würde man eines dieser Dinge (ab) verwenden wollen .
Bart van Heukelom
4

In der Java-Sprache muss die erste Anweisung in einem Konstruktor ein Aufruf des Superklassenkonstruktors sein. Bytecode hat diese Einschränkung nicht. Stattdessen lautet die Regel, dass der Superklassenkonstruktor oder ein anderer Konstruktor in derselben Klasse für das Objekt aufgerufen werden muss, bevor auf die Mitglieder zugegriffen werden kann. Dies sollte mehr Freiheit ermöglichen wie:

  • Erstellen Sie eine Instanz eines anderen Objekts, speichern Sie es in einer lokalen Variablen (oder einem Stapel) und übergeben Sie es als Parameter an den Superklassenkonstruktor, während die Referenz in dieser Variablen für andere Zwecke erhalten bleibt.
  • Rufen Sie basierend auf einer Bedingung verschiedene andere Konstruktoren auf. Dies sollte möglich sein: Wie kann man in Java einen anderen Konstruktor bedingt aufrufen?

Ich habe diese nicht getestet. Bitte korrigieren Sie mich, wenn ich falsch liege.

msell
quelle
Sie können sogar Mitglieder einer Instanz festlegen, bevor Sie den Konstruktor der Oberklasse aufrufen. Das Lesen von Feldern oder Aufrufen von Methoden ist jedoch vorher nicht möglich.
Rafael Winterhalter
3

Mit Bytecode können Sie anstelle von einfachem Java-Code Code generieren, der ohne Compiler geladen und ausgeführt werden kann. Viele Systeme haben JRE anstelle von JDK. Wenn Sie Code dynamisch generieren möchten, ist es möglicherweise besser, wenn nicht sogar einfacher, Bytecode zu generieren, anstatt Java-Code zu kompilieren, bevor er verwendet werden kann.

Peter Lawrey
quelle
6
Aber dann überspringen Sie einfach den Compiler und produzieren nichts, was mit dem Compiler nicht produziert werden könnte (wenn es verfügbar wäre).
Bart van Heukelom
2

Ich habe als I-Play einen Bytecode-Optimierer geschrieben (er wurde entwickelt, um die Codegröße für J2ME-Anwendungen zu reduzieren). Eine Funktion, die ich hinzugefügt habe, war die Möglichkeit, Inline-Bytecode zu verwenden (ähnlich der Inline-Assemblersprache in C ++). Ich habe es geschafft, die Größe einer Funktion, die Teil einer Bibliotheksmethode war, mithilfe der DUP-Anweisung zu reduzieren, da ich den Wert zweimal benötige. Ich hatte auch Null-Byte-Anweisungen (wenn Sie eine Methode aufrufen, die ein Zeichen akzeptiert und ein int übergeben möchte, von dem Sie wissen, dass es nicht umgewandelt werden muss, habe ich int2char (var) hinzugefügt, um char (var) zu ersetzen, und es würde entfernt die i2c-Anweisung, um die Größe des Codes zu reduzieren. Ich habe ihn auch dazu gebracht, float a = 2,3; float b = 3,4; float c = a + b; und das würde in einen festen Punkt konvertiert (schneller, und auch einige J2ME nicht Gleitkomma unterstützen).

nharding
quelle
2

Wenn Sie in Java versuchen, eine öffentliche Methode mit einer geschützten Methode (oder einer anderen Einschränkung des Zugriffs) zu überschreiben, wird die folgende Fehlermeldung angezeigt: "Versuch, schwächere Zugriffsrechte zuzuweisen". Wenn Sie dies mit JVM-Bytecode tun, ist der Verifizierer damit einverstanden, und Sie können diese Methoden über die übergeordnete Klasse aufrufen, als wären sie öffentlich.

Joseph Sible-Reinstate Monica
quelle