字节码function在Java语言中不可用
在Java字节码中,你现在可以做什么(Java 6),你不能在Java语言中做什么?
我知道两者都是图灵完整的,所以阅读“可以做”为“可以明显更快/更好,或者只是以不同的方式”。
我正在考虑像invokedynamic
这样的额外的字节码,除了特定的版本是未来的版本之外,它不能用Java生成。
据我所知,在Java 6支持的字节码中没有主要function,这些function也不能从Java源代码访问。 主要的原因很明显,Java字节码是用Java语言devise的。
有一些function不是由现代Java编译器产生的,但是:
-
ACC_SUPER
标志 :这是一个可以在类上设置的标志,并指定如何为这个类处理
invokespecial
字节码的特定invokespecial
情况。 它是由所有现代Java编译器(如果我没有记错的话,“现代”> = Java 1.1)设置,只有古老的Java编译器才会生成未设置的类文件。 这个标志仅用于向后兼容的原因。 请注意,从Java 7u51开始,ACC_SUPER由于安全原因而被完全忽略。 -
jsr
/ret
字节码。这些字节码被用来实现子例程(主要是为了实现
finally
块)。 从Java 6开始,它们不再生产 。 他们弃用的原因是他们把静态validation复杂化了很多,没有什么大的收获(即使用的代码几乎总是可以用很less的开销就可以用正常的跳转来重新实现)。 -
在只有返回types不同的类中有两个方法。
Java语言规范不允许同一类中的两个方法仅在返回types(即同名,同一个参数列表…)上有所不同。 但是,JVM规范没有这样的限制,所以一个类文件可以包含两个这样的方法,使用普通的Java编译器就无法生成这样的类文件。 这个答案中有一个很好的例子/解释。
在处理了Java字节码很长一段时间后,对这个问题做了一些额外的研究,下面是我的发现的总结:
调用超级构造函数或辅助构造函数之前,在构造函数中执行代码
在Java编程语言(JPL)中,构造函数的第一条语句必须是超级构造函数或同一类的另一个构造函数的调用。 Java字节码(JBC)不适用。 在字节码中,在构造函数之前执行任何代码是绝对合法的,只要:
- 在这个代码块之后的某个时候调用另一个兼容的构造函数。
- 这个调用不在一个条件语句中。
- 在这个构造函数调用之前,没有读取构造实例的字段,也没有调用它的任何方法。 这意味着下一个项目。
在调用超级构造函数或辅助构造函数之前设置实例字段
如前所述,在调用另一个构造函数之前设置实例的字段值是完全合法的。 甚至还存在一个传统的黑客攻击,使得它能够在6:
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虚拟机的validation者却错过了这个检查,这就是为什么这是可能的。 这被几个代码生成工具用来作为一种黑客,但是像这样实现一个类是不合法的。
后者只是这个编译器版本中的一个bug。 在较新的编译器版本中,这又是可能的。
定义一个没有任何构造函数的类
Java编译器将始终为任何类实现至less一个构造函数。 在Java字节码中,这不是必需的。 这允许创build即使使用reflection也不能构build的类。 但是,使用sun.misc.Unsafe
仍然允许创build这样的实例。
定义具有相同签名但具有不同返回types的方法
在JPL中,通过名称和原始参数types将方法标识为唯一。 在JBC中,原始的退货types是另外考虑的。
定义不同名称但不同types的字段
只要声明不同的字段types,类文件就可以包含多个相同名称的字段。 JVM始终将一个字段称为名称和types的元组。
抛出未声明的检查exception,而不捕捉它们
Java运行时和Java字节代码不知道检查exception的概念。 只有Java编译器validation检查的exception总是被捕获或被抛出。
在lambdaexpression式之外使用dynamic方法调用
所谓的dynamic方法调用可以用于任何事情,不仅适用于Java的lambdaexpression式。 使用此function可以在运行时切换执行逻辑。 许多dynamic编程语言归结为JBC,通过使用此指令提高了性能 。 在Java字节码中,您也可以模拟Java 7中的lambdaexpression式,其中编译器尚未允许任何使用dynamic方法调用,而JVM已经理解该指令。
使用通常不被认为合法的标识符
曾经想过在你的方法名称中使用空格和换行符? 创build您自己的JBC,并为代码审查好运。 标识符唯一的非法字符是.
, ;
, [
和/
。 此外,未命名为<init>
或<clinit>
方法不能包含<
和>
。
重新分配final
参数或this
参考
final
参数在JBC中不存在,因此可以重新分配。 任何参数(包括this
引用)都只存储在JVM中的一个简单数组中,允许在单个方法框架内的索引0
处重新分配this
引用。
重新分配final
字段
只要在构造函数中分配了最后一个字段,就可以重新分配这个值,甚至根本不分配一个值。 因此,以下两个构造函数是合法的:
class Foo { final int bar; Foo() { } // bar == 0 Foo(Void v) { // bar == 2 bar = 1; bar = 2; } }
对于static final
字段,甚至可以重新分配类初始值设定项之外的字段。
将构造函数和类初始化方法看作是方法
这更像是一个概念性的特征,但是JBC中的构造函数与普通方法没有任何区别。 只有JVM的validation者才能确保构造函数调用另一个合法的构造函数。 除此之外,它只是一个Java命名约定,构造函数必须被称为<init>
,并且类初始化方法称为<clinit>
。 除此之外,方法和构造函数的表示方式是相同的。 正如Holger在评论中指出的,即使不能调用这些方法,甚至可以使用返回types来定义除void
以外的返回types或具有参数的类初始化方法。
调用任何超级方法(直到Java 1.1)
但是,这仅适用于Java版本1和1.1。 在JBC中,方法总是以明确的目标types进行调度。 这意味着对于
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
来调用Foo#baz
同时跳过Bar#baz
。 尽pipe仍然有可能定义一个明确的调用来调用另一个超级方法实现,而不是直接超类的调用,但是在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
定义的自己的 bar
方法。 由于bar
是一个非私有的实例方法,所以这个调用总是虚拟的。 使用字节码,可以定义调用来使用INVOKESPECIAL
操作码,它将Foo::foo
的bar
方法调用直接链接到Foo
的版本。 这个操作码通常用于实现超级方法调用,但是您可以重用操作码来实现所描述的行为。
细粒度types的注释
在Java中,注释是根据注解声明的@Target
来应用的。 使用字节码操作,可以独立于此控件定义注释。 另外,即使@Target
注解适用于两个元素,也可以注释一个参数types而不注释参数。
定义types或其成员的任何属性
在Java语言中,只能为字段,方法或类定义注释。 在JBC中,基本上可以将任何信息embedded到Java类中。 为了利用这些信息,你可以不再依赖Java类加载机制,但是你需要自己提取元信息。
溢出并隐式分配byte
, short
, char
和boolean
值
后者的原始types在JBC中通常是不知道的,但是只能为数组types或字段和方法描述符定义。 在字节码指令中,所有指定的types都占用32位的空间,允许将它们表示为int
。 正式的,只有int
, float
, long
和double
types存在于字节码内,都需要JVM的validation者的规则进行明确的转换。
不释放显示器
一个synchronized
块实际上由两个语句组成,一个是获取和一个释放一个监视器。 在JBC,你可以获得一个而不会释放它。
注意 :在最近的HotSpot实现中,这会导致在方法结束时出现IllegalMonitorStateException
,或者如果方法被exception本身终止,则会导致隐式释放。
将多个return
语句添加到types初始值设定项中
在Java中,甚至是一个普通的types初始值设定项,如
class Foo { static { return; } }
是非法的。 在字节代码中,types初始值设定项就像其他方法一样处理,即返回语句可以在任何地方定义。
创build不可约循环
Java编译器将循环转换为Java字节代码中的goto语句。 这样的语句可以用来创build不可简化的循环,Java编译器从来不会这样做。
定义一个recursioncatch块
在Java字节码中,您可以定义一个块:
try { throw new Exception(); } catch (Exception e) { <goto on exception> throw Exception(); }
当在Java中使用synchronized
块时,隐式创build类似的语句,其中释放监视器的任何exception返回到释放此监视器的指令。 通常情况下,这样的指令不应该发生exception,但如果它(如不赞成的ThreadDeath
),监视器仍然会被释放。
调用任何默认方法
Java编译器需要满足几个条件才能允许默认方法的调用:
- 该方法必须是最具体的方法(不能被任何types实现的子接口覆盖,包括超types)。
- 默认方法的接口types必须由调用默认方法的类直接实现。 但是,如果接口
B
扩展接口A
但不覆盖A
中的方法,则该方法仍然可以被调用。
对于Java字节码,只有第二个条件是有效的。 然而,第一个是无关紧要的。
在不是this
的实例上调用超级方法
Java编译器只允许在this
实例上调用超级(或接口默认)方法。 在字节代码中,也可以在相同types的实例上调用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; } } }
对于任何合成领域,类别或方法来说,这通常是正确的。
定义不同步的通用types信息
虽然Java运行时不处理genericstypes(在Java编译器应用types擦除之后),但此信息仍作为元信息被编译到类中,并可通过reflectionAPI访问。
validation器不检查这些元数据String
编码值的一致性。 因此可以定义与擦除不匹配的genericstypes的信息。 作为一个顺序,以下说法可能是正确的:
Method method = ... assertTrue(method.getParameterTypes() != method.getGenericParameterTypes()); Field field = ... assertTrue(field.getFieldType() == String.class); assertTrue(field.getGenericFieldType() == Integer.class);
另外,可以将签名定义为无效,从而引发运行时exception。 当信息被第一次访问时,这个exception会被抛出,因为它被懒惰地评估。 (类似于带有错误的注释值。)
仅为某些方法附加参数元信息
Java编译器允许在编译启用parameter
标志的类时embedded参数名称和修饰符信息。 然而,在Java类文件格式中,这些信息是按照方法存储的,这使得仅仅为某些方法embedded这种方法信息成为可能。
搞砸了你的JVM
例如,在Java字节码中,可以定义调用任何types的任何方法。 通常情况下,如果一个types不知道这种方法,validation者会抱怨。 但是,如果你在数组上调用一个未知的方法,我发现了一些JVM版本的错误,在这个版本中,validation者会错过这个,一旦指令被调用,你的JVM就完成了。 虽然这不是一个特性,但是从技术上来讲, javac编译的Java是不可能的。 Java有一些双重validation。 第一个validation由Java编译器应用,第二个validation在加载类时由JVM应用。 通过跳过编译器,您可能会发现validation者validation中的一个弱点。 不过,这只是一个普遍的说法而已。
当没有外部类时,注释构造函数的接收器types
自Java 8以来,内部类的非静态方法和构造函数可以声明接收器types并注释这些types。 顶级类的构造函数不能注释它们的接收器types,因为它们最不声明。
class Foo { class Bar { Bar(@TypeAnnotation Foo Foo.this) { } } Foo() { } // Must not declare a receiver type }
因为Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
不会返回表示Foo
的AnnotatedType
,所以可以在类文件中直接包含Foo
构造函数的types注释,这些注释稍后将被reflectionAPI读取。
使用未使用/传统的字节码指令
由于其他人的命名,我也将其包括在内。 Java以前使用JSR
和RET
语句的子程序。 JBC甚至为此知道它自己的返回地址types。 但是,子程序的使用并没有过分复杂的静态代码分析,这就是为什么这些指令不再被使用的原因。 相反,Java编译器会复制它编译的代码。 然而,这基本上创造了相同的逻辑,这就是为什么我不认为它实现了不同的东西。 同样的,例如你可以添加一个Java编译器没有使用的NOOP
字节码指令,但是这并不能让你实现新的function。 正如在上下文中指出的那样,这些提到的“特性指令”现在被从一组合法操作码中去除,这使得它们甚至不具有特征。
以下是可以在Java字节码中完成的一些function,但不能在Java源代码中完成:
-
从方法中抛出一个检查的exception而不声明该方法抛出它。 checked和uncheckedexception是只有Java编译器而不是JVM检查的事情。 正因为如此,Scala可以从方法中抛出检查exception而不用声明它们。 虽然Javagenerics有一个叫做偷偷摸摸的解决方法。
-
在一个只有返回types不同的类中有两个方法,正如Joachim的回答中已经提到的那样:Java语言规范不允许在同一个类中有两个方法, 只是它们的返回types不同(即相同的名称,相同的参数列表, …)。 但是,JVM规范没有这样的限制,所以一个类文件可以包含两个这样的方法,使用普通的Java编译器就无法生成这样的类文件。 这个答案中有一个很好的例子/解释。
-
GOTO
可以与标签一起使用来创build自己的控制结构(除了等等) - 你可以在方法中覆盖
this
局部variables - 结合这两个你可以创build创build尾部调用优化字节码(我在JCompilo中这样做)
作为一个相关点,你可以得到方法的参数名称,如果编译debugging( Paranamer通过读取字节码来做到这一点
也许本文档中的第7A部分是有趣的,尽pipe它是关于字节码的缺陷而不是字节码的特性 。
在Java语言中,构造函数中的第一条语句必须是对超类构造函数的调用。 字节码没有这个限制,相反,规则是在访问成员之前,必须为对象调用超类构造函数或同一类中的另一个构造函数。 这应该允许更多的自由,例如:
- 创build另一个对象的实例,将其存储在局部variables(或堆栈)中,并将其作为parameter passing给超类构造函数,同时仍将引用保留在该variables中供其他用途使用。
- 根据条件调用不同的其他构造函数。 这应该是可能的: 如何在Java中有条件地调用不同的构造函数?
我没有testing过这些,所以如果我错了,请纠正我。
你可以使用字节码而不是简单的Java代码,这是生成代码,可以在没有编译器的情况下加载和运行。 许多系统具有JRE而不是JDK,如果要dynamic生成代码,则可能更好(如果不是更简单)生成字节码,而不是Java代码,则必须在可以使用之前进行编译。
当我是一个I-Play时,我写了一个字节码优化器(它被devise为减lessJ2ME应用程序的代码大小)。 我添加的一个function是能够使用内联字节码(类似于C ++中的内联汇编语言)。 我设法通过使用DUP指令来减less作为库方法一部分的函数的大小,因为我需要两次这个值。 我也有零字节指令(如果你正在调用一个方法,需要一个字符,你想传递一个int,你知道不需要被投掷我添加int2char(var)来replace字符(var),它会删除我也做了float a = 2.3; float b = 3.4; float c = a + b;那会转换成定点的(更快一些,也有一些J2ME没有支持浮点)。