字节码功能在Java语言中不可用


146

当前(Java 6)中有您可以用Java字节码执行的,您无法在Java语言中执行的操作吗?

我知道两者都已经完成了Turing,所以将“可以做”理解为“可以更快/更好地做事,或者只是以其他方式做”。

我正在考虑invokedynamic无法使用Java生成额外的字节码,例如,除非特定的字节码用于将来的版本。


3
定义“事物”。最后,Java语言和Java字节码都已经完成了图灵处理……
Michael Borgwardt

2
是真正的问题吗?使用Jasmin而不是Java进行字节码编程有什么好处吗?
彼得·劳瑞

2
就像rol在汇编程序中一样,您不能用C ++编写。
Martijn Courteaux 2011年

1
这是一个非常糟糕的优化编译器,无法编译(x<<n)|(x>>(32-n))rol指令。
Random832

Answers:


62

据我所知,Java 6支持的字节码中没有主要功能,这些功能也无法从Java源代码访问。显然,主要原因是Java字节码在设计时就考虑了Java语言。

但是,有些功能不是现代Java编译器提供的:

  • ACC_SUPER标志

    这是一个可以在类上设置的标志,并指定如何invokespecial为此类处理字节码的特定特殊情况。它是由所有现代Java编译器(如果我没有记错的话,其中“现代”> = Java 1.1)设置的,只有古老的Java编译器会生成未设置该类的类文件。该标志仅出于向后兼容的原因而存在。请注意,从Java 7u51开始,由于安全原因,ACC_SUPER被完全忽略。

  • jsr/ ret字节码。

    这些字节码用于实现子例程(主要用于实现finally块)。从Java 6开始不再生产它们。之所以会弃用它们,是因为它们使静态验证复杂化了很多,但却没有太大的收获(即所使用的代码几乎总是可以用很少的开销通过常规跳转来重新实现)。

  • 一个类中有两个方法,只是返回类型不同。

    Java语言规范不允许在同一个类中的两个方法时,它们之间的区别在他们的返回类型(即相同的名字,相同的参数列表,...)。但是,JVM规范没有这样的限制,因此类文件可以包含两个这样的方法,只是没有办法使用普通的Java编译器来生成这样的类文件。这个答案有一个很好的例子/解释。


5
我可以添加另一个答案,但我们也可以为您提供规范的答案。您可能要提到字节码中方法的签名包括return type。也就是说,可以有两种方法具有完全相同的参数类型,但返回类型不同。看到这个讨论:stackoverflow.com/questions/3110014/is-this-valid-java/...
亚当潘德

8
您可以具有几乎任何字符的类,方法和字段名称。我参与了一个项目,其中“字段”的名称中带有空格和连字符。:P
彼得·劳里

3
@Peter:说到文件系统字符,我遇到了混淆器,该混淆器将一个类重命名为JAR文件a,另一个将A其重命名为JAR文件。在意识到缺少类的位置之前,我在Windows机器上解压缩了大约半个小时。:)
亚当·佩恩特

3
@JoachimSauer:转述JVM规范,第75页:类名,方法,字段和局部变量可以包含任何除字符'.'';''[',或'/'。方法名称相同,但不能包含'<''>'。(除了<init>and <clinit>实例和静态构造函数的显着例外。)我应该指出,如果严格遵守规范,则实际上对类名的约束更多,但不会强制执行约束。
leviathanbadger

3
@JoachimSauer:也是我自己的一个未记载的附加内容:java语言包括"throws ex1, ex2, ..., exn"作为方法签名的一部分;您不能将异常抛出子句添加到重写的方法中。但是,JVM不在乎。因此final,JVM真正保证只有方法是无异常的- 当然,除了RuntimeExceptions和Errors 之外。对于检查的异常处理来说太多了:D
leviathanbadger

401

在使用Java字节码工作了一段时间并就此问题进行了一些其他研究之后,以下是我的发现的摘要:

在调用超级构造函数或辅助构造函数之前,在构造函数中执行代码

