在 Java 语言中不可用的字节码特性

当前(Java6)是否存在在 Java 字节码中可以做的事情,而不能在 Java 语言中做?

我知道两者都是完整的图灵,所以可以把“ can do”理解为“可以做得更快/更好,或者只是以不同的方式”。

我在考虑额外的字节码,比如 invokedynamic,它不能用 Java 生成,除非特定的字节码用于未来的版本。

58081 次浏览

据我所知,Java6支持的字节码中没有不能从 Java 源代码访问的主要特性。这样做的主要原因显然是 Java 字节码在设计时考虑了 Java 语言。

然而,现代 Java 编译器没有产生一些特性:

  • 返回文章页面 ACC_SUPER旗帜:

    这是一个可以在类上设置的标志,并指定如何处理此类的 invokespecial字节码的特定角落大小写。它是由所有现代 Java 编译器设置的(如果我没记错的话,其中的“现代”是 > = Java 1.1) ,只有古代 Java 编译器生成的类文件没有设置它。此标志仅因向后兼容性原因而存在。注意,从 Java7u51开始,由于安全原因,ACC _ SUPER 被完全忽略。

  • jsr/ret字节码。

    这些字节码用于实现子例程(主要用于实现 finally块)。它们是 自 Java6以来不再生成。它们不支持的原因是,它们使静态验证复杂化很多,却没有什么好处(例如,使用的代码几乎总是可以通过正常的跳转来重新实现,开销很小)。

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

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

你可以用字节码而不是普通的 Java 代码来做的事情是生成不需要编译器就可以加载和运行的代码。许多系统使用 JRE 而不是 JDK,如果您想要动态地生成代码,那么生成字节代码而不是 Java 代码可能更好,如果不是更容易的话,在使用之前必须编译它。

也许 这份文件中的7A 节是有趣的,尽管它是关于字节码 陷阱而不是字节码 特征的。

下面是一些可以在 Java 字节码中实现但不能在 Java 源代码中实现的特性:

  • 从方法引发已检查异常,但不声明该方法引发该异常。已检查和未检查的异常只能由 Java 编译器检查,而不能由 JVM 检查。正因为如此,例如 Scala 可以从方法中抛出检查过的异常,而无需声明它们。尽管使用 Java 泛型有一个称为 鬼鬼祟祟的投球的变通方法。

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

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

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

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

  • GOTO可以与标签一起创建您自己的控制结构(除了 for while等)
  • 可以在方法内重写 this本地变量
  • 结合这两者,您可以创建尾部调用优化字节码(我在 JCompilo中这样做)

作为一个相关点,如果使用 debug (Paranamer 通过读取字节码来实现这一点

在使用 Java 字节码工作了一段时间并对这个问题做了一些额外的研究之后,下面是我的发现的总结:

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

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

  • 在此代码块之后的某个时间调用另一个兼容的构造函数。
  • 这通电话不在 If判断语句范围内。
  • 在此构造函数调用之前,不读取构造实例的任何字段,也不调用它的任何方法。这意味着下一项。

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

如前所述,在调用另一个构造函数之前设置实例的字段值是完全合法的。甚至还存在一种遗留黑客技术,使其能够在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);
}
}

在 Java7u23之前,HotSpot VM 的验证器确实没有进行这项检查,这就是为什么它是可能的。这被一些代码生成工具作为一种技巧使用,但是实现这样的类已经不合法了。

后者在这个编译器版本中只是一个 bug,在较新的编译器版本中,这也是可能的。

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

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

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

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

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

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

引发未声明的检查异常而不捕获它们

Java 运行时和 Java 字节码不知道检查异常的概念。只有 Java 编译器才能验证检查异常是否总是被捕获或者在引发异常时被声明。

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

所谓的 动态方法调用可以用于任何事情,而不仅仅是 Java 的 lambda 表达式。例如,使用此特性可以在运行时切换执行逻辑。许多动态编程语言通过使用此指令归结为 JBC 提高了他们的表现。在 Java 字节码中,您还可以模拟 Java7中的 lambda 表达式,其中编译器还不允许使用任何动态方法调用,而 JVM 已经理解了该指令。

使用通常认为不合法的标识符

