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

Bar*_*lom 144 java jvm bytecode

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

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

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

Raf*_*ter 395

在使用Java字节代码很长一段时间并对此事做了一些额外的研究之后,这里是我的发现的总结:

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

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

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

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

如前所述,在调用另一个构造函数之前设置实例的字段值是完全合法的.甚至存在一个遗留黑客,它使它能够在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();
  }
}
Run Code Online (Sandbox Code Playgroud)

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

分支超级构造函数调用

在Java中,无法定义构造函数调用

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

class Bar() {
  if(System.currentTimeMillis() % 2 == 0) {
    super();
  } else {
    super(null);
  }
}
Run Code Online (Sandbox Code Playgroud)

在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参考

finalJBC中不存在参数,因此可以重新分配.任何参数(包括this引用)仅存储在JVM中的简单数组中,允许在单个方法帧内的this索引处重新分配引用0.

重新分配final字段

只要在构造函数中指定了最终字段,重新分配此值或甚至根本不分配值是合法的.因此,以下两个构造函数是合法的:

class Foo {
  final int bar;
  Foo() { } // bar == 0
  Foo(Void v) { // bar == 2
    bar = 1;
    bar = 2;
  }
}
Run Code Online (Sandbox Code Playgroud)

对于static final字段,甚至允许在类初始化程序之外重新分配字段.

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

这更像是一个概念特征,但在JBC中构造函数的处理方式与常规方法不同.只有JVM的验证器才能确保构造函数调用另一个合法的构造函数.除此之外,它只是一个Java命名约定,必须调用构造<init>函数并调用类初始化程序<clinit>.除了这种差异,方法和构造函数的表示是相同的.正如Holger在注释中指出的那样,你甚至可以定义带有返回类型的构造函数void或带有参数的类初始值设定项,即使不可能调用这些方法.

调用任何超级方法(直到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"); }
}
Run Code Online (Sandbox Code Playgroud)

跳过时可以实现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();
  }
}
Run Code Online (Sandbox Code Playgroud)

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

细粒型注释

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

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

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

溢出和隐式分配byte,short,charboolean

后面的原始类型在JBC中通常不是已知的,但仅针对数组类型或字段和方法描述符定义.在字节代码指令中,所有命名类型都占用32位空间,这允许将它们表示为int.据官方统计,仅在int,float,longdouble字节代码全部由JVM的验证规则需要显式转换中存在的类型.

不要发布显示器

一个synchronized块实际上是由两种说法,一是收购和一个释放的监视器组成.在JBC中,您可以在不释放它的情况下获得一个.

注意:在最近的HotSpot实现中,IllegalMonitorStateException如果方法由异常本身终止,则会导致方法结束或隐式释放.

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

在Java中,甚至是一个简单的类型初始化程序,如

class Foo {
  static {
    return;
  }
}
Run Code Online (Sandbox Code Playgroud)

是非法的.在字节代码中,类型初始化程序被视为与任何其他方法一样,即返回语句可以在任何地方定义.

创建不可缩减的循环

Java编译器将循环转换为Java字节代码中的goto语句.这些语句可用于创建不可简化的循环,Java编译器从不这样做.

定义递归catch块

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

try {
  throw new Exception();
} catch (Exception e) {
  <goto on exception>
  throw Exception();
}
Run Code Online (Sandbox Code Playgroud)

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

调用任何默认方法

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

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

对于Java字节代码,只有第二个条件计数.然而,第一个是无关紧要的.

在不是的实例上调用超级方法 this

Java编译器只允许在实例上调用超级(或接口默认)方法this.但是,在字节代码中,也可以在类似于以下内容的相同类型的实例上调用super方法:

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

访问合成成员

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

class Foo {
  class Bar { 
    void bar(Bar bar) {
      Foo foo = bar.Foo.this;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

对于任何合成领域,类或方法通常都是如此.

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

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

验证程序不检查这些元数据String编码值的一致性.因此,可以定义与擦除不匹配的泛型类型的信息.作为一个概念,以下断言可能是真的:

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

Field field = ...
assertTrue(field.getFieldType() == String.class);
assertTrue(field.getGenericFieldType() == Integer.class);
Run Code Online (Sandbox Code Playgroud)

此外,签名可以定义为无效,以便抛出运行时异常.首次访问信息时会抛出此异常,因为它是懒惰评估的.(类似于带错误的注释值.)

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

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
}
Run Code Online (Sandbox Code Playgroud)

Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()但是,由于返回AnnotatedType表示Foo,因此可以Foo直接在类文件中包含类型注释,其中这些注释稍后由反射API读取.

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

由于其他人将其命名,我也会将其包括在内.Java以前通过JSRRET语句使用子例程.为此,JBC甚至知道它自己的返回地址类型.但是,子程序的使用确实使静态代码分析过于复杂,这就是不再使用这些指令的原因.相反,Java编译器将复制它编译的代码.然而,这基本上创造了相同的逻辑,这就是为什么我并不真正考虑它来实现不同的东西.类似地,您可以添加NOOPJava编译器未使用的字节代码指令,但这实际上不允许您实现新的东西.正如在上下文中指出的那样,这些提到的"特征指令"现在从合法的操作码集中删除,这使得它们更少地成为特征.

