什么是堆栈映射帧?

67

我最近一直在研究Java虚拟机规范(JVMS),以更好地理解我的程序是如何工作的,但我发现有一个部分我还没有完全理解……

4.7.4节描述了StackMapTable属性,在该部分中,文档详细介绍了堆栈映射帧。问题在于它有点啰嗦,而我最喜欢通过例子来学习,而不是阅读。

我知道第一个堆栈映射帧是从方法描述符中派生出来的,但我不明白如何做到这一点(这个问题据说在这里得到了解答)。此外,我也不完全理解堆栈映射帧的作用。我认为它们类似于Java中的块,但似乎你不能在彼此之间嵌套堆栈映射帧。

无论如何,我有两个具体的问题:

  • 堆栈映射帧是用来做什么的?
  • 第一个堆栈映射帧是如何创建的?

还有一个通用问题:

  • 有人能否提供比JVMS中给出的更简洁易懂的解释?

1
@EJP 这是我正在努力的事情。这也是我决定首先阅读JVMS的主要原因之一。 - Steven
3
我也一直在阅读JVM规范,相信我,这不仅仅是阅读规范那么简单,例如:要理解类型验证工作原理的部分(与此问题有关),您需要具备基本的Prolog编程知识......因此,我认为这个问题和回答值得放在Stackoverflow上。 - morgano
@morgano,我发现忽略Prolog的内容并专注于经典推理验证器会更有帮助。新的验证器非常相似,只是他们决定用200页的Prolog来指定它,而不是像旧的那样使用模糊的英语描述。 - Antimony
@Antimony,确切地说,这个问题就是要将正式规范翻译成简明易懂的英语。 - morgano
1
我已经在这里尽力全面地解释了它们 http://www.volatileinterface.com/understanding-the-java-class-file-format-stack-map-tables/ - Markovian8261
显示剩余3条评论
1个回答

159

Java要求所有加载的类都经过验证,以保持沙箱的安全性并确保代码安全优化。请注意,这是在字节码级别上完成的,因此验证不会验证Java语言的不变量,它仅仅验证字节码是否符合字节码规则。

除其他外,字节码验证还确保指令格式正确,所有跳转都是到方法内有效指令,所有指令都操作正确类型的值。最后一个是栈映射表的作用所在。

问题是字节码本身不包含显式的类型信息。类型通过数据流分析隐式确定。例如,iconst指令创建整数值。如果将其存储在1号槽中,那么该槽现在具有int类型。如果控制流从存储float值的代码合并到该位置,则该槽现在被视为无效类型,意味着您不能再对该值进行任何操作,直到覆盖它。

历史上,字节码验证器使用这些数据流规则来推断所有类型。不幸的是,在单个线性遍历字节码时不可能推断出所有类型,因为向后跳转可能使已推断的类型无效。经典验证器通过迭代整个代码,直到一切都停止改变,从而解决了这个问题,可能需要多次遍历。

然而,在Java中验证使类加载变慢。Oracle决定通过添加一个新的、更快的验证器来解决这个问题,可以在一次遍历中验证字节码。为了做到这一点,他们要求从Java 7开始的所有新类(Java 6处于过渡状态)携带有关它们类型的元数据,以便可以在单个遍历中验证字节码。由于字节码格式本身无法改变,因此该类型信息单独存储在名为StackMapTable的属性中。

将每个代码点的每个值的类型都存储起来显然会占用大量空间并且非常浪费。为了使元数据更小更有效,他们决定只在跳转目标位置列出类型。如果你考虑一下,这是唯一需要额外信息进行单遍验证的时间。在跳转目标之间,所有控制流都是线性的,因此可以使用旧的推断规则推断出中间位置的类型。

每个显式列出类型的位置称为堆栈映射帧。StackMapTable属性按顺序包含一系列帧,尽管它们通常表示为与先前帧的差异,以减少数据大小。如果方法中没有帧,则可以完全省略StackMapTable属性,这种情况发生在控制流从未汇合时(即CFG是树形结构)。

因此,这是StackMapTable如何工作和为什么添加它的基本思想。最后一个问题是如何创建隐式初始帧。答案当然是,在方法开始时,操作数栈为空,并且局部变量槽具有由方法参数的类型给出的类型,这些类型是从方法描述符中确定的。

如果您习惯于Java,则方法参数类型在字节码级别上有一些细微的差异。首先,虚方法具有隐式的this作为第一个参数。其次,boolean,byte,char和short在字节码级别上不存在。相反,它们在幕后都实现为int。


12
作为对您最后一段的修改,longdouble 参数与所有本地变量一样,在堆栈帧中会占用 两个 本地变量的空间。 - Holger
16
一个很好的对冷门话题的解释。 - Mike Strobel
@ThomasRS 是的,ASM可以自动计算帧(虽然在许多情况下手动计算它的成本是自动计算的两倍 - 但从朋友的经验来看,手动计算帧是一件痛苦的事情)。 - arviman
我只是不知道为什么他们把它命名为“stack map frame”?这让我觉得它与jvm栈中的帧有关! - sify
好的,也许它意味着操作数堆栈变量及其类型的映射。操作数堆栈位于帧中,帧位于JVM堆栈中,堆栈映射中的每个条目称为堆栈映射帧! - sify
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接