平媒用稿,请勿转载

堆栈数据冗余

由于GC的需要,虚拟机需要知道每个线程堆栈里面每个元素放的是java对象还是一个普通的数字,因此在之前的设计中设计了两个堆栈,一个stack存放具体的数字,一个typeStack存放类型,每次push的时候,我们都会同时写typeStack,带来两个问题

  1. 多了一次写入
  2. 为了降低typeStack浪费的空间,之前使用了bitset来压缩typeStack,typeStack中每一位代表一个位置的stack是否为对象。因此写入的性能变差了
  3. 另外,在目前的内存结构中,typeStack是整体放在stack后面的,stack的长度是64k,因此连续写入的时候这两个位置还不在一页上

因此,首先尝试把这个给typeStack优化掉。

条条大路通罗马,虽然到达某个指令所经过的分支等等都有所不同,但是java的编译器保证了java到达每条指令时的栈状态是确定的,也就是说,无论之前走了怎样的分支,相同的位置的指令执行之前的栈状态(栈的大小,每个单元的类型)是确定的。这样我们就可以在运行之前先计算出每条指令的栈状态,存起来,GC的时候再根据当前指令获取即可。

那么如何计算每个指令之前的栈状态呢?第一条指令之前当然是根据参数来,后面的指令只要用动态规划算法把所有指令都执行一遍即可。不过如果你写了try{}finally{}这种代码,finally中的代码在java6之前是作为方法内的子程序存放的在不同的场合(如正常执行完,抛异常,return)通过jsr调用finally那部分,而通过不同的途径进入finally中的代码,栈的内容是不一样的,如果finally中再嵌套finally,进入的状态就有四种,因此在这种情况,java6之前的jvm规范推荐了一种复杂的办法把能到达这条指令的所有jsr作为key,几种情况分开保存。

而在java6之后,虚拟机规范通过两种方式简化了这个流程:

  1. 废弃了jsr/ret指令对,编译器对于finally中的代码,有几种调用到的情况就克隆出几份来
  2. 在method中增加了一种叫stackMap的属性,里面通过增量的方式保存了某些指令执行前的栈状态。这些指令涵盖了所有的跳转点。因此,如果需要知道某条指令之前的栈状态,只需要找到最近的stackMap帧,向后一条一条地按照指令对栈的作用计算栈的状态即可。

在应用stackMap,废弃掉typeStack之后,Linpack测试立马从25flops涨到47flops,涨幅接近60%。那么还能不能更快?

且听下回分解~

目录

高可移植解释型虚拟机优化专场·开场篇
高可移植解释型虚拟机优化专场·第一章 stackMap的解析和优化
高可移植解释型虚拟机优化专场·第一章·补