垃圾收集器之 ParNew 与 CMS

大纲

在这里插入图片描述

一、垃圾收集算法

说到垃圾收集器必然离不开垃圾收集算法。
垃圾收集算法时基于分代收集理论的。
即一般我们的堆区域都有年轻代、老年代之分,这样区分的目的主要是将不同生命周期的对象区分开来,比如朝生夕死的对象都在年轻代,而长久存在的对象会存储在老年代,这样来进行不同的有针对性的垃圾收集处理更加高效,比如年轻代的朝生夕死的对象用复制算法来进行收集效率会很高,而老年的长久存活的对象对于内存要求很高就不适合复制算法,这个适合会在标记清除算法和标记整理算法中选择一个较为合适的来进行处理,其中复制算法要快于其他两种算法 10 倍以上。

1.标记复制算法

标记复制算法即将内存区域划分为两块,一块为空闲区域,一块为使用区域,在 gc 时会将存活对象赋值到空闲区域,其余对象清除。以此往复。这样做的 gc 效率很高但内存利用率会降低一般。
在这里插入图片描述

2.标记清除算法

标记清除算法即对存活对象进行标记,其余对象直接清除。简单粗暴所有内存都会利用到但会产生大量的内存碎片
在这里插入图片描述

3.标记整理算法

标记整理算法类似标记清除算法,但他不是直接清除而是将存活对象一次向前挪动,最后清除存活对象边界范围外的对象,来保证内存的连续。
在这里插入图片描述

二、垃圾收集器

1.Serial 串行垃圾收集器

Serial 垃圾收集器可以说是最古老的垃圾收集器,顾名思义他是串行进行垃圾收集的,在整个垃圾收集的时候回进行 STW 暂停掉整个用户线程的执行。其在年轻代垃圾收集采用的是标记复制算法,老年代的垃圾收集版本为 Serial Old 采用的是标记整理算法。
Serial 垃圾收集器在 gc 的整个过程单线程串行收集会进行长时间的 STW,所以对于用户体验很不好,但因为是单线程 STW 其收集过程不受任何影响收集过程简单高效,故有很高的单线程收集效率。
Serial Old 是 Serial 垃圾收集器的老年代版本,其主要作用是在 jdk 1.5 之前配合 parallel 一起使用,以及作为 CMS 垃圾收集器的后备方案。
开启参数:

-XX:+UseSerialGC  -XX:+UseSerialOldGC

在这里插入图片描述

2.Parallel 并行收集器

Parallel 收集器与 Serial 收集器唯一的区别是其垃圾回收的过程是多线程的,默认线程个数为当前 cpu 核数,这个个数可以自己设置 -XX:ParallelGCThreads 但一般不推荐修改。
可以理解为 Parallel 为 Serial 的多线程版
同时 Parallel 也提供了老年代版本的收集器来对老年代进行垃圾回收,同样其年轻代算法采用的是标记复制算法,老年代是标记整理算法。

-XX:+UseParallelGC(年轻代),-XX:+UseParallelOldGC(老年代)

在这里插入图片描述
Parallel 相比于 CMS 更加注重的是 cpu 的吞吐量,CSM 更加注重的是用户体验,即 STW 停顿的最大时间。 在 JDK 1.8 中默认使用的垃圾收集器为 Parallel 核 Parallel Old

3. ParNew

ParNew 与 Parallel 并没有本质上的区别,其主要是为了配合 CSM 的垃圾收集而提供的年轻代的垃圾收集器,其只有年轻代的收集版本,垃圾收集上与 Parallel 相同。
目前仅有 Serial 和 ParNew 可与 CSM 进行配合垃圾收集。
在这里插入图片描述

4. CMS

CMS 垃圾收集器,是一个老年代的垃圾收集器,其主要目标是为了提高用户体验,即尽可能多的缩短 STW 的时间。G1 和 ZGC 都是基于这个思想和流程来进行的进一步优化和调整。
其垃圾收集过程主要分为以下几个阶段:

  1. 初始标记
    初始标记是需要进行 STW 的,这个标记仅仅标记 GC Roots 的直接引用对象,并不会向下寻找所有引用,这个过程很快。这个过程 jdk 1.7 及之前为单线程,1.8 之后为并行,可通过参数控制。
  2. 并发标记
    并发标记过程是同用户线程一起进行的,这个过程不会进行 STW ,这个过程会遍历标记所有引用,因为这个过程用户线程也在同时运行,就有可能出现已标记对象的状态的变更,这个变更可能会导致漏回收或者误删除,当然 CMS 解决了这个问题 通过三色标记法
  3. 重新标记
    重新标记的过程就是在修正并发标记所带来的问题,这个过程是 STW 的,修正有两种方案,基于三色标记进行增量更新或原始快照。CMS 选择使用增量更新而 G1 选择使用原始快照,其优劣会下面的三色标记中讲到。这个过程是并行的
  4. 并发清理
    清理过程与用户线程同时进行,不会 STW 。这个时候如果有新的对象进来会直接标记为黑色不进行清理。
  5. 并发重置
    重置本次 gc 标记数据。

