在使用 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, the HotSpot VM's verifier did however miss this check which is why it was possible. This was used by several code generation tools as a sort of a hack but it is not longer legal to implement a class like this.
后者只是这个编译器版本中的一个错误。在较新的编译器版本中,这又是可能的。
定义一个没有任何构造函数的类
Java 编译器将始终为任何类实现至少一个构造函数。在 Java 字节码中,这不是必需的。这允许创建即使使用反射也无法构造的类。然而,使用sun.misc.Unsafe
仍然允许创建此类实例。
定义具有相同签名但返回类型不同的方法
在 JPL 中,方法通过其名称及其原始参数类型来标识为唯一。在 JBC 中,还考虑了原始返回类型。
定义名称不同但类型不同的字段
一个类文件可以包含多个同名的字段,只要它们声明不同的字段类型即可。 JVM 始终将字段引用为名称和类型的元组。
抛出未声明的检查异常而不捕获它们
Java 运行时和 Java 字节码不知道检查异常的概念。只有 Java 编译器会验证检查异常是否始终被捕获或在抛出时被声明。
在 lambda 表达式之外使用动态方法调用
所谓的动态方法调用 https://stackoverflow.com/q/6638735/1237575可以用于任何东西,而不仅仅是 Java 的 lambda 表达式。例如,使用此功能可以在运行时切换执行逻辑。许多动态编程语言都可以归结为 JBC提高了他们的表现 http://groovy.codehaus.org/InvokeDynamic+support通过使用此指令。在 Java 字节代码中,您还可以在 Java 7 中模拟 lambda 表达式,其中编译器尚未允许使用动态方法调用,而 JVM 已经理解该指令。
使用通常不被视为合法的标识符
曾经想过在方法名称中使用空格和换行符吗?创建您自己的 JBC,祝代码审查好运。标识符唯一的非法字符是.
, ;
, [
and /
。此外,未命名的方法<init>
or <clinit>
不能包含<
and >
.
重新分配final
参数或this
参考
final
JBC 中不存在参数,因此可以重新分配。任何参数,包括this
引用仅存储在 JVM 内的一个简单数组中,这允许重新分配this
索引处的参考0
在单个方法框架内。
重新分配final
fields
只要在构造函数中分配了最终字段,重新分配该值甚至根本不分配值都是合法的。因此,以下两个构造函数是合法的:
class Foo {
final int bar;
Foo() { } // bar == 0
Foo(Void v) { // bar == 2
bar = 1;
bar = 2;
}
}
For static final
字段,甚至允许重新分配范围之外的字段
类初始值设定项。
将构造函数和类初始值设定项视为方法
这更多的是一个概念特征但在 JBC 中,构造函数的处理方式与普通方法没有任何不同。只有 JVM 的验证程序才能确保构造函数调用另一个合法的构造函数。除此之外,这只是 Java 命名约定,必须调用构造函数<init>
并且调用了类初始值设定项<clinit>
。除了这种差异之外,方法和构造函数的表示是相同的。正如 Holger 在评论中指出的那样,您甚至可以定义具有除void
或带有参数的类初始值设定项,即使无法调用这些方法。
创建不对称记录*.
创建记录时
record Foo(Object bar) { }
javac 将生成一个类文件,其中包含一个名为bar
,一个名为的访问器方法bar()
和一个构造函数采用一个Object
。此外,还有一个记录属性bar
被添加。通过手动生成记录,可以创建不同的构造函数形状,以跳过该字段并以不同的方式实现访问器。同时,仍然可以使反射 API 相信该类代表实际记录。
调用任何超级方法(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"); }
}
有可能实施Qux#baz
调用Foo#baz
跳过时Bar#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();
}
}
上面的代码总是会产生一个RuntimeException
when foo
在以下实例上调用Bar
。无法定义Foo::foo
调用方法its own bar
方法定义在Foo
. As bar
是非私有实例方法,调用始终是虚拟的。然而,使用字节码,我们可以定义调用以使用INVOKESPECIAL
直接链接的操作码bar
方法调用Foo::foo
to Foo
的版本。此操作码通常用于实现超级方法调用,但您可以重用该操作码来实现所描述的行为。
细粒度类型注释
在Java中,注释是根据它们的用途来应用的@Target
注释声明的。使用字节码操作,可以独立于该控件定义注释。此外,例如可以注释参数类型而不注释参数,即使@Target
注释适用于这两个元素。
为类型或其成员定义任何属性
在 Java 语言中,只能为字段、方法或类定义注释。在 JBC 中,您基本上可以将任何信息嵌入到 Java 类中。然而,为了利用这些信息,您可以不再依赖Java类加载机制,而是需要自己提取元信息。
溢出和隐式赋值byte
, short
, char
and boolean
values
后面的基本类型在 JBC 中通常不为人所知,而是仅为数组类型或字段和方法描述符定义的。在字节码指令中,所有命名类型都占用 32 位空间,这允许将它们表示为int
。官方规定,只有int
, float
, long
and double
类型存在于字节代码中,它们都需要根据 JVM 验证器的规则进行显式转换。
不释放监视器
A synchronized
block实际上由两条语句组成,一条用于获取监视器,另一条用于释放监视器。在 JBC 中,您无需释放即可获取它。
Note:在最近的 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();
}
使用 a 时会隐式创建类似的语句synchronized
Java 中的块,其中释放监视器时的任何异常都会返回到释放此监视器的指令。通常,此类指令不应发生异常,但如果发生异常(例如已弃用的ThreadDeath
),监视器仍然会被释放。
调用任何默认方法
Java 编译器需要满足几个条件才能允许调用默认方法:
- 该方法必须是最具体的方法(不得被实现的子接口覆盖)any类型,包括超类型)。
- 默认方法的接口类型必须由调用默认方法的类直接实现。但是,如果接口
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";
}
}
访问合成成员
在 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 版本中发现了一个错误,验证程序将错过此错误,并且一旦调用指令,您的 JVM 将完成。虽然这很难说是一个功能,但从技术上来说这是不可能的javac编译的Java。 Java 有某种双重验证。第一个验证由 Java 编译器应用,第二个验证由 JVM 在加载类时应用。通过跳过编译器,您可能会发现验证器验证中的弱点。不过,这只是一个笼统的说法,而不是一个功能。
当没有外部类时,注释构造函数的接收者类型
从 Java 8 开始,内部类的非静态方法和构造函数可以声明接收者类型并注释这些类型。顶级类的构造函数无法注释其接收者类型,因为它们大多数不声明接收者类型。
class Foo {
class Bar {
Bar(@TypeAnnotation Foo Foo.this) { }
}
Foo() { } // Must not declare a receiver type
}
Since Foo.class.getDeclaredConstructor().getAnnotatedReceiverType()
但是会返回一个AnnotatedType
代表Foo
,可以包含类型注释Foo
的构造函数直接在类文件中,这些注释稍后由反射 API 读取。
使用未使用/遗留的字节码指令
既然其他人都这么命名了,我也将它包括在内。 Java 以前使用子例程JSR
and RET
声明。为此,JBC 甚至知道自己的返回地址类型。然而,子例程的使用确实使静态代码分析过于复杂,这就是不再使用这些指令的原因。相反,Java 编译器将重复其编译的代码。然而,这基本上创建了相同的逻辑,这就是为什么我并不真正认为它能实现不同的目标。同样,您可以例如添加NOOP
Java 编译器也不使用字节码指令,但这也不会真正让您实现新的东西。正如上下文中所指出的,这些提到的“功能指令”现在已从合法操作码集中删除,这确实使它们不再是一个功能。