标签:布局 编译 堆内存 完成后 region pause 扫描 set serial
进行垃圾回收时,必须暂停所有的工作线程,直到它回收结束,这个暂停称之为 STW,JVM 开发团队一直努力消除或降低 STW 的时间。
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
但是要注意:内存移动是必须实打实的移动(复制),所以对应的引用(直接指针)需要调整。
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。
一种更加优化的复制回收分代策略:具体做法是分配一块较大的Eden 区和两块较小的Survivor 空间(你可以叫做From 或者To,也可以叫做 Survivor1 和 Survivor2)
专门研究表明,新生代中的对象 98% 是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有10%的内存会被“浪费”。当然,98% 的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10% 的对象存活,当 Survivor 空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。
它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。标记整理算法虽然没有内存碎片,但是效率偏低。
我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用对象的地方都需要更新(直接指针需要调整)。
三色标记最大的好处是可以异步执行,可以以极少的中断时间或者没有中断来进行整个 GC。
对比:G1 在处理并发标记的过程比 CMS 效率要高
SATB 算法是关注引用的删除。Incremental Update 算法关注引用的增加。
G1 如果使用 Incremental Update 算法,因为变成灰色的成员还要重新扫,重新再来一遍,效率太低了。
所以G1 在处理并发标记的过程比CMS 效率要高,这个主要是解决漏标的算法决定的。
收集器 | 收集对象和算法 | 收集器类型 | 说明 | 场景 |
---|---|---|---|---|
Serial | 新生代 复制算法 |
单线程 | 简单高效;适合内存不大的情况 | |
ParNew | 新生代 复制算法 |
并行的多线程收集器 | 是 Serial 的多线程版 | 与 CMS 搭配 |
Parallel Scavenge | 新生代 复制算法 |
并行的多线程收集器 | 类似 ParNew,面向吞吐量 | 是 Server 级别多 CPU 机器上的默认 GC 方式,主要适合后台运算且不需要太多交互的任务 |
Serial Old | 老年代 标记 - 整理算法 |
单线程 | Client 模式下虚拟机使用 | |
Parrallel Old | 老年代 标记 - 整理算法 |
并行的多线程收集器 | 面向吞吐量 | 在注重吞吐量以及 CPU 资源敏感的场合使用 |
CMS | 老年代 标记 - 清除算法 |
并行与并发收集器 | 尽可能地缩短 STW,缺点: 1. 至少 4 核 CPU 2. 内存碎片,使用 Serial Old 整理碎片 3. 浮动垃圾,需要更大的堆空间,6G 以上 |
重视服务的响应速度、系统停顿时间和用户体验的互联网网站或者B/S系统。互联网后端目前 CMS 是主流的垃圾回收器。 |
G1 | 全部 化整为零 Region 标记 - 整理 & 复制 |
并行与并发收集器 | 采用分区回收的思维,基本不牺牲吞吐量的前提下完成低停顿的内存回收;可预测的停顿是其最大的优势 | 面向服务端应用的垃圾回收器,目标为取代 CMS |
开启:-XX:+UseSerialGC
,串行垃圾收集器,几十兆 ~ 一两百兆
JVM 刚诞生就只有这种,最古老的,单线程,独占式,成熟,适合单 CPU,一般用在客户端模式下使用。这种垃圾回收器只适合几十兆到一两百兆的堆空间进行垃圾回收,可以控制停顿时间再100ms 左右,但是对于超过这个大小的内存回收速度很慢。
开启:-XX:+UseParallelGC
,并行垃圾收集器(吞吐量优先垃圾回收器),上百兆~几G
为了提高回收效率,从 JDK1.3 开始,JVM 使用了多线程的垃圾回收机制,关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。适合回收堆空间上百兆~几个G。
-XX:MaxGCPauseMillis
最大停顿时间
-XX:GCTimeRatio
垃圾收集时间占总时间的比率
-XX:+UseAdaptiveSizePolicy
动态调整(默认开启)
-Xmn
)、Eden 与 Survivor 区的比例(-XX:SurvivorRatio
)、晋升老年代对象大小(-XX:PretenureSizeThreshold
)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。开启:-XX:+UseConcMarkSweepGC
,第一个并发垃圾收集器,几G ~ 20G
ParNew 是多线程垃圾回收器。和 Serial 唯一的区别是多线程,停顿时间比 Serial 少。
CMS 是一种以获取最短回收停顿时间为目标的收集器,重视服务的响应速度。由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以总体上看CMS 的内存回收过程是与用户线程一起并发执行的。
-XX:+UseCMSCompactAtFullCollection
无法分配大对象时使用 Serial Old 进行碎片整理(默认开启)
在实现并发的垃圾回收时,如果采用标记整理算法,那么还涉及到对象的移动,对象的移动必定涉及到引用的变化,这个需要暂停业务线程来处理栈信息,这样使得并发收集的暂停时间更长,所以使用简单的标记-清除算法才可以降低 CMS 的 STW 的时间。
开启:-XX:+UseG1GC
,并发垃圾收集器,6G ~ 上百G
大于 6G 直接用 G1,不用 CMS,省事,没那么多烦恼。
为了实现 STW 的时间可预测,G1 将堆内存“化整为零”,将堆内存划分成多个大小相等独立区域(Region),每一个 Region 都可以根据需要,扮演新生代的 Eden 空间、Survivor 空间,或者老年代空间。回收器能够对扮演不同角色的 Region 采用不同的策略去处理。
-XX:G1HeapRegionSize
: Region 大小,1MB ~ 32MB ,2 的 N 次幂
-XX:MaxGCPauseMillis
: 最大 GC 暂停时间RSet/CardTable
处理跨代引用或 G1 中的跨 Region 引用的问题。
RSet 本身就是一个 Hash 表。RSet 的价值在于使得垃圾收集器不需要扫描整个堆,找到谁引用了当前分区中的对象,只需要扫描 RSet 即可。
如果一个老年代 CardTable 中有对象指向新生代, 就将它设为 Dirty(标志位1), 下次扫描时,只需要扫描 CardTable 上是 Dirty 的内存区域即可。
字节数组 CARDTABLE 的每一个元素都对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称作“卡页”(Card Page)。一般来说,卡页大小都是以 2 的 N 次幂的字节数,假设使用的卡页是 2 的 10 次幂,即 1K,内存区域的起始地址是 0x0000 的话,数组 CARD_TABLE 的第 0、1、2 号元素,分别对应了地址范围为 0x0000~0x03FF、0x0400 ~ 0x07FF 0x0800~0x011FF 的卡页内存。
用户线程暂停,GC 线程要开始工作,但是要确保用户线程暂停的这行字节码指令是不会导致引用关系的变化。所以 JVM 会在字节码指令中,选一些指令,作为“安全点”,比如方法调用、循环跳转、异常跳转等,一般是这些指令才会产生安全点。
为什么它叫安全点,是这样的,GC 时要暂停业务线程,并不是抢占式中断(立马把业务线程中断)而是主动是中断。
主动式中断是设置中断标志,各业务线程在运行过程中会不停的主动去轮询这个标志,一旦发现中断标志为True,就会在自己最近的“安全点”上主动中断挂起。
要是业务线程都不执行(业务线程处于Sleep 或者是Blocked 状态),那么程序就没办法进入安全点,对于这种情况,就必须引入安全区域。
安全区域是指能够确保在某一段代码片段之中, 引用关系不会发生变化,因此,在这个区域中任意地方开始垃圾收集都是安全的。我们也可以把安全区城看作被扩展拉伸了的安全点。
当用户线程执行到安全区域里面的代码时,首先会标识自己已经进入了安全区域,这段时间里 JVM 要发起 GC 就不必去管这个线程了。 当线程要离开安全区域时,它要看 JVM 是否已经完成了(根节点枚举,或者其他 GC 中需要暂停用户线程的阶段)
标签:布局 编译 堆内存 完成后 region pause 扫描 set serial
原文地址:https://www.cnblogs.com/xch-jiang/p/14618424.html