有没有想过在方法的名称中使用空格和换行符?创建您自己的 JBC 并为代码审查带来好运。标识符的唯一非法字符是 .;[/。此外,未命名为 <init><clinit>的方法不能包含 <>

重新分配 final参数或 this引用

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

重新分配 final字段

只要在构造函数中分配了 final 字段,重新分配该值或甚至根本不分配值都是合法的。因此,以下两个构造函数是合法的:

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

对于 static final字段,甚至允许重新分配 类初始值设定项。

将构造函数和类初始值设定项视为方法

这更像是一个 概念特征概念特征,但是构造函数在 JBC 中的处理方式与普通方法没有什么不同。只有 JVM 的验证器才能确保构造函数调用另一个合法构造函数。除此之外,构造函数必须被称为 <init>,而类初始化器被称为 <clinit>,这只是一个 Java 变数命名原则。除了这个不同之外,方法和构造函数的表示也是相同的。正如 Holger 在注释中指出的,您甚至可以定义返回类型不同于 void的构造函数或带参数的类初始化器,即使不可能调用这些方法。

创建非对称记录 *

创建记录时

record Foo(Object bar) { }

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

调用任何 super 方法(直到 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"); }
}

在跳过 Bar#baz时,实现 Qux#baz来调用 Foo#baz是可能的。虽然仍然可以定义一个显式调用来调用另一个超级方法实现,而不是直接超级类的实现,但是在1.1之后的 Java 版本中,这不再有任何效果。在 Java 1.1中,这种行为是通过设置 ACC_SUPER标志来控制的,这将启用只调用直接超类的实现的相同行为。

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

在 Java 中,不可能定义一个类

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


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

当对 Bar的实例调用 foo时,上面的代码总是会导致 RuntimeException。不可能定义 Foo::foo方法来调用 Foo中定义的 foo1 bar方法。由于 bar是非私有实例方法,因此调用始终是虚拟的。然而,通过字节码,可以定义使用 INVOKESPECIAL操作码的调用,该操作码直接将 Foo::foo中的 bar方法调用链接到 Foo的版本。这个操作码通常用于实现超级方法调用,但是您可以重用这个操作码来实现所描述的行为。

细粒度类型注释

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

在 Java 中使用 synchronized块时,会隐式地创建类似的语句,在释放监视器时,任何异常都会返回到释放该监视器的指令。通常情况下,这样的指令不会出现异常,但是如果会出现异常(例如,已废弃的 ThreadDeath) ,监视器仍然会被释放。

调用任何默认方法

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

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

对于 Java 字节码,只有第二个条件才算数。

对不是 this的实例调用 super 方法

Java 编译器只允许在 this实例上调用 super (或接口默认值)方法。但是,在字节码中,也可以对类似于以下类型的同一类型的实例调用 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 版本中发现一个 bug,在这个版本中,验证器将错过这个问题,并且一旦调用指令,JVM 就会结束。尽管这几乎不是一个特性,但是从技术上来说,这是用 Javac编译的 Java 所不能实现的。Java 有某种双重验证。第一个验证由 Java 编译器应用,第二个验证由加载类时的 JVM 应用。通过跳过编译器,您可能会发现验证器验证中的一个弱点。不过,这只是一个笼统的陈述,而不是一个特性。

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

自 Java8以来,内部类的非静态方法和构造函数可以声明接收方类型并对这些类型进行注释。顶级类的构造函数不能注释它们的接收方类型,因为它们大多不声明接收方类型。

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

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

使用未使用的/遗留的字节码指令

既然有人这样命名,我就把它也包括进去。Java 以前通过 JSRRET语句使用子例程。为此,JBC 甚至知道自己的返回地址类型。然而,子程序的使用确实使静态程序分析过于复杂,这就是为什么这些指令不再使用的原因。相反,Java 编译器将复制它所编译的代码。然而,这基本上创造了相同的逻辑,这就是为什么我并不真的认为它实现了一些不同的东西。类似地,例如,您可以添加 Java 编译器也不使用的 NOOP字节码指令,但是这也不会真正允许您实现新的东西。正如在上下文中指出的,这些提到的“特性指令”现在已经从合法的操作码集中移除,这使得它们更加不是一个特性。

当我还是一个 I-Play 时,我编写了一个字节码优化器(它的设计目的是为了减少 J2ME 应用程序的代码大小)。我添加的一个特性是能够使用内联字节码(类似于 C + + 中的内联汇编语言)。通过使用 DUP 指令,我成功地减少了作为库方法一部分的函数的大小,因为我需要这个值两次。我也有零字节的指令(如果你调用一个方法,接受一个 char,你想传递一个 int,你知道不需要被强制转换,我添加 int2char (var)来替换 char (var) ,它会删除 i2c 指令来减少代码的大小。我还让它做了浮点数 a = 2.3; 浮点数 b = 3.4; 浮点数 c = a + b; 这将被转换为固定点(更快,还有一些 J2ME 不支持浮点数)。

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