`
RednaxelaFX
  • 浏览: 3015971 次
  • 性别: Icon_minigender_1
  • 来自: 海外
社区版块
存档分类
最新评论

为什么JVM与CLR都不对接口方法调用做静态校验?

阅读更多
作者:RednaxelaFX
主页:http://rednaxelafx.iteye.com
日期:2009-06-02

系列笔记:
JVM在校验阶段不检查接口的实现状况
CLR上的接口调用也是在运行时检查的

前面两帖我们看到JVM与CLR这两个主流的高级语言虚拟机都对接口方法调用管得很松,不对其被调用对象做静态类型校验。可是为什么呢?

==================================================================

这个问题先放在一边,让我们来看看如果让JVM执行一段字节码,其中调用了不存在的虚方法会怎样。还是用bitescript来生成class文件:
require 'rubygems'
require 'bitescript'
include BiteScript

JObject = java.lang.Object
JString = java.lang.String

fb = FileBuilder.build(__FILE__) do
  public_class 'TestVirtualCall' do
    public_static_method 'main', void, string[] do
      # Object o = new Object();
      new JObject
      dup
      invokespecial JObject, '<init>', [void]
      astore 1
      
      # int i = ((String)o).length();
      aload 1
      ## checkcast JString                   # without this cast, there will be a
      invokevirtual JString, 'length', [int] # VerifyError at verification time
      istore 2
      
      returnvoid
    end
  end
end

fb.generate do |filename, class_builder|
  File.open(filename, 'w') do |file|
    file.write(class_builder.generate)
  end
end

执行这段脚本生成TestVirtualCall.class,执行之,会看到:
Exception in thread "main" java.lang.VerifyError: (class: TestVirtualCall, method: main signature: ([Ljava/lang/String;)V) Incompatible object argument for function call
Could not find the main class: TestVirtualCall.  Program will exit.

也就是说JVM在校验的时候就检查到了错误:要调用的length()是String类上的,而引用是Object类型的,不是String或其派生类,于是被认为校验失败。

JVM与CLR都以支持静态的面向对象类型为主。由于基类上所有实例方法中非私有的方法都自动会被派生类所继承,所以可以保证这些方法在派生类上肯定存在(有没有被覆盖没关系)。持有某类型的引用时,即便实际指向的是这个类的派生类的实例,至少调用该类型上的方法都肯定没问题。所以JVM与CLR对虚方法调用的限制都可以做得比较严格,可以根据引用的类型(而不是实际实例的类型)来做校验。
虽说这种严格性也使得一些本来可以成功的调用在校验时会被拒绝,像这样:
// Object o = (Object)new ArrayList();
new Ljava/util/ArrayList;
dup
invokespecial java/util/ArrayList."<init>":()V
checkcast java/lang/Object // << ensures the JVM think local 1 is of type Object
astore 1

// o.size(); // note that o actually points to an instance of ArrayList
aload 1
invokevirtual java/util/ArrayList.size:()I

上面这段字节码的最后一条指令在校验的时候会被认为不合法,因为静态校验会认为o的类型是o,与ArrayList.size()要求的被调用对象类型不匹配。但我们通过脑内推导可以看出这里的o实际指向的是ArrayList的实例,这个调用本来应该可以成功的。但JVM不买这帐,宁可认定一些貌似可以貌似不行的状况为不合法,以保证VM的正确性的可证明性。

==================================================================

那调用接口方法的状况呢?

JVM与CLR的类型系统中,接口可以被任何类所实现。基类实现的接口派生也必须实现;基类没有实现的接口,派生类照样可以实现。于是,当我们持有某个类型A的引用时,如果该类型自身并没有实现某接口IB,我们无法确定该引用所指向的对象实例是不是肯定没有实现接口IB:或许类型B继承类型A同时实现了接口IB,而我们所持有的类型A的引用指向的正是类型B的实例。
这种情况无法只用静态检查验证其正确性,但又十分有用,不能一刀切都拒绝掉。所以干脆推迟到运行时通过实际的实例上的类型信息来做检查。
静态校验与动态检查相结合,JVM与CLR等支持静态的面向对象类型系统的虚拟机得以保证其上运行的程序的类型安全。

可以看这样的一个例子,其中IFoo与FooImpl取自前一帖的例子:
require 'rubygems'
require 'bitescript'
include BiteScript

IFoo    = Java::IFoo
FooImpl = Java::FooImpl

fb = FileBuilder.build(__FILE__) do
  public_class 'TestVirtualCall2' do
    public_static_method 'main', void, string[] do
      # Object o = new FooImpl();
      new FooImpl
      dup
      invokespecial FooImpl, '<init>', [void]
      checkcast JObject  # << make sure the JVM think local 1 is of type Object
      astore 1

      # o.method();
      aload 1
      invokeinterface IFoo, 'method', [void] # this works!
      returnvoid
    end
  end
end

fb.generate do |filename, class_builder|
  File.open(filename, 'w') do |file|
    file.write(class_builder.generate)
  end
end

这段脚本生成的TestVirtualCall2.class执行正常,打印出"FooImpl.method()"。
与前面调用虚方法的例子对比,可以看到JVM对接口方法调用的静态检查确实松一些。结合前一帖的例子看,这仍然是类型安全的,虽然要推迟到运行时才能检查。

==================================================================

值得注意的是,以上讨论都是以静态类型系统为前提的。如果类的成员能动态的被任意添加删除修改,那接口或基类所定下的契约就不能保证被实现,也就失去了上述讨论的前提。
分享到:
评论
2 楼 RednaxelaFX 2009-06-05  
cescshen 写道
引用
引用基类上所有实例方法中非私有的方法都自动会被派生类所继承

以前好像看到说,基类的私有方法也会被派生类继承,只是不能调用.

这要看你怎么看待“继承”这个概念了。确实,基类的所有实例成员都会被派生类所继承,包括成员域和成员方法;其中只有成员方法与多态有关,而私有方法不参与多态。如果不能被调用、不参与多态,那么它对外界来说就是不可观察到的,至少不会成为“契约”的一部分。(Java序列化的那俩方法……那是例外)

可以举个例子。在CLR当中,每个类型都与一个方法表的实例联系在一起。方法表当中,继承下来的虚方法的项总是位于方法表中固定偏移量的位置上,紧接着是该类型新增加的虚方法,然后才是私有方法和构造器等。派生类的方法表里不包含基类的私有方法或静态方法的项——既然不能调用,何必记录下来呢?这样的设计在调用虚方法总能使用相对方法表起始位置的固定偏移量,间接调用,而在调用静态方法或私有方法时则使用固定地址,直接调用。
1 楼 cescshen 2009-06-05  
引用
基类上所有实例方法中非私有的方法都自动会被派生类所继承

以前好像看到说,基类的私有方法也会被派生类继承,只是不能调用.

相关推荐

Global site tag (gtag.js) - Google Analytics