  • 关于方法名称,你可以通过定义名称为`<clinit>`但接受参数或具有非`void`返回类型的方法来拥有多个`<clinit>`方法.但是这些方法不是很有用,JVM会忽略它们,字节代码也不能调用它们.唯一的用途是混淆读者. (3认同)
  • 好的,[我找到了相关规范](http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.10):"*结构化锁定*是在方法调用期间,给定监视器上的每个出口与该监视器上的前一个条目匹配的情况.由于无法保证提交给Java虚拟机的所有代码都将执行结构化锁定,因此允许实施Java虚拟机,但不需要强制执行以下两个保证结构化锁定的规则......." (3认同)
  • 我刚刚发现,Oracle的JVM在方法出口处检测到未发布的监视器,如果省略了`monitorexit`指令,则会抛出"IllegalMonitorStateException".如果一个特殊的方法退出无法执行`monitorexit`,它会静默重置监视器. (2认同)

Joa*_*uer 61

据我所知,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编译器就无法生成这样的类文件.在这个答案中有一个很好的例子/解释.

  • 您可以使用几乎任何字符的类,方法和字段名称.我参与了一个项目,其中"字段"的名称中有空格和连字符.:P (8认同)
  • 我可以添加另一个答案,但我们不妨将其作为规范的答案.您可能要提到字节码*中的方法签名包括返回类型*.也就是说,您可以使用两种方法具有完全相同的参数类型,但返回类型不同.请参阅此讨论:http://stackoverflow.com/questions/3110014/is-this-valid-java/3110204#3110204 (5认同)
  • @Peter:说到文件系统字符,我遇到了一个混淆器,它在JAR文件中将一个类重命名为`a`,另一个重命名为`A`.在我意识到丢失的课程在哪里之前,我花了大约半个小时在Windows机器上解压**.:) (3认同)
  • @JoachimSauer:释义JVM规范,第75页:类名,方法,字段和局部变量可以包含*any*字符,除了''.'`,`';'`,`'['`或''/' `.方法名称相同,但它们也不能包含`'<'`或`'>'`.(例如`<init>`和`<clinit>`和静态构造函数的明显例外.)我应该指出,如果你严格遵循规范,那么类名实际上要受到更多限制,但约束是没有强制执行. (3认同)
  • @JoachimSauer:另外,我自己添加了一个未记录的:java语言包括``throws ex1,ex2,...,exn"`作为方法签名的一部分; 您不能向重写方法添加异常抛出子句.但是,JVM无所谓.因此,只有`final`方法才能真正保证JVM无异常 - 除了`RuntimeException`s和`Error`s,当然.对于经过检查的异常处理非常重要:D (3认同)
  • @ aboveyou00:即使是最终方法也不提供保证,因为在字节码级别上,无论是否声明其类型,它们都可能抛出任何异常. (2认同)

Esk*_*ola 13

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

  • 从方法中抛出已检查的异常,而不声明该方法将其抛出.已检查和未检查的异常只能由Java编译器检查,而不是JVM.因此,例如Scala可以在不声明方法的情况下从方法中抛出已检查的异常.虽然使用Java泛型,但有一个名为sneaky throw的解决方法.

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

  • 请注意,**是**在Java中做第一件事的方法.它有时被称为[偷偷摸摸](http://blog.jayway.com/2010/01/29/sneaky-throw/). (4认同)
  • 如果不在Java字节码中调用构造函数,则无法创建实例.验证程序将拒绝任何尝试使用未初始化实例的代码.对象反序列化实现使用本机代码帮助程序来创建没有构造函数调用的实例. (2认同)

Dan*_*art 7

  • GOTO可以与标签一起使用来创建自己的控制结构(除了for while等)
  • 您可以覆盖this方法内的局部变量
  • 结合这两个你可以创建创建尾调用优化字节码(我在JCompilo中这样做)

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

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

elj*_*nso 5

也许本文档中的第 7A 节是有趣的,尽管它是关于字节码陷阱而不是字节码特性


mse*_*ell 5

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

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

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