在Java编程语言(JPL)中,构造函数的第一条语句必须是超级构造函数或同一类的另一个构造函数的调用。对于Java字节码(JBC),情况并非如此。在字节码内,只要在构造函数之前执行任何代码是绝对合法的:

  • 在此代码块之后的某个时间,将调用另一个兼容的构造函数。
  • 此调用不在条件语句中。
  • 在此构造函数调用之前,不会读取已构造实例的任何字段,并且不会调用其任何方法。这意味着下一项。

在调用超级构造函数或辅助构造函数之前设置实例字段

如前所述,在调用另一个构造函数之前设置实例的字段值是完全合法的。甚至存在一个传统的hack,使它能够在6之前的Java版本中利用此“功能”:

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();
  }
}

这样,可以在调用超级构造函数之前设置一个字段,但这不再可能。在JBC中,此行为仍然可以实现。

分支超级构造函数调用

在Java中,无法定义类似

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

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

但是,在Java 7u23之前,HotSpot VM的验证程序确实错过了此检查,这就是可能的原因。几种代码生成工具已将其用作一种黑客手段,但实现这样的类不再合法。

后者只是此编译器版本中的错误。在较新的编译器版本中,这再次成为可能。

定义一个没有任何构造函数的类

Java编译器将始终为任何类实现至少一个构造函数。在Java字节码中,这不是必需的。这样可以创建即使使用反射也无法构造的类。但是,使用sun.misc.Unsafe仍然允许创建此类实例。

定义具有相同签名但具有不同返回类型的方法

在JPL中,方法的名称和原始参数类型将其标识为唯一。在JBC中,还应考虑原始返回类型。

定义不因名称而异但仅因类型而异的字段

一个类文件可以包含多个同名字段,只要它们声明不同的字段类型即可。JVM始终将字段称为名称和类型的元组。

抛出未声明的检查异常,而不捕获它们

Java运行时和Java字节码不了解已检查异常的概念。只有Java编译器才能验证是否抛出了已捕获的异常或声明了已检查的异常。

在Lambda表达式之外使用动态方法调用

所谓的动态方法调用可以用于任何事物,不仅用于Java的lambda表达式。例如,使用此功能可以在运行时切换执行逻辑。归结为JBC的许多动态编程语言都通过使用此指令来提高其性能。在Java字节码中,您还可以在Java 7中模拟lambda表达式,其中,在JVM已理解指令的情况下,编译器尚未允许使用任何动态方法调用。

使用通常不合法的标识符

是否曾经幻想使用空格和方法名称中的换行符?创建您自己的JBC,祝您好运,以便进行代码审查。标识符唯一的非法字符.;[/。此外,未命名的方法<init><clinit>不能包含<和的方法>

重新分配final参数或this参考

final参数在JBC中不存在,因此可以重新分配。任何参数,包括this引用,都仅存储在JVM内的简单数组中,这允许在单个方法框架内的this索引处重新分配引用0

重新分配final字段

只要在构造函数中分配了最终字段,就可以合法地重新分配该值,甚至完全不分配值。因此,以下两个构造函数是合法的:

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

对于static final字段,甚至可以在类初始化程序之外重新分配字段。

将构造函数和类初始化程序视为方法

这更多是概念上的功能,但在JBC中对构造函数的对待与常规方法没有什么不同。只有JVM的验证程序才能确保构造函数调用另一个合法的构造函数。除此之外,这仅仅是Java命名约定,必须调用构造<init>函数,并调用类初始化程序<clinit>。除了这种差异之外,方法和构造函数的表示是相同的。正如Holger在评论中指出的那样void,即使无法调用这些方法,您甚至可以定义返回类型以外的构造函数或带有参数的类初始值设定项。

创建非对称记录*

创建记录时

record Foo(Object bar) { }

javac将生成一个类文件,该类文件具有一个名为的字段bar,一个名为的访问器方法bar()以及一个包含一个的构造函数Object。此外,bar添加的记录属性。通过手动生成记录,可以创建不同的构造函数形状,以跳过字段并以不同方式实现访问器。同时,仍然可以使反射API相信该类表示实际记录。

调用任何超级方法(直到Java 1.1)

但是,这仅适用于Java版本1和1.1。在JBC中,方法总是在显式目标类型上分派。这意味着

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"); }
}

