首页 文章

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

提问于
浏览
139

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

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

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

9 回答

  • 12

    我在I-Play时编写了一个字节码优化器(它旨在减少J2ME应用程序的代码大小) . 我添加的一个功能是使用内联字节码的能力(类似于C中的内联汇编语言) . 我设法通过使用DUP指令减少作为库方法一部分的函数的大小,因为我需要两次该值 . 我也有零字节指令(如果你正在调用一个接受char的方法而你想传递一个int,你知道不需要进行转换我添加了int2char(var)来替换char(var)它会删除i2c指令减小了代码的大小 . 我也做了浮点数a = 2.3;浮点数b = 3.4;浮点数c = ab;并且它将被转换为定点(更快,也有些J2ME不支持浮动点) .

  • 59

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

    • 创建另一个对象的实例,将其存储在局部变量(或堆栈)中,并将其作为参数传递给超类构造函数,同时仍保留该变量中的引用以供其他用途 .

    • 根据条件调用其他不同的构造函数 . 这应该是可能的:How to call a different constructor conditionally in Java?

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

  • 386

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

    • Throwing a checked exception from a method without declaring that the method throws it. 已检查和未检查的异常只能由Java编译器检查,而不是JVM . 因此,例如Scala可以在不声明方法的情况下从方法中抛出已检查的异常 . 虽然使用Java泛型,但有一个名为sneaky throw的变通方法 .

    Joachim's answer中已经提到的

    • Having two methods in a class that only differ in return type, :Java语言规范不允许同一类中的两个方法只有它们的返回类型(即同名,相同的参数列表,......)不同 . 但是,JVM规范没有这样的限制,所以类文件可以包含两个这样的方法,'s just no way to produce such a class file using the normal Java compiler. There'是this answer中的一个很好的例子/解释 .
  • 3
    • GOTO 可以与标签一起使用来创建自己的控制结构( for while 等除外)

    • 您可以覆盖方法内的 this 局部变量

    • 结合这两个你可以创建创建尾调用优化字节码(我在JCompilo中这样做)

    作为相关点,如果使用debug编译,则可以获取方法的参数名称(Paranamer does this by reading the bytecode

  • 6

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

  • 1

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

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

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

    • jsr / ret 字节码 .

    这些字节码用于实现子例程(主要用于实现 finally 块) . 他们是no longer produced since Java 6 . 它们被弃用的原因是它们很大程度上使静态验证复杂化而没有很大的收益(即使用的代码几乎总是可以通过正常跳转重新实现而且开销很小) .

    • 在类中有两个方法,它们的返回类型不同 .

    当Java语言规范的返回类型(即同名,相同的参数列表,......)不同时,它们不允许同一类中的两个方法 . 但是JVM规范没有这样的限制,所以一个类文件可以包含两个这样的方法,那里有一个很好的方法's just no way to produce such a class file using the normal Java compiler. There'this answer中的示例/说明 .

  • 4

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

    Execute code in a constructor before calling a super constructor or auxiliary constructor

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

    • 在此代码块之后的某个时间调用另一个兼容的构造函数 .

    • 此调用不在条件语句中 .

    • 在此构造函数调用之前,不会读取构造实例的任何字段,也不会调用其任何方法 . 这意味着下一个项目 .

    Set instance fields before calling a super constructor or auxiliary constructor

    如前所述,在调用另一个构造函数之前设置实例的字段值是完全合法的 . 甚至存在一个传统的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中,仍然可以实现此行为 .

    Branch a super constructor call

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

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

    在Java 7u23之前,HotSpot VM的验证程序确实错过了这个检查,这就是为什么它是可能的 . 一些代码生成工具使用它作为一种黑客攻击,但实现这样的类不再合法 .

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

    Define a class without any constructor

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

    Define methods with identical signature but with different return type

    在JPL中,方法通过其名称及其原始参数类型被标识为唯一 . 在JBC中,还考虑了原始返回类型 .

    Define fields that do not differ by name but only by type

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

    Throw undeclared checked exceptions without catching them

    Java运行时和Java字节代码不知道已检查异常的概念 . 只有Java编译器才会验证如果抛出已检查的异常,则始终捕获或声明它们 .

    Use dynamic method invocation outside of lambda expressions

    所谓的dynamic method invocation可用于任何事情,不仅适用于Java的lambda表达式 . 使用此功能允许例如在运行时切换执行逻辑 . 许多动态编程语言通过使用此指令归结为JBC improved their performance . 在Java字节代码中,您还可以在Java 7中模拟lambda表达式,其中编译器在JVM已经理解该指令时尚未允许使用动态方法调用 .

    Use identifiers that are not normally considered legal

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

    Reassign final parameters or the this reference

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

    Reassign final fields

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

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

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

    Treat constructors and the class initializer as if they were methods

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

    Call any super method (until 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 标志来控制此行为,该标志将启用仅调用直接超类的实现的相同行为 .

    Define a non-virtual call of a method that is declared in the same class

    在Java中,无法定义类

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

    Bar 的实例上调用 foo 时,上述代码将始终生成 RuntimeException . 无法定义 Foo::foo 方法来调用 Foo 中定义的自己的 bar 方法 . 由于 bar 是非私有实例方法,因此调用始终是虚拟的 . 但是,使用字节代码,可以定义调用以使用 INVOKESPECIAL 操作码,该操作码直接将 Foo::foo 中的 bar 方法调用链接到 Foo 的版本 . 此操作码通常用于实现超级方法调用,但您可以重用操作码来实现所描述的行为 .

    Fine-grain type annotations

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

    Define any attribute for a type or its members

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

    Overflow and implicitly assign byte, short, char and boolean values

    后一种原始类型在JBC中通常不是已知的,但仅为数组类型或字段和方法描述符定义 . 在字节代码指令中,所有命名类型都占用32位空间,这允许将它们表示为 int . 正式地说,字节代码中只存在 intfloatlongdouble 类型,这些类型都需要通过JVM验证程序的规则进行显式转换 .

    Not release a monitor

    synchronized 块实际上由两个语句组成,一个用于获取,一个用于释放监视器 . 在JBC中,您可以在不释放它的情况下获得一个 .

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

    Add more than one return statement to a type initializer

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

    class Foo {
      static {
        return;
      }
    }
    

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

    Create irreducible loops

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

    Define a recursive catch block

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

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

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

    Call any default method

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

    • 该方法必须是最具体的方法(不得被 any 类型实现的子接口覆盖,包括超类型) .

    • 默认方法的接口类型必须由调用默认方法的类直接实现 . 但是,如果接口 B 扩展接口 A 但未覆盖 A 中的方法,则仍可以调用该方法 .

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

    Invoke a super method on an instance that is not this

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

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

    Access synthetic members

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

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

    对于任何合成领域,类或方法,这通常都是正确的 .

    Define out-of-sync generic type information

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

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

    Method method = ...
    assertTrue(method.getParameterTypes() != method.getGenericParameterTypes());
    
    Field field = ...
    assertTrue(field.getFieldType() == String.class);
    assertTrue(field.getGenericFieldType() == Integer.class);
    

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

    Append parameter meta information only for certain methods

    Java编译器允许在编译启用了 parameter 标志的类时嵌入参数名称和修饰符信息 . 然而,在Java类文件格式中,该信息是按方法存储的,这使得仅为某些方法嵌入这样的方法信息成为可能 .

    Mess things up and hard-crash your JVM

    例如,在Java字节代码中,您可以定义调用任何类型的任何方法 . 通常,如果类型不知道这种方法,验证者会抱怨 . 但是,如果在数组上调用未知方法,我在某些JVM版本中发现了一个错误,其中验证程序将错过此错误,并且一旦调用指令,您的JVM就会完成 . 这虽然不是一个特性,但从技术上讲,这是javac编译Java无法实现的 . Java有一些双重验证 . 第一个验证由Java编译器应用,第二个验证由JVM在加载类时应用 . 通过跳过编译器,您可能会在验证程序的验证中发现一个弱点 . 不过,这是一个通用陈述,而不是一个特征 .

    Annotate a constructor's receiver type when there is no outer class

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

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

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

    Use unused / legacy byte code instructions

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

  • 2

    您可以使用字节代码而不是普通的Java代码来生成可以在没有编译器的情况下加载和运行的代码 . 许多系统都有JRE而不是JDK,如果你想动态生成代码,生成字节代码可能会更好,如果不是更容易,而不是Java代码必须先编译才能使用 .

  • 0

    也许this document中的第7A节是有意义的,虽然它是关于字节码 pitfalls 而不是字节码 features .

相关问题