在这里插入图片描述
CMS 优点是低停顿,并发收集
其缺点也很明显:

  • 对 cpu 资源敏感,会与服务争抢 cpu 资源
  • 无法处理浮动垃圾,在并发标记和并发清理阶段产生的浮动垃圾只能留到下次 gc 清理
  • 因为使用的是标记清除算法,所以会产生大量的内存碎片
  • 垃圾回收过程的不确定性,即在并标记和并发清理阶段产生了大量的对象导致本次 gc 没有进行完却又出发了回收即 concurrent mode failure,此时会 STW 并转换为使用 Serial Old 进行串行收集
CMS 常用参数
-XX:+UseConcMarkSweepGC:启用cms
-XX:ConcGCThreads:并发的GC线程数
-XX:+UseCMSCompactAtFullCollection:FullGC 之后做压缩整理(减少碎片)
-XX:CMSFullGCsBeforeCompaction:间隔多少次 FullGC 之后压缩整理一次,默认是0,代表间隔 0 次 FullGC 后会进行压缩整理,即每次 FullGC 后都会进行整理
-XX:CMSInitiatingOccupancyFraction: 当老年代使用达到该比例时会触发 FullGC(默认是92,这是百分比)
-XX:+UseCMSInitiatingOccupancyOnly:只使用设定的回收阈值(-XX:CMSInitiatingOccupancyFraction设定的值),如果不指定,JVM仅在第一次使用设定值,后续则会自动调整
-XX:+CMSScavengeBeforeRemark:在CMS GC 标记前启动一次 minor gc,目的在于减少老年代对年轻代的引用,降低CMS GC的标记阶段时的开销,一般CMS的GC耗时 80%都在标记阶段
-XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
-XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW;

一个 CSM 参数设置范例:

-Xmx4096M -Xms4096M -Xmn1536M 
-XX:MaxMetaspaceSize=512M -XX:MetaspaceSize=512M 
-XX:+UseConcMarkSweepGC 
-XX:+UseCMSInitiatingOccupancyOnly 
-XX:CMSInitiatingOccupancyFraction=70 
-XX:+ExplicitGCInvokesConcurrentAndUnloadsClasses 
-XX:+CMSClassUnloadingEnabled 
-XX:+ParallelRefProcEnabled 
-XX:+CMSScavengeBeforeRemark 
-XX:ErrorFile=/home/admin/logs/xelephant/hs_err_pid%p.log 
-Xloggc:/home/admin/logs/xelephant/gc.log 
-XX:HeapDumpPath=/home/admin/logs/xelephant 
-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-XX:+HeapDumpOnOutOfMemoryError

5. 三色标记算法

所谓三色标记,即垃圾收集器在标记过程中将对象分别标记为三种颜色:
黑色:通过 GC ROOT 扫描到且所有直接引用都扫描到的对象标记为黑色
灰色:通过 GC ROOT 扫描到但未扫描完所有直接引用对象标记为灰色
白色:为被扫描的的对象为白色
在回收时会回收所有标记为白色对象

这里以 CMS 的收集算法为例来看一下三色标记的过程
初始标记-》并发标记-》重新标记
初始标记
初始标记的过程只扫描 GC ROOT 直接引用,如下图并将其标位灰色,等待并发标记时对其进行深度扫描。故此过程极快。
在这里插入图片描述
并发标记
在并发标记时情况会复杂很多,可能会出现多标或者漏标的情况,如下图在并发标记进行的某个时刻,B 对象已经扫描标记为灰色,C 对象已扫描没有其他引用标记为黑色,而 D 对象还没进行扫描,此时将 B 对象的引用 D 置为 null 而给 A 对象添加引用指向 D ,此时的 D 对象仍是白色,而因为 A 对象已经标为黑色在重新标记的时候不会再进行扫描。此时 D 对象漏标,清理时仍为白色,会导致非垃圾对象的误回收。同样的假如 B 对象在某一时刻已经被标记为黑色,此时将 C 对象的引用置 null ,而 B C 都为黑色不会进行重新扫描,就导致已经是垃圾的 C 对象最终不会被回收,产生了浮动垃圾也就是多标的情况。
在这里插入图片描述
对于多标漏标的情况垃圾回收器基于三色标记法和对象的读写屏障提供了两种解决方案,增量更新原始快照。那么 CMS 使用的是增量更新,而 G1 使用的是原始快照。
增量更新:增量更新即为在并发标记过程中在黑色对象成员变量在更新引用时会将该引用记录下来,在重新标记的时候重新对该对象进行扫描标记。也可以理解为如果对象写入了新值就将该对象的状态标记为灰色。
原始快照:原始快照顾名思义即保留原始的信息,在灰色对象成员变量引用更新的时候记录下老对象的引用,在重新标记过程中将这些老对象全都标记为黑色不进行回收,这样可能会产生浮动垃圾,但同时也减少了从新扫描的时间,与增量更新各有利弊。
读写屏障
读操作:读取成员变量 B b=a.b
写操作:对象的成员变量引用发生变化 a.b=null
这里所谓的读写屏障其实就是在对变量做读写操作的前后插入一些代码逻辑,去实现一些功能,c++ 赋值伪代码如下

