3.1.2 分代边界
在固定边界的分代内存管理中,边界该如何确定?因为整个堆空间划分为新生代和老生代两个代,所以只要确定其中一个代的大小,另外一个代的大小也就确定下来,边界也就确定了。JVM通过确定新生代的大小来确定边界,假定新生代的大小记为MaxNewSize。从整体的堆空间中确定新生代空间大小常用的方法有以下两种:
1)绝对值划分:设置一个新生代的大小。
2)比例划分:设置一个比例,假定记为NewRatio,假定堆大小记为HeapSize,在JVM中新生代大小可以通过公式计算得到。该参数的含义是:新生代和老生代的比例为1∶NewRatio[7]。
JVM同时支持两种设置方式,这意味着使用者既可以通过设置新生代大小(绝对值方式)确定边界,也可以通过设置新生代占用整个堆空间的比例来确定边界。由于JVM同时支持两种方式,而两种方式修改的是同一个参数,如果两种方式同时使用,则会造成参数设置冲突。而在实际工作中,笔者也遇到过一些用户对于参数不了解或者错误使用的情况,同时设置这两种参数,从而造成了参数冲突。在JVM实现中,为了防止误用,需要解决这样的冲突。通常解决这类冲突的方法是对这两种参数的设置方式使用不同的优先级,当设置高优先级参数时,低优先级参数失效。在JVM中,绝对值参数设置方式优先级更高,即假设使用者同时设置了参数MaxNewSize和NewRatio,只有MaxNewSize有效,NewRatio无效[8]。
笔者在实际工作中遇到过许多JVM使用者不知道或者忘记设置新生代大小的情况,新生代大小的设置实际上对应用的性能有较大的影响(新生代用于应用程序对象的分配,所以新生代的大小会直接影响应用的效率。参考2.3节垃圾回收的基础知识)。JVM中关于新生代大小参数设置的效果如表3-1所示。
表3-1 新生代大小参数设置效果
在讨论分代边界的时候,我们假定堆空间大小固定为HeapSize,并根据上面的方法计算新生代和老生代的大小,进而确定边界。但是在上一节的讨论中,使用的堆空间并不固定,存在最大堆空间和最小堆空间。那么边界是与最大堆空间相关,一直保持不变,还是与实际使用的堆空间相关,随着使用堆空间的大小变化而变化呢?其实这个问题并没有一个绝对的设计原则。串行回收使用固定的边界,其好处如下:
1)新生代扩展处理简单。假设边界随着堆空间的实际使用量的变化而变化,在新生代需要扩展的时候该如何处理?根据图3-1所示的内存对象布局,为了保持新生代和老生代管理内存的连续性,只能把老生代管理的内存向后移动,移动出的空闲部分归新生代扩展使用。移动内存是非常耗时的操作,而使用固定边界可以避免内存移动,从而获得更高性能。
2)代际信息管理简单。通常为了高效地进行垃圾回收,可以使用引用集管理代际之间的引用,例如使用卡表。当边界固定时,卡表相关的写屏障处理简单,通过比较对象地址和边界的关系,非常容易判断对象是位于新生代中还是老生代中,从而减少写屏障的额外消耗。
固定新生代大小最大的缺点是内存管理的灵活性差,应用在启动时就需要确定新生代大小,这通常并不容易。当然垃圾回收算法可以增强,将固定边界优化为浮动边界,第5章介绍的并行回收、第6章介绍的G1都涉及这方面的设计和实现。
结合堆空间大小动态变化和边界固定的特点,将图3-1和图3-2组合后,应用堆空间的内存布局如图3-3所示。
图3-3 增加分代后的堆空间设计