可以实现跳过时Qux#baz调用。尽管仍然可以定义一个显式调用来调用除直接超类之外的另一个超级方法实现,但这在1.1版之后的Java版本中不再起作用。在Java 1.1中,此行为是通过设置标志来控制的,该标志将启用仅调用直接超类的实现的相同行为。Foo#bazBar#bazACC_SUPER

定义在同一个类中声明的方法的非虚拟调用

在Java中,无法定义一个类

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

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

上面的代码将始终导致在的实例上调用RuntimeExceptionwhen 。不能定义该方法来调用在中定义的自己的方法。与非私有实例方法一样,该调用始终是虚拟的。但是,使用字节码,就可以定义调用以使用将方法调用直接链接到的版本的操作码。该操作码通常用于实现超级方法调用,但是您可以重用该操作码来实现所描述的行为。fooBarFoo::foo barFoobarINVOKESPECIALbarFoo::fooFoo

细粒度类型注释

在Java中,根据@Target注释声明的注释来应用注释。使用字节码操作,可以独立于此控件定义注释。而且,例如即使@Target注释适用于两个元素,也可以在不注释参数的情况下注释参数类型。

定义类型或其成员的任何属性

在Java语言中,只能为字段,方法或类定义注释。在JBC中,您基本上可以将任何信息嵌入Java类中。但是,为了利用此信息,您不再可以依赖Java类加载机制,而是需要自己提取元信息。

溢出和隐式分配byteshortcharboolean

在JBC中通常不知道后者的原始类型,而仅为数组类型或字段和方法描述符定义。在字节码指令中,所有已命名类型都占用32位空格,以便将其表示为int。据官方统计,仅在intfloatlongdouble字节代码中存在类型的都需要由JVM的验证规则显式转换。

不释放显示器

一个synchronized块实际上由两个语句组成,一个要获取,一个要释放监视器。在JBC中,您无需发布就可以获取一个。

注意:在HotSpot的最新实现中,这会导致IllegalMonitorStateException方法末尾出现,或者如果方法由异常本身终止则导致隐式释放。

将多个return语句添加到类型初始值设定项

在Java中,即使是琐碎的类型初始化程序,例如

class Foo {
  static {
    return;
  }
}

是非法的。在字节码中,类型初始值设定项与其他任何方法一样被对待,即return语句可以在任何地方定义。

创建不可约环

Java编译器将循环转换为Java字节码中的goto语句。此类语句可用于创建不可简化的循环,而Java编译器从未这样做过。

定义一个递归catch块

在Java字节码中,您可以定义一个块:

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

synchronized在Java中使用块时,会隐式创建类似的语句,其中释放监视器时的任何异常都会返回到释放该监视器的指令。通常,此类指令不会发生任何异常,但是如果发生异常(例如,已弃用ThreadDeath),则仍将释放监视器。

调用任何默认方法

为了允许默认方法的调用,Java编译器需要满足几个条件:

  1. 该方法必须是最具体的一种(一定不能被任何类型(包括超级类型)实现的子接口覆盖)。
  2. 默认方法的接口类型必须由调用默认方法的类直接实现。但是,如果interface B扩展了interface A但未覆盖中的方法A,则仍然可以调用该方法。

对于Java字节码,仅第二条件计数。但是第一个是不相关的。

在非实例上调用超级方法 this

Java编译器仅允许在的实例上调用超级(或接口默认)方法this。但是,在字节码中,也可以在相同类型的实例上调用super方法,如下所示:

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

访问综合成员

在Java字节码中,可以直接访问合成成员。例如,考虑在以下示例中如何Bar访问另一个实例的外部实例:

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

对于任何合成字段,类或方法,通常都是如此。

定义不同步的通用类型信息

尽管Java运行时不处理泛型类型(在Java编译器应用类型擦除之后),但此信息仍作为元信息附加到编译的类中,并可以通过反射API进行访问。

验证者不检查这些元数据String编码值的一致性。因此,可以定义与擦除不匹配的通用类型的信息。因此,以下断言可能是正确的:

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

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

同样,可以将签名定义为无效,从而引发运行时异常。第一次访问该信息时会抛出此异常,因为它是惰性评估的。(类似于带有错误的注释值。)

仅对某些方法附加参数元信息

Java编译器允许在parameter启用标志的情况下编译类时嵌入参数名称和修饰符信息。但是,在Java类文件格式中,此信息按方法存储,这使得仅针对某些方法嵌入此类方法信息成为可能。

搞砸了,使JVM崩溃了

例如,在Java字节码中,您可以定义以任何类型调用任何方法。通常,如果类型未知,验证者会抱怨。但是,如果您在数组上调用未知方法,则我在某些JVM版本中发现了一个错误,该错误会导致验证程序丢失此错误,并且一旦调用指令,您的JVM将结束。虽然这几乎不是一个功能,但是从技术上讲,这是javac编译Java 不可能实现的功能。Java具有某种双重验证。第一次验证由Java编译器应用,第二次验证由JVM在加载类时应用。通过跳过编译器,您可能会在验证者的验证中发现薄弱环节。不过,这只是一般性的说明,而不是功能。

没有外部类时,注释构造函数的接收者类型

从Java 8开始,非静态方法和内部类的构造函数可以声明接收器类型并注释这些类型。顶级类的构造函数无法注释其接收者类型,因为它们大多数都不声明一个。

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

Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()但是,由于确实会返回一个AnnotatedType表示Foo,因此可以Foo在类文件中直接包含的构造函数的类型注释,这些注释随后将由反射API读取。

使用未使用的/旧的字节码指令

由于其他人已将其命名,因此我也将其包括在内。Java以前通过JSRand RET语句使用子例程。为此,JBC甚至知道自己的寄信人地址类型。但是,子例程的使用确实使静态代码分析过于复杂,这就是为什么不再使用这些指令的原因。相反,Java编译器将复制其编译的代码。但是,这基本上创建了相同的逻辑,这就是为什么我不真正认为它可以实现不同的目的。同样,您可以例如添加NOOP字节代码指令,Java编译器也不使用,但是这实际上也不允许您实现新的东西。正如上下文中指出的那样,这些提及的“功能说明”现在已从一组合法操作码中删除,这确实使它们的功能更少。


3
关于方法名称,可以<clinit>通过使用名称<clinit>但接受参数或具有非void返回类型的方法来定义多个方法。但是这些方法不是很有用,JVM将忽略它们,字节码不能调用它们。唯一的用途是使读者感到困惑。
Holger 2014年

2
我刚刚发现,Oracle的JVM在方法出口处检测到未发布的监视器,IllegalMonitorStateException如果您省略了该monitorexit指令,则会抛出。而且,如果发生异常方法退出而无法执行monitorexit,它将以静默方式重置监视器。
Holger 2014年

1
@Holger-不知道,我知道这至少在早期的JVM中是可能的,JRockit甚至有自己的处理程序来实现这种实现。我将更新条目。
拉斐尔·温特豪德2014年

1
嗯,JVM规范没有规定这种行为。我刚发现它是因为我试图使用此类非标准字节码创建一个悬空的固有锁。
Holger 2014年

3
好的,我发现了相关的规范:“ 结构化锁定是指在方法调用过程中,给定监视器上的每个出口与该监视器上的先前条目匹配的情况。由于不能保证提交给Java虚拟机的所有代码都将执行结构化锁定,因此允许但不要求强制执行以下两个保证结构化锁定的规则的Java虚拟机实现。…”
Holger 2014年

14

以下是一些可以用Java字节码完成但不能用Java源代码完成的功能:

  • 从方法抛出已检查的异常而未声明该方法将其抛出。被检查和未被检查的异常是仅由Java编译器而不是JVM检查的事物。因此,例如,Scala可以从方法中抛出检查后的异常而无需声明它们。尽管使用Java泛型,有一种解决方法称为溜溜球

  • 正如Joachim的回答中已经提到的,在一个类中只有两个方法的返回类型有所不同:Java语言规范不允许两个类中的两个方法在返回类型上有所不同(即,相同的名称,相同的参数列表, ...)。但是,JVM规范没有这样的限制,因此类文件可以包含两个这样的方法,只是没有办法使用普通的Java编译器来生成这样的类文件。这个答案有一个很好的例子/解释。


4
请注意,一种方法做的第一件事就是在Java中。有时称为偷偷摸摸的掷球
约阿希姆·绍尔

