这两个指令都使用静态而非动态分派。似乎唯一重要的区别是invokespecial
将始终具有作为其调度方法所属类的实例的对象作为其第一个参数。但是,invokespecial
实际上没有把对象放在那里;编译器负责通过在发出invokespecial
之前发出适当的堆栈操作序列来实现这一点。因此,将invokespecial
替换为invokestatic
不应该影响运行时堆栈/堆的操作方式 - 尽管我希望它会导致VerifyError
违反规范。
我很好奇制作两个截然不同的指令背后可能的原因。我看了一下OpenJDK解释器的来源,似乎invokespecial
和invokestatic
几乎完全相同。有两个单独的指令有助于JIT编译器更好地优化代码,还是帮助类文件验证器更有效地证明某些安全属性?或者这只是JVM设计中的一个怪癖?
答案 0 :(得分:2)
有定义:
存在显着差异。假设我们想设计一个invokesmart
指令,可以在inkovestatic
和invokespecial
之间巧妙地选择:
首先,区分静态和虚拟调用不会有问题,因为我们不能有两个具有相同名称,相同参数类型和相同返回类型的方法,即使一个是静态的,第二个是虚拟的。 JVM不允许这样做(出于一个奇怪的原因)。感谢raphw注意到这一点。
首先,调用智能 foo/Bar.baz(I)I
的意思是什么?这可能意味着:
foo.Bar.baz
并添加另一个int
的静态方法调用int
。 // (int) -> (int)
foo.Bar.baz
和foo.Bar
并添加int
的实例方法调用int
。 // (foo.Bar, int) -> (int)
你会如何选择?可能存在两种方法。
我们可以尝试通过要求 foo/Bar.baz(Lfoo/Bar;I)
进行静态调用来解决它。但是,我们可能同时拥有public static int baz(Bar, int)
和public int baz(int)
。
我们可能会说无关紧要并可能禁用这种情况。 (我认为这不是一个好主意,只是想象一下。)这意味着什么?
ACC_SUPER
。因此,这意味着非常不一致的行为。
答案 1 :(得分:2)
免责声明:很难说清楚,因为我从未读过关于此的明确的Oracle声明,但我认为这就是原因:
当您查看Java字节代码时,您可能会询问有关其他指令的相同问题。为什么验证者在堆栈中推送两个int
并在之后将它们视为单个long
时会阻止您? (尝试一下,它会阻止你。)你可以说,通过允许这个,你可以用一个较小的指令集表达相同的逻辑。 (为了进一步讨论这个参数,一个字节不能表达太多的指令,因此Java字节代码集应该尽可能地减少。)
当然,从理论上讲,你不需要一个字节代码指令来将int
和long
推送到堆栈中,你是对的,你不需要两个{ {1}}和INVOKESPECIAL
以表示方法调用。方法由其方法描述符(名称和原始参数类型)唯一标识,并且您无法在同一个类中定义具有相同描述的静态和非静态方法。为了验证字节代码,Java编译器必须检查目标方法是否为INVOKESTATIC
。
备注:这与v6ak的答案相矛盾。但是,非静态方法的方法描述符不会更改为包含对static
的引用。因此,Java运行时总是可以从假设的this.getClass()
指令的方法描述符推断出适当的方法绑定。见JVMS§4.3.3。
这个理论太多了。但是,两种调用类型表达的意图是完全不同的。请记住,Java字节代码应该被 javac 之外的其他工具用于创建JVM应用程序。使用字节代码,这些工具可以生成与Java源代码更相似的机器代码。但它仍然是相当高的水平。例如,仍然验证字节代码,并在编译为机器代码时自动优化字节代码。但是,字节代码是一种有意包含一些冗余的抽象,以使字节代码的含义更明确。就像Java语言对类似事物使用不同的名称来使语言更具可读性一样,字节码指令集也包含一些冗余。另外一个好处是,验证和字节码解释/编译可以加速,因为方法的调用类型并不总是需要推断,而是在字节代码中明确说明。这是可取的,因为验证,解释和编译是在运行时完成的。
作为最后的轶事,我应该提到类的静态初始值设定项INVOKESMART
在Java 5之前没有被标记为<clinit>
。在此上下文中,静态调用也可以通过方法的名称来推断,但是这个会导致更多的运行时间开销。
答案 2 :(得分:0)
为了清楚地了解这些代码,您需要在eclipse IDE中添加eclipse plugin for ASM,并找出为您创建的示例Hello World程序生成的字节码。