/**
* @param field 某对象的成员变量,如 a.b.d 
* @param new_value 新值,如 null
*/
void oop_field_store(oop* field, oop new_value) { 
    *field = new_value; // 赋值操作
} 

此时我们可以在赋值操作的代码中插入写前屏障和写后屏障,类似于 Aop 的概念

void oop_field_store(oop* field, oop new_value) {  
    pre_write_barrier(field);          // 写屏障-写前操作
    *field = new_value; 
    post_write_barrier(field, value);  // 写屏障-写后操作
}

基于写前屏障和写后屏障就可以实现增量更新和原始快照 SATB。

写前屏障实现 SATB 原始快照 在对象的成员变量引用变更时将旧的引用记录下来

void pre_write_barrier(oop* field) {
    oop old_value = *field;    // 获取旧值
    remark_set.add(old_value); // 记录原来的引用对象
}

写后屏障实现 增量更新 在对象的成员变量引用变更时将新的引用记录下来

void post_write_barrier(oop* field, oop new_value) {  
    remark_set.add(new_value);  // 记录新引用的对象
}

CMS 实现增量更新源码


template <class T> inline void oop_store(volatile T* p, oop v) {
  update_barrier_set_pre((T*)p, v);   // cast away volatile
  // Used by release_obj_field_put, so use release_store_ptr.
  oopDesc::release_encode_store_heap_oop(p, v);
  // When using CMS we must mark the card corresponding to p as dirty
  // with release sematics to prevent that CMS sees the dirty card but
  // not the new value v at p due to reordering of the two
  // stores. Note that CMS has a concurrent precleaning phase, where
  // it reads the card table while the Java threads are running.
  update_barrier_set((void*)p, v, true /* release */);    // cast away type
}

G1 实现 SATB 源码

void G1SATBCardTableModRefBS::enqueue(oop pre_val) {
  // Nulls should have been already filtered.
  assert(pre_val->is_oop(true), "Error");

  if (!JavaThread::satb_mark_queue_set().is_active()) return;
  Thread* thr = Thread::current();
  if (thr->is_Java_thread()) {
    JavaThread* jt = (JavaThread*)thr;
    jt->satb_mark_queue().enqueue(pre_val);
  } else {
    MutexLockerEx x(Shared_SATB_Q_lock, Mutex::_no_safepoint_check_flag);
    JavaThread::satb_mark_queue_set().shared_satb_queue()->enqueue(pre_val);
  }
}
记忆集与卡表

在新生代垃圾标记收集的过程中,可能会出现跨代引用的情况,例如年轻代的某个对象是由老年代中的某个对象引用着的,导致新生代 GC ROOTS 可达性分析的扫描还需要到老年代中去遍历扫描,效率很差,为此新生代引入了记忆集的概念。
在新生代中会维护一块内存区域来记录所有的跨代引用对象。
hotspot 使用卡表的方式实现记忆集 记忆集与卡表的关系可以理解为 Map 与 HashMap 的关系
具体的实现方式是在老年代会将老年代的内存区域划分为一个一个的卡页,默认每个卡页的大小为 512 bytes ,在年轻代会划分一块小的内存区域来维护 CARD_TABLE[] 卡表(字节数组),如果老年代卡页中存在一个跨代引用的对象就会在卡表中记录下来(卡页内存的地址,卡页状态),在 GC ROOTS 扫描时会将该卡页中的对象也加入扫描。

如果您的Oracle数据库安装了Parallel Execution选项,并且您已经使用了PARALLEL提示来尝试并行执行查询,但是查询仍然未使用并行执行,请考虑以下几个可能的原因: 1. 数据库配置问题:请确保您的数据库实例已经正确配置了Parallel Execution选项。您可以通过检查V$OPTION视图中的PARALLEL选项来验证这一点。 2. 对象未以并行方式创建:如果您尝试并行执行的对象(如表、索引等)未以并行方式创建,则无法使用并行执行。您可以使用下面的命令来检查表或索引是否以并行方式创建: ``` SELECT owner, table_name, degree FROM dba_tables WHERE table_name = 'your_table_name'; ``` ``` SELECT owner, index_name, degree FROM dba_indexes WHERE table_name = 'your_table_name'; ``` 3. 并行度太低:如果您的查询使用了PARALLEL提示,但是并行度设置得太低,Oracle可能会决定不使用并行执行。您可以尝试增加并行度,例如: ``` SELECT /*+ PARALLEL(your_table_name, 8) */ * FROM your_table_name; ``` 在这个例子中,我们将并行度设置为8,您可以根据需要进行调整。 4. 查询语句不适合并行执行:某些查询语句可能不适合并行执行。例如,如果查询中包含大量的小表连接,则并行执行可能会导致性能下降。在这种情况下,您可以尝试重新编写查询,或者取消使用PARALLEL提示。 如果以上方法均未解决您的问题,请尝试检查Oracle数据库的日志文件以查看更多信息。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

dying 搁浅

两杯酒,一杯敬你余生多欢喜。

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值