现在偷偷摸摸!:D感谢您的分享。
Esko Luontola,2011年

我想您也可以Thread.stop(Throwable)偷偷摸摸地扔。我认为已经链接的速度更快。
Bart van Heukelom

2
如果不使用Java字节码调用构造函数,则无法创建实例。验证者将拒绝任何尝试使用未初始化实例的代码。对象反序列化实现使用本机代码助手来创建实例,而无需构造函数调用。
Holger 2013年

对于Foo扩展对象的类,您无法通过调用在Object中声明的构造函数来实例化Foo。验证者将拒绝它。您可以使用Java的ReflectionFactory创建这样的构造函数,但这几乎不是字节码功能,而是由Jni实现的。您的回答是错误的,霍尔格是正确的。
拉斐尔·温特豪德2014年

8
  • GOTO可以与标签一起使用以创建自己的控件结构(等除外for while
  • 您可以this在方法内覆盖局部变量
  • 将这两种方法结合起来,就可以创建创建尾部调用优化的字节码(我在JCompilo做到了

与此相关的是,如果使用debug进行编译,则可以获得方法的参数名称(Paranamer通过读取字节码来完成此操作


你怎么override这个局部变量?
迈克尔(Michael)

2
@Michael压倒性太强了。在字节码级别上,所有局部变量均由数字索引访问,并且写入现有变量或初始化新变量(具有相异作用域)之间没有区别,在两种情况下,这仅是写入局部变量。该this变量的索引为零,但是除了this在进入实例方法时用引用进行预初始化之外,它只是一个局部变量。因此,您可以为它写一个不同的值,这可以像结束this'作用域或更改this变量一样,具体取决于您如何使用它。
Holger

我懂了!真的this可以重新分配吗?我认为这只是覆盖一词,让我想知道它的确切含义。
迈克尔

5

尽管它是关于字节码陷阱而不是字节码功能的,但也许本文档中的第7A节很有趣。


有趣的读物,但看起来好像不想(滥用)任何这些东西。
Bart van Heukelom

4

在Java语言中,构造函数中的第一条语句必须是对超类构造函数的调用。字节码没有此限制,而是规则是在访问成员之前,必须为对象调用超类构造函数或同一类中的另一个构造函数。这应该允许更多的自由,例如:

  • 创建另一个对象的实例,将其存储在局部变量(或堆栈)中,并将其作为参数传递给超类构造函数,同时仍将引用保留在该变量中以供其他用途。
  • 根据条件调用其他不同的构造函数。这应该是可能的:如何在Java中有条件地调用其他构造函数?

我没有测试这些,所以如果我错了,请纠正我。


您甚至可以在调用实例的超类构造函数之前设置实例的成员。但是,在此之前无法读取字段或调用方法。
拉斐尔·温特豪德2014年

3

您可以使用字节码而不是普通的Java代码来处理生成的代码,这些代码无需编译器即可加载和运行。许多系统使用JRE而不是JDK,并且如果要动态生成代码,则在使用它之前,最好先生成字节代码而不是Java代码,如果不是那么容易的话。


6
但是然后您只是跳过了编译器,而不生成无法使用编译器生成的内容(如果可用)。
Bart van Heukelom

2

我在玩I-Play时就写了一个字节码优化器(旨在减少J2ME应用程序的代码大小)。我添加的一个功能是可以使用内联字节码(类似于C ++中的内联汇编语言)。通过使用DUP指令,我设法减小了作为库方法一部分的函数的大小,因为我需要两次该值。我也有零字节指令(如果您正在调用一个采用char的方法,并且想要传递一个int,您知道不需要强制转换,那么我添加了int2char(var)来替换char(var),它将删除i2c指令以减小代码的大小。我还使它执行了float a = 2.3; float b = 3.4; float c = a + b;然后将其转换为定点(速度更快,而且有些J2ME没有支持浮点)。


2

在Java中,如果尝试使用受保护的方法(或访问的任何其他减少方法)覆盖公共方法,则会收到错误消息:“试图分配较弱的访问权限”。如果使用JVM字节码执行此操作,则验证程序就可以使用它,并且可以通过父类调用这些方法,就像它们是公共的一样。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.