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