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