1.堆与GC
1.1垃圾收集
负责垃圾收集的程序模块,称为垃圾收集器。实现一款垃圾收集器,首先需要明确他的主要任务:
- 确保仍被引用的对象在内存中保持存在。
- 回收无任何引用的对象所占用的内存空间。
在设计垃圾收集器时,会有一些策略值得商榷:
- GC工作线程:串行还是并行
- GC工作线程与应用线程的关系:并发还是暂停应用程序?
- 基本收集算法:压缩、非压缩还是拷贝?
垃圾收集器的性能指标主要包括以下几项。
- 吞吐量:应用程序运行时间/(应用程序运行时间+垃圾收集时间)。即没有花在垃圾收集的时间占总时间的比例。
- 垃圾收集开销:与吞吐量相对,这表示垃圾收集耗用时间占总时间的比例。
- 暂停时间:在垃圾收集操作发生时,应用程序被停止执行的时间。
- 收集频率:相对于应用程序的执行,垃圾收集操作发生的频率。
- 堆空间:堆空间所占内存的大小
- 及时性:一个对象由称为垃圾到被回收所经历的时间。
1.1.1收集算法
在现代虚拟机中,GC常用的基本收集算法主要有如下3中类型。
(1)标记-清除(Mark-Sweep),该算法可分为两个阶段
- 标记阶段:标记出所有可回收的对象。
- 清除阶段:回收所有已标记的对象,释放这部分空间。
(2)复制算法(Copying)
- 划分区域:将内存区域按比例划分为1个Eden区作为分配对象的“主战场”和2个幸存区(即suvivor空间,划分为2个等比例的from区和to区)。
- 复制:收集时,打扫战场,将Eden区中仍存活的对象复制到某一幸存区中。
- 清除:由于上一阶段已确保仍存活的对象已被妥善安置,现在可以清理“战场”了,释放Eden区和另一块幸存区。
- 晋升:若在复制阶段,一块幸存区接纳不了所有的幸存对象,则直接晋升到老年代。
(3)标记-压缩算法(Mark-Compack),该算法分为两个阶段。
- 标记阶段:标记出所有可回收的对象。
- 压缩阶段:将标记阶段的对象移到空间的一端,释放剩余的空间。
1.2分代收集
分代收集是指在内存空间中划分不同的区域,在各自区域中分别存储不同年龄的对象。每个区域可根据自身的特点灵活采取适合自身的收集策略。
垃圾收集类型分为两种。
- Minor Collection:对新生代进行收集。
- Full Collection:除了对新生代进行收集外,也对老年代或永久代进行收集,又称为Major Collection。Full Collection对所有分代都进行了收集:首先,按照新生代配置的收集算法对新生代进行收集;接着,使用老年代收集算法对老年代和永久代进行收集。
1.2.1分代模型
1.3快速分配
通常情况下,系统有大量连续的内存块可以用来分配对象,这种情况下若使用碰撞指针(bump-the-pointer)算法来进行对象内存空间分配的话,效率是可观的。
bump-the-pointer是一种线性分配技术。以HotSpot为例,垃圾收集器在完成GC后,内存空间已使用和未使用的内存块是相对独立并且地址连续的。收集器在内部维护了一个记录上一次分配对象的末尾指针,当需要分配新的对象时,检查剩余空间能否满足新对象的分配,如果满足的话,则只需要移动指针就可以完成空间分配,所以效率很高。
对于多线程应用,分配操作需要保证线程安全,如果通过全局锁的方式来保证线程安排的话,内存分配将会成为性能瓶颈。所以HotSpot采用的是线程局部分配缓存技术(即Thread-Local Allocation Buffers,简称TLABs)。每个线程都会有自己的TLAB,位于Eden区中的一小块空间。因为每个TLAB是仅对一个线程可见的,所以分配操作可以使用bump-the-pointer技术快速完成,而不必使用任何锁机制;只有当线程将TLAB填满并且需要获取一个新的TLAB时,同步才是必须的。
1.4栈上分配和逸出分析
在栈中分配的基本思路是这样的:分析局部变量的作用域仅限于方法内部,则JVM直接在栈帧内分配对象空间,避免在堆中分配。
这个分析的过程称为逸出分析,而栈帧内分配对象的方式称为栈上分配。这样做的目的是:减少新生代的收集次数,间接提供JVM性能。虚拟机是允许对逸出分析开关进行配置的,从Sun Java 6u23以后,HotSpot默认开启逸出分析。
2垃圾收集器
1、串行收集器:Serial
串行收集器采用单线程方式进行收集,且在GC线程工作时,系统不允许应用程序打扰。此时,应用程序进入暂停状态。具有代表性的串行收集器有以下两种:
- Serial收集器,作用于新生代,基于“复制”算法;
- Serial Old收集器,作用于老年代,基于“标记——整理”算法;
2、并行收集器:ParNew
并行收集器充分利用了多处理器的优势,采用了多个GC线程并行收集。在HotSpot中具有代表性的并行收集器有以下两种:
- ParNew收集器:作用于新生代,基于“复制”算法;
- Parallel Old收集器,作用于老年代,基于“标记——整理”算法;
3、吞吐量优先收集器:Parallel
在ParNew基础上演化而来的Parallel Scanvenge收集器被誉为“吞吐量优先”收集器,仅作用于新生代。
4、堆的类型
HotSpot提供了一些VM选项,可以用来选择堆的类型。事实上,这是由配置的收集器决定的。系统将按照如下顺序选择堆的类型。
若开启VM选择UseParallelGC,系统将自动选择堆类型为ParallelScavengeHeap。
若开启VM选项为UseG1GC,系统将自动选择堆类型为G1CollectedHeap,而收集策略只能选择G2专用的G1CollectorPolicy_BestRegionsFirst。
若上述2个选择都没有配置,则将选择堆类型为GenCollectedHeap。而对于收集策略,还可以细分,例如:
- 默认配置ConcurrentMarkSweepPolicy,老年代的收集使用“标记——清除”算法;
- 若配置UseSerialGC,将选择MaskSweepPolicy;
- 若配置UseConcMarkSweepGC,将选择ConcurrentMarkSweepPolicy;若开启了自适应策略选项UseAdaptiveSizePolicy,则选择ASConcurrentMarkSweepPolicy作为收集策略。
5、收集策略
收集策略指的是在各个分代中应用的垃圾收集算法组合。我们先看一下在各个分代中可以配置的收集算法类型。
(1)在新生代和老年代中。
- ASParNew:在ParNew的基础上应用了自适应调节策略(UseAdaptiveSizePolicy)。
- ASConcurrentMarkSweep:在ConcurrentMarkSweep的基础上应用了自适应调节策略。
- DefNew:默认新生代收集器。
- ParNew:并行收集新生代。
- MarkSweepCompact:基于“标记-清除-压缩”算法。
- ConcurrentMarkSweep:基于CMS收集器。
(2)在永久代中。
- MarkSweepCompact:基于“标记-清除-压缩”算法。
- MarkSweep:基于“标记-清除”算法。
- ConcurrentMarkSweep:基于CMS收集器。
2.1CMS收集器
CMS收集器包含的主要模块:
CMSAdaptiveSizePolicy模块:实现CMS自适应调节策略。
CMSCollectorPolicy模块:实现CMS收集策略。
ConcurrentMarkSweepGeneration模块:实现CMS分代。包括CMS的核心组件,如CMSCollector、CMSMarkStack、SweepClosure、ConcurrentMarkSweepGeneration、MarkFromDirtyCardsClosure、SurvivorSpacePrecleanClosure、CMSBitMap等。
CMSPermGen模块:在CMS空间中实现PermGen,永久代也由CMS收集器收集。
CMSGeneration模块:实现CMS分代。
ConcurrentMarkSweepThread模块:实现CMS线程。
VMCMSOperation模块:定义了在CMS内部执行的操作,如VM_CMS_Initial_Mark、VM_CMS_Final_Mark等。
2.1.1.如何找到垃圾——可达性分析
CMS收集器工作时,GC工作线程与用户线程可以并发执行,以达到降低收集停顿时间的目的。对于交互响应速度敏感的应用程序,非常适合这种垃圾收集器。CMS仅作用于老年代的收集。
为了确保可达性分析得整个阶段中对象的引用关系保持固定,JVM需要暂停其他工作。JVM暂停的时候,需要选择一个时机,由于JVM系统运行期间的复杂性,不可能做到随时暂停,因此引入了安全点(safepoint)概念:程序只有在运行到安全点的时候,才准暂停下来。HotSpot采用主动中断的方式,让执行现场在运行时轮询是否需要暂停的标识,若需要暂停则中断挂起。
为支持分阶段实施并发“标记-清除”收集算法,HotSpot内部定义了多个CMS状态,其状态机如下图。
CMS完整的收集过程如下。
- 初始标记
- 并发标记
- 并发预清理
- 重新标记
- 并发清理
- 并发重置
2.2G1收集器
G1重新定义了堆空间,打破了原有的分代模型,将堆划分为一个个区域。这么做的目的是在进行收集时不必在全堆范围内进行。
1、何时使用G1
官方建议,如果发现符合如下特征,可以考虑更换成G1收集器以追求更佳性能:
- 实时数据占用了超过半数的堆空间。
- 对象分配率或“晋升”的速度变化明显。
- 期望消耗耗时较长的GC或停顿(超过0.5~1秒)
2、核心思路:Region
G1将堆分成许多相同大小的区域单元。
G1收集过程如下:
(1)初始标记:STW。G1将这个过程伴随在一次普通的新生代GC中完成。该阶段标记的是幸存区Regions(Root Regions)。当然,该区域仍有可能引用老年代的对象。
(2)根区域扫描:扫描幸存区中引用老年代的Regions。该阶段与应用程序并发进行。这一过程必须能够在新生代GC发生前完成。
(3)并发标记:找出全堆中存活的对象。该阶段与应用程序并发进行。这一过程允许被新生代GC打断。
(4)重新标记:STW,完成堆中存活对象的标记。冲击标记基于SATB(snapshot-at-the-beginning)算法,比CMS收集器算法快很多。
(5)清理:包括3个阶段:首先,计算活跃对象并完全释放自由Regions(STW);然后,处理Remembered Sets(STW);最后,重置空闲Regions并将它们放回空闲列表(并发)。
(6)复制:STW。将存活对象疏散或复制至新的未使用区域内。
《HotSpot实战》系列笔记共计4篇: