JVM在校验阶段不检查接口的实现状况

继续看到底要运行一个Java程序需要做的各种检查是在什么时候发生的。这次我们来看看接口调用的问题。

当前的JVM规范中,与方法调用相关的指令有4个:invokevirtual、invokeinterface、invokestatic与invokespecial。其中调用接口方法时使用的JVM指令是invokeinterface。这个指令与另外3个方法调用指令有一个显著的差异:它不要求JVM的校验器(verifier)检查被调用对象(receiver)的类型;另外3个方法调用指令都要求校验被调用对象。也就是说,使用invokeinterface时如果被调用对象没有实现指定的接口,则应该在运行时而不是链接时抛出异常;而另外3个方法调用指令都要求在链接时抛出异常。

看看JVM规范是怎么说的:

Java Virtual Machine Specification, 2nd Edition 写道

invokeinterface

Runtime Exceptions

if the class of objectref does not implement the resolved interface, invokeinterface throws an IncompatibleClassChangeError.

可以留意一下另外3个方法调用指令中“IncompatibleClassChangeError”都是Linking Exception而不是Runtime Exception。

这种规定对Java程序来说可见的行为就是:如果一个方法通不过校验,则整个方法都不会被执行;如果能通过校验而抛出运行时异常,则方法当中抛出异常之前的部分都会被执行。

当然,我们直接用Java语言写出来的程序很难引发这样的错误,因为Java编译器会做检查来保证一定程度的类型安全。但是Java的class文件,或者说Java字节码可以由Java编译器以外的别的方式生成,此时就得不到Java编译器对类型安全的保证,而要依赖于JVM对字节码的校验以及运行时的检查了。

我是之前在读John Rose对JSR 292的invokedynamic的讲解时留意到invokeinterface的这个特点的。John特别提到invokedynamic就像invokeinterface一样,都不在校验时对被调用对象的类型做检查。不过之前一直没见过调用对一个没实现接口的对象调用接口方法实际是个什么样子。

好吧,这次就来看个例子。首先创建一个接口IFoo,一个实现了该接口的类FooImpl,和一个未实现该接口的类Bar:

IFoo.java:

Java代码

public interface IFoo {    void method();}

FooImpl.java:

Java代码

public class FooImpl implements IFoo {    public void method() {        System.out.println("FooImpl.method()");    }}

Bar.java:

Java代码

public class Bar {    public void anotherMethod() {        System.out.println("Bar.anotherMethod()");    }}

接下来构造出一个能引发运行时异常的程序。大致的意思是这样的:

Java代码

public class TestInterfaceCall {    public static void main(String[] args) {        IFoo f = new FooImpl();        f.method();        Bar b = new Bar();        ((IFoo)b).method(); // << watch this    }}

注意第7行代码。如果就这么写然后编译的话,生成的字节码里会有一个checkcast指令将Bar类型的引用转换为IFoo类型的引用。如果有checkcast的话,运行时就会在该指令上报错,因为Bar没有实现IFoo。但这次我想引发的错误不是强制转换相关,而是接口调用相关:想达到的效果是以b为被调用对象,但调用IFoo.method()而不是Bar上已有的方法。所以要靠自己来生成字节码,避免checkcast指令。

上个月的两个相关帖里我使用了ObjectWeb的ASM库来生成Java字节码。这个库很实用,但写起来还是繁琐了些。这次我决定用Charles Nutter写的bitescript。使用该库需要JRuby 1.2.0或更高的版本,我这次用的是JRuby 1.3.0RC2。

安装bitescript只要用JRuby的gem就行:

Command prompt代码

gem install bitescript

然后编写生成字节码用的脚本:

test.rb:

Ruby代码

require 'rubygems'require 'bitescript'include BiteScript.IFoo    = Java::IFooFooImpl = Java::FooImplBar     = Java::Barfb = FileBuilder.build(__FILE__) do  public_class 'TestInterfaceCall' do    public_static_method 'main', void, string[] do      # IFoo f = new FooImpl();      new FooImpl      dup      invokespecial FooImpl, '<init>', [void]      asTore 1      # f.method();      aload 1      invokeinterface IFoo, 'method', [void]            # Bar b = new Bar();      new Bar      dup      invokespecial Bar, '<init>', [void]      asTore 2            # ((IFoo)b).method();      aload 2      ## checkcast IFoo # skip the cast to trigger IncompatibleClassChangeError      invokeinterface IFoo, 'method', [void]      returnvoid    end  endendfb.generate do |filename, class_builder|  File.open(filename, 'w') do |file|    file.write(class_builder.generate)  endend

可以对比一下直接用ASM时的代码,显然用bitescript要简洁易懂得多。Good job, Charles!

执行这个脚本,把生成出来的TestInterfaceCall.class与前面的IFoo.class、FooImpl.class和Bar.class放在同一个目录下。然后运行java TestInterfaceCall,

Command prompt代码

D:/sdk/jruby-1.3.0RC2/test_bitescript>java TestInterfaceCallFooImpl.method()Exception in thread "main" java.lang.IncompatibleClassChangeError        at TestInterfaceCall.main(test.rb)

可以看到程序打印出了”FooImpl.method()”这句话,也就是说异常是在运行时而不是链接时抛出的。

如今用到Java的字节码改写/动态生成的工具已经很普遍了,如果在使用它们的时候不够小心,相信这里所提到的运行时异常也会有机会见到的 =v=

P.S. 我这次运行的环境是:

D:/sdk/jruby-1.3.0RC2/test_bitescript>java -versionjava version "1.6.0_11"Java(TM) SE Runtime Environment (build 1.6.0_11-b03)Java HotSpot(TM) Client VM (build 11.0-b16, mixed mode, sharing)

我不敢说我可以忘却,或者勇敢,坚强,等等等等一切堂皇而陈旧的字眼。

JVM在校验阶段不检查接口的实现状况

相关文章:

你感兴趣的文章:

标签云: