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

问题

目前(Java 6)你可以在Java字节码中使用Java语言无法做到的事情吗?

我知道两者都是图灵完整的,所以读"可以做"就像"可以做得更快/更好,或者只是以不同的方式"。

我正在考虑额外的字节码,如invokedynamic,这些字节码不能使用Java生成,除了特定的字节码是针对未来的版本。


#1 热门回答(384 赞)

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

Until Java 7u23,HotSpot VM的验证程序确实错过了这个检查,这就是可能的原因。一些代码生成工具使用它作为一种黑客攻击,但实现这样的类不再合法。
后者只是这个编译器版本中的一个错误。在较新的编译器版本中,这也是可能的。
定义一个没有任何构造函数的类
Java编译器将始终为任何类实现至少一个构造函数。在Java字节代码中,这不是必需的。这允许创建即使在使用反射时也无法构造的类。但是,使用sun.misc.Unsafestill可以创建此类实例。
定义具有相同签名但具有不同返回类型的方法
在JPL中,方法通过其名称及其原始参数类型被标识为唯一。在JBC中,还考虑了原始返回类型。
定义不按名称但仅按类型不同的字段
类文件可以包含多个同名字段,只要它们声明不同的字段类型即可。 JVM始终将字段称为名称和类型的元组。
抛出未声明的已检查异常而不捕获它们
Java运行时和Java字节代码不知道已检查异常的概念。只有Java编译器才会验证如果抛出已检查的异常,则始终捕获或声明它们。
在lambda表达式之外使用动态方法调用
所谓的dynamic method invocation可以用于任何事情,不仅适用于Java的lambda表达式。使用此功能允许例如在运行时切换执行逻辑。许多动态编程语言使用此指令归结为JBCimproved their performance。在Java字节代码中,你还可以在Java 7中模拟lambda表达式,其中编译器在JVM已经理解该指令时尚未允许使用动态方法调用。
使用通常不被视为合法的标识符
曾经想过在你的方法名称中使用空格和换行符吗?创建自己的JBC并祝好运代码审查。标识符的唯一非法字符是.,;,[/。此外,未命名为<init><clinit>的方法不能包含<>
重新分配final参数或this参考
JBC中不存在final个参数,因此可以重新分配。任何参数(包括this引用)仅存储在JVM内的简单数组中,允许在单个方法帧中将index0处的this引用重新分配。
Reassignfinalfields
只要在构造函数中分配了最终字段,重新分配此值或甚至根本不分配值是合法的。因此,以下两个构造函数是合法的:

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

对于static final字段,甚至允许重新分配类初始化程序之外的字段。
将构造函数和类初始值设定项视为方法
这更像是一种常见的功能,但在JBC中,构造函数与常规方法的处理方式没有任何区别。只有JVM的验证程序才能确保构造函数调用另一个合法的构造函数。除此之外,它只是一个Java命名约定,构造函数必须被称为<init>,并且类初始化程序被称为<clinit>。除了这种差异,方法和构造函数的表示是相同的。正如霍尔格在评论中指出的那样,你可以即使不能调用这些方法,甚至可以定义带有除了2885408633之外的返回类型的构造函数或带有参数的类初始值设定项。
调用任何超级方法(直到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到invokeFoo#baz。虽然仍然可以定义一个显式调用来调用另一个超级方法实现而不是直接超类的实现,但是在1.1之后的Java版本中它不再有任何影响。在Java 1.1中,通过设置ACC_SUPERflag来控制此行为,该标志将启用仅调用直接超类的实现的相同行为。
定义在同一类中声明的方法的非虚拟调用
在Java中,无法定义类

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

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

以上代码将始终导致在实例为Bar上调用aRuntimeExceptionwhenfoo。无法定义Foo::foo方法以调用在198987031中定义的ownbar方法。 Asbar是一个非私有实例方法,该调用始终是虚拟的。但是,使用字节代码,可以定义调用以使用INVOKESPECIALopcode,该代码直接链接bar方法调用inFoo::footoFoo的版本。此操作码通常用于实现超级方法调用,但你可以重用操作码来实现所描述的行为。
细粒度注释
在Java中,注释根据注释声明的@Target应用。使用字节代码操作,可以独立于此控件定义注释。此外,例如,即使@Target注释适用于两个元素,也可以在不注释参数的情况下注释参数类型。
为类型或其成员定义任何属性
在Java语言中,只能为字段,方法或类定义注释。在JBC中,你基本上可以将任何信息嵌入到Java类中。但是,为了利用这些信息,你可以不再依赖Java类加载机制,而是需要自己提取元信息。
溢出并隐式赋值byte,short,charboolean
后一种原始类型在JBC中通常不是已知的,但仅为数组类型或字段和方法描述符定义。在字节代码指令中,所有命名类型占用32位空间,允许将它们表示为int。正式地说,字节代码中只存在int,float,longdouble类型,它们都需要通过JVM验证程序的规则进行显式转换。
不释放监视器
Asynchronizedblock实际上由两个语句组成,一个用于获取,另一个用于释放监视器。在JBC中,你可以在不释放它的情况下获得一个。

注意:在最近的HotSpot实现中,如果方法由异常本身终止,则会导致方法结束时的IllegalMonitorStateException或隐式释放。
将多个return语句添加到类型初始值设定项中
在Java中,甚至是一个简单的类型初始化程序,如

class Foo {
  static {
    return;
  }
}

是非法的。在字节代码中,类型初始化程序被视为与任何其他方法一样,即返回语句可以在任何地方定义。
创建不可约的循环
Java编译器将循环转换为Java字节代码中的goto语句。这些语句可用于创建不可简化的循环,Java编译器从不这样做。
定义递归catch块
在Java字节代码中,你可以定义一个块:

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

在Java中使用asynchronizedblock时会隐式创建类似的语句,其中释放监视器时的任何异常都会返回到释放此监视器的指令。通常情况下,这样的指令不会发生异常,但如果是这样的话(例如deprecatedThreadDeath),监视器仍然会被释放。
调用任何默认方法
Java编译器需要满足几个条件才能允许默认方法的调用:

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

对于Java字节代码,只有第二个条件计数。然而,第一个是无关紧要的。
在非this的实例上调用super方法
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编译器允许在编译启用了parameterflag的类时嵌入参数名称和修饰符信息。然而,在Java类文件格式中,该信息是按方法存储的,这使得仅为某些方法嵌入这样的方法信息成为可能。
搞砸了JVM并使其崩溃
例如,在Java字节代码中,你可以定义调用任何类型的任何方法。通常,如果类型不知道这种方法,验证者会抱怨。但是,如果在数组上调用未知方法,我在某些JVM版本中发现了一个错误,其中验证程序将错过此错误,并且一旦调用指令,你的JVM就会完成。这不是一个功能,但从技术上来说,这是javaccompiled 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()返回anAnnotatedType表示Foo后,可以直接在类文件中包含Foo构造函数的类型注释,这些注释稍后将由反射API读取。
使用未使用/传统字节代码指令
由于其他人将其命名,我也会将其包括在内。 Java以前使用的是JSRRET语句的子程序。为此,JBC甚至知道它自己的返回地址类型。但是,子程序的使用确实使静态代码分析过于复杂,这就是不再使用这些指令的原因。相反,Java编译器将复制它编译的代码。然而,这基本上创造了相同的逻辑,这就是为什么我没有真正考虑它来实现不同的东西。类似地,你可以例如添加Java编译器未使用的NOOPbyte代码指令,但这实际上不允许你实现新的东西。正如在上下文中指出的那样,这些提到的"特征指令"现在从合法的操作码集中删除,这使得它们更少地成为特征。


#2 热门回答(58 赞)

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

但是,有些功能不是由现代Java编译器生成的:

  • ACC_SUPER标志:这是一个可以在类上设置的标志,它指定如何为该类处理invokes特殊字节码的特定角点情况。它是由所有现代Java编译器设置的(其中"现代"是> = Java 1.1,如果我没记错的话)并且只有古老的Java编译器生成了这个未设置的类文件。此标志仅出于向后兼容性原因而存在。请注意,从Java 7u51开始,由于安全原因,ACC_SUPER将被完全忽略。
  • jsr / ret字节码。这些字节码用于实现子例程(主要用于实现finally块)。它们不再是自Java 6以来产生的。它们被弃用的原因是它们使静态验证复杂化而没有很大的好处(即使用的代码几乎总是可以通过正常跳转重新实现而且开销很小)。
  • 在类中只有两种方法只有不同的返回类型。当Java语言规范的返回类型(即同名,相同的参数列表,......)不同时,它们不允许同一类中的两个方法。但是,JVM规范没有这样的限制,因此类文件可以包含两个这样的方法,使用普通的Java编译器就无法生成这样的类文件。在这个答案中有一个很好的例子/解释。

#3 热门回答(12 赞)

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

  • 从方法中抛出已检查的异常,而不声明方法将其抛出。已检查和未检查的异常只能由Java编译器检查,而不是JVM。因此,例如Scala可以在不声明方法的情况下从方法中抛出已检查的异常。虽然使用Java泛型,但有一个名为sneaky throw的解决方法。
  • 在类中只有两个方法只有不同的返回类型,如Joachim的答案中所述:Java语言规范不允许同一类中的两个方法只有它们的返回类型不同(即同名,相同的参数列表) ,...)。但是,JVM规范没有这样的限制,因此类文件可以包含两个这样的方法,使用普通的Java编译器就无法生成这样的类文件。在这个答案中有一个很好的例子/解释。