Java要求所有加载的类都要经过验证,以维护沙箱的安全性并确保代码可以安全地进行优化。请注意,这是在字节码级别完成的,因此验证不会验证Java语言的不变量,它只是根据字节码的规则验证字节码是否有意义。
除此之外,字节码验证可确保指令格式正确,所有跳转到方法中的有效指令,并且所有指令都基于正确类型的值。最后一个是堆栈映射的用武之地。
问题是字节码本身不包含显式类型信息。类型是通过数据流分析隐式确定的。例如,iconst 指令创建一个整数值。如果将其存储在插槽 1 中,则该插槽现在具有 int。如果控制流从存储浮点数的代码合并而来,则该槽现在被视为具有无效类型,这意味着在覆盖该值之前,您无法对该值执行更多操作。
从历史上看,字节码验证程序使用这些数据流规则推断所有类型。遗憾的是,不可能在通过字节码的单个线性传递中推断出所有类型,因为向后跳转可能会使已推断的类型无效。经典的验证器通过循环访问代码解决了这个问题,直到所有内容都停止更改,这可能需要多次传递。
但是,验证会使 Java 中的类加载速度变慢。Oracle决定通过添加一个新的、更快的验证器来解决这个问题,该验证器可以在一次传递中验证字节码。为此,他们需要从 Java 7 开始的所有新类(Java 6 处于过渡状态)携带有关其类型的元数据,以便可以在一次传递中验证字节码。由于字节码格式本身无法更改,因此此类型信息单独存储在名为 的属性中。StackMapTable
简单地在代码中的每个点存储每个值的类型显然会占用大量空间并且非常浪费。为了使元数据更小、更高效,他们决定只列出作为跳转目标的位置的类型。如果您考虑一下,这是您唯一需要额外信息来执行单次通过验证的时间。在跳转目标之间,所有控制流都是线性的,因此您可以使用旧的推理规则推断位置之间的类型。
显式列出类型的每个位置称为堆栈地图框。该属性包含按顺序排列的帧列表,尽管它们通常表示为与前一帧的差异,以便减小数据大小。如果方法中没有帧,当控制流从未加入时(即CFG是一棵树),则可以完全省略StackMapTable属性。StackMapTable
因此,这就是StackMapTable如何工作以及添加它的原因的基本思想。最后一个问题是如何创建隐式初始帧。答案当然是,在方法的开头,操作数堆栈为空,局部变量槽具有由方法参数类型给出的类型,这些类型由方法分叉器确定。
如果您习惯于 Java,那么方法参数类型在字节码级别的工作方式有一些细微的差异。首先,虚拟方法具有隐式作为第一个参数。其次, 、 、 和 不存在于字节码级别。相反,它们都是在幕后作为int实现的。this
boolean
byte
char
short