3.1.1 堆设计
从应用程序运行的角度来说,应用所需的堆空间大小与应用程序中对象的分配速率和运行时间相关。由于应用对象分配速率和运行时间不同,且对于堆空间大小的需求不尽相同,因此应用启动时应该告诉JVM需要多少堆空间,常见的做法是在应用启动时通过参数设置堆空间大小。除了需要确定堆空间的大小以外,使用者还需要根据垃圾回收器堆的设计了解如何使用堆空间,才能充分利用堆空间。
JVM管理的堆空间是基于OS管理的内存之上的,应用在启动时向OS请求整个运行期所需要的全部内存。当然这样的设计并非完美,至少存在两个问题:其一,从OS直接请求内存是相对耗时的操作,请求运行时全部内存将导致JVM启动时间过长;其二,JVM启动时从OS请求了内存但并不会立即使用,实际上造成了资源浪费。JVM如此设计的原因在于:应用都是较长时间运行,期望通过启动初始化运行时所需的内存加快运行的效率。那么有没有比较好的方案既能保证应用的执行效率,又能兼顾应用启动速度和内存利用率呢?
JVM通过细化堆空间设计解决这个问题。JVM提供了两个参数:一个是最小的堆空间,另一个是最大的堆空间。假定这两个参数分别记为InitialHeapSize和MaxHeapSize[1]。设计思路修改为:JVM启动时向OS请求最小的堆空间,并在运行时根据内存使用的情况逐步扩展,直到堆空间达到参数设置的最大堆空间。这样的设计在一定程度上解决了JVM启动慢、资源利用率低的问题,其本质是把应用启动时的内存资源初始化请求推迟到应用运行时,这可能导致应用运行性能受到内存资源扩展的影响。所以在一些应用中为了减少运行时内存扩展带来的影响,会在启动时把最小堆空间和最大堆空间设置成相同的值。
1.5节讨论垃圾回收工作范围时,提到垃圾回收不仅包含向OS请求内存,还包含向OS归还申请的内存。早期JVM设计主要考虑的是如何合理地向OS请求内存,很少考虑如何向OS归还内存。但这样的设计在一些场景中存在问题。例如,一个应用在运行过程中内存使用越来越多,在业务处理高峰时内存使用达到了最大堆空间,但当业务峰值下降之后,由于没有合理的内存归还机制,申请的内存一直被占用但没有再次使用,这实际上造成了资源浪费。这样的问题在云场景中表现得非常明显,在云场景中,用户按资源使用付费,不愿意也不应该为未使用的内存付费,所以最新的JVM都会考虑在什么情况下向OS归还内存。需要指出的是,向OS归还内存也是一个耗时的操作,不当的设计和实现会导致程序暂停时间过长。另外,归还时机和归还的内存数量不当,也可能导致内存归还后应用内存不足,会立即向OS再次请求内存,从而发生内存使用颠簸,这也会引起应用性能下降。针对这一问题,一个可能的设计是引入一个新的参数,假定参数记为SoftMaxHeapSize[2],用于控制内存归还的边界。该参数满足条件:InitialHeapSize≤SoftMaxHeapSize[3]≤MaxHeapSize,这3个参数的作用如下:
- InitialHeapSize作为应用启动时最小的堆空间。
- 根据运行的需要,应用程序使用的内存可以扩展,但最大使用量不超过MaxHeapSize。
- SoftMaxHeapSize作为控制参数,当内存使用超过该阈值到MaxHeapSize之间的部分,在满足一定条件的情况下,可以归还给OS。对于不支持SoftMaxHeapSize的垃圾回收器,可以简单地认为SoftMaxHeapSize等于MaxHeapSize。
根据这3个参数的含义,堆空间的划分如图3-2所示。
图3-2 堆空间划分示意图
那么在实际工作中该如何设置这3个参数值?通常的原则如下:
1)MaxHeapSize是对应用程序最大内存量的估计。
2)SoftMaxHeapSize是对应用程序常见工作负载使用的内存量的估计。
3)InitialHeapSize一方面是对应用程序启动后所需最小内存使用量的估计(最小内存一般指应用满足最小工作负载时的内存使用量),另一方面是在启动速度和资源利用之间寻找一个平衡值(即在最小内存使用量和最大内存使用量之间寻找一个合适的值)。
在JVM的实现中,应用也可以不提供这3个参数值。如果应用启动时没有提供参数值,那么JVM会为参数提供一个默认值,然后根据系统的硬件配置启发式地为参数推导一个“合适”的值。例如,JVM运行在32位系统之上,MaxHeapSize的默认值是96MB;JVM运行在64位系统之上,MaxHeapSize的默认值是124.8MB。然后JVM进一步启发式地推导:在小内存系统中使用50%的物理内存作为MaxHeapSize的上限(小内存指的是默认值大于50%的物理内存),否则使用25%[4]的物理内存作为MaxHeapSize的上限,然后再通过其他参数加以调整(具体公式会在第9章详细介绍)。
JVM的设计者推荐Out-Of-Box(开箱即用)的使用方式,即JVM使用者无须进行任何参数配置即可较好地使用JVM。但是在实际工作中,对于堆空间这样重要的参数,使用者还是需要明确地设置,如明确设置MaxHeapSize[5]等相关参数[6],既能确保资源没有浪费,又能保证资源充分利用。