1 相关概念:
并行收集:指多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态。
并发收集:指用户线程与垃圾收集线程同时工作(不一定是并行的可能会交替执行)。用户程序在继续运行,而垃圾收集程序运行在另一个 CPU 上
吞吐量:即 CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 )),也就是。例如:虚拟机共运行 100 分钟,垃圾收集器花掉 1 分钟,那么吞吐量就是 99% 。
-
串行
单线程 堆内存较小,适合个人电脑
-
吞吐量优先
多线程 堆内存较大,多核 cpu 让单位时间内,STW 的时间最短 。 0.2 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高
-
响应时间优先
多线程 堆内存较大,多核 cpu 尽可能让单次 STW 的时间最短 0.1 0.1 0.1 0.1 0.1 = 0.5
2 串行
-XX:+UseSerialGC=serial + serialOld
安全点:让其他线程都在这个点停下来,以免垃圾回收时移动对象地址,使得其他线程找不到被移动的对象
因为是串行的,所以只有一个垃圾回收线程。且在该线程执行回收工作时,其他线程进入阻塞状态
Serial 收集器
Serial 收集器是最基本的、发展历史最悠久的收集器
特点:单线程、简单高效(与其他收集器的单线程相比),采用复制算法。对于限定单个 CPU 的环境来说,Serial 收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。收集器进行垃圾回收时,必须暂停其他所有的工作线程,直到它结束(Stop The World)!
ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本
特点:多线程、ParNew 收集器默认开启的收集线程数与CPU的数量相同,在 CPU 非常多的环境中,可以使用 -XX:ParallelGCThreads 参数来限制垃圾收集的线程数。和 Serial 收集器一样存在 Stop The World 问题
Serial Old 收集器
Serial Old 是 Serial 收集器的老年代版本
特点:同样是单线程收集器,采用标记-整理算法
3 吞吐量优先(JDK1.8默认的垃圾回收器)
-XX:+UseParallelGC ~ -XX:+UsePrallerOldGC
-XX:+UseAdaptiveSizePolicy
-XX:GCTimeRatio=ratio // 1/(1+radio)
-XX:MaxGCPauseMillis=ms // 默认值为 200ms
-XX:ParallelGCThreads=n
垃圾回收线程与服务器核数相关
Parallel Scavenge 收集器
与吞吐量关系密切,故也称为吞吐量优先收集器
特点:属于新生代收集器也是采用复制算法的收集器(用到了新生代的幸存区),又是并行的多线程收集器(与 ParNew 收集器类似)
该收集器的目标是达到一个可控制的吞吐量。还有一个值得关注的点是:GC自适应调节策略(与 ParNew 收集器最重要的一个区别)
GC自适应调节策略:
Parallel Scavenge 收集器可设置 -XX:+UseAdptiveSizePolicy 参数。
当开关打开时不需要手动指定新生代的大小(-Xmn)、Eden 与 Survivor 区的比例(-XX:SurvivorRation)、
晋升老年代的对象年龄(-XX:PretenureSizeThreshold)等,虚拟机会根据系统的运行状况收集性能监控信息,动态设置这些参数以提供最优的停顿时间和最高的吞吐量,这种调节方式称为 GC 的自适应调节策略。
Parallel Scavenge 收集器使用两个参数控制吞吐量:
XX:MaxGCPauseMillis=ms 控制最大的垃圾收集停顿时间(默认200ms)
XX:GCTimeRatio=rario 直接设置吞吐量的大小
Parallel Old 收集器是 Parallel Scavenge 收集器的老年代版本
特点:多线程,采用标记-整理算法(老年代没有幸存区)
4 响应时间优先
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
-XX:ParallelGCThreads=n ~ -XX:ConcGCThreads=threads
-XX:CMSInitiatingOccupancyFraction=percent
-XX:+CMSScavengeBeforeRemark
CMS 收集器
Concurrent Mark Sweep,一种以获取最短回收停顿时间为目标的老年代收集器
特点:基于标记-清除算法实现。并发收集、低停顿,但是会产生内存碎片
应用场景:适用于注重服务的响应速度,希望系统停顿时间最短,给用户带来更好的体验等场景下。如 web 程序、b/s 服务
CMS 收集器的运行过程分为下列4步:
初始标记:标记 GC Roots 能直接到的对象。速度很快但是仍存在 Stop The World 问题。
并发标记:进行 GC Roots Tracing 的过程,找出存活对象且用户线程可并发执行。
重新标记:为了修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。仍然存在 Stop The World 问题
并发清除:对标记的对象进行清除回收,清除的过程中,可能任然会有新的垃圾产生,这些垃圾就叫浮动垃圾,如果当用户需要存入一个很大的对象时,新生代放不下去,老年代由于浮动垃圾过多,就会退化为 serial Old 收集器,将老年代垃圾进行标记-整理,当然这也是很耗费时间的!
CMS 收集器的内存回收过程是与用户线程一起并发执行的,可以搭配 ParNew 收集器(多线程,新生代,复制算法)与 Serial Old 收集器(单线程,老年代,标记-整理算法)使用。
5 G1收集器
定义:Garbage First
2004 论文发布
2009 JDK 6u14 体验
2012 JDK 7u4 官方支持
2017 JDK 9 默认
适用场景
同时注重吞吐量(Throughput)和低延迟(Low latency),默认的暂停目标是 200 ms
超大堆内存,会将堆划分为多个大小相等的 Region
整体上是 标记+整理 算法,两个区域之间是 复制 算法
相关 JVM 参数
-XX:+UseG1GC
-XX:G1HeapRegionSize=size
-XX:MaxGCPauseMillis=time
1) G1 垃圾回收阶段
Young Collection:对新生代垃圾收集
Young Collection + Concurrent Mark:如果老年代内存到达一定的阈值了,新生代垃圾收集同时会执行一些并发的标记。
Mixed Collection:会对新生代 + 老年代 + 幸存区等进行混合收集,然后收集结束,会重新进入新生代收集。
Young Collection
分代是按对象的生命周期划分,分区则是将堆空间划分连续几个不同小区间,每一个小区间独立回收,可以控制一次回收多少个小区间,方便控制 GC 产生的停顿时间!
E:eden,S:幸存区,O:老年代
新生代收集会产生 STW !
Young Collection + CM
在 Young GC 时会进行 GC Root 的初始化标记。
老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的 JVM 参数决定
-XX:InitiatingHeapOccupancyPercent=percent (默认45%)
当老年代占堆内存的45%的时候,就开始进行并发标记了。
Mixed Collection
会对 E S O 进行全面的回收
最终标记会 STW
拷贝存活会 STW
-XX:MaxGCPauseMills=xxms 用于指定最长的停顿时间!
问:为什么有的老年代被拷贝了,有的没拷贝?
因为指定了最大停顿时间,如果对所有老年代都进行回收,耗时可能过高。为了保证时间不超过设定的停顿时间,会回收最有价值的老年代(回收后,能够得到更多内存)
Full GC
SerialGC
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc
ParallelGC
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足发生的垃圾收集 - full gc
CMS
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足
G1
新生代内存不足发生的垃圾收集 - minor gc
老年代内存不足
G1 在老年代内存不足时(老年代所占内存超过阈值)。
如果垃圾产生速度慢于垃圾回收速度,不会触发 Full GC,还是并发地进行清理。
如果垃圾产生速度快于垃圾回收速度,便会触发 Full GC,然后退化成 serial Old 收集器串行的收集,就会导致停顿的时候长。
Young Collection 跨代引用
新生代回收的跨代引用(老年代引用新生代)问题
卡表 与 Remembered Set
Remembered Set 存在于E中,用于保存新生代对象对应的脏卡
脏卡:O 被划分为多个区域(一个区域512K),如果该区域引用了新生代对象,则该区域被称为脏卡
在引用变更时通过 post-write barried + dirty card queue
concurrent refinement threads 更新 Remembered Set
Remark
重新标记阶段
在垃圾回收时,收集器处理对象的过程中
黑色:已被处理,需要保留的
灰色:正在处理中的
白色:还未处理的
但是在并发标记过程中,有可能 A 被处理了以后未引用 C ,但该处理过程还未结束,在处理过程结束之前 A 引用了 C ,这时就会用到 remark 。
过程如下
之前 C 未被引用,这时 A 引用了 C ,就会给 C 加一个写屏障,写屏障的指令会被执行,将 C 放入一个队列当中,并将 C 变为 处理中状态。
在并发标记阶段结束以后,重新标记阶段会 STW ,然后将放在该队列中的对象重新处理,发现有强引用引用它,就会处理它,由灰色变成黑色。
JDK 8u20 字符串去重
过程
-
将所有新分配的字符串(底层是 char[] )放入一个队列
-
当新生代回收时,G1 并发检查是否有重复的字符串
-
如果字符串的值一样,就让他们引用同一个char[]
-
注意,其与 String.intern() 的区别
- String.intern() 关注的是字符串对象
- 字符串去重关注的是 char[]
- 在 JVM 内部,使用了不同的字符串标
优点与缺点
节省了大量内存
新生代回收时间略微增加,导致略微多占用 CPU
-XX:+UseStringDeduplication
JDK 8u40 并发标记类卸载
在并发标记阶段结束以后,就能知道哪些类不再被使用。如果一个类加载器的所有类都不在使用,则卸载它所加载的所有类-XX:+ClassUnloadingWithConcurrentMark 默认启用。
JDK 8u60 回收巨型对象
- 一个对象大于region的一半时,就称为巨型对象
- G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有incoming引用,如果老年代incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉
JDK 9 并发标记起始时间的调整
- 并发标记必须在堆空间占满前完成,否则退化为 FulGC。
- JDK 9 之前需要使用 -XX:InitiatingHeapOccupancyPercent,阈值默认为45%。
- JDK 9 可以动态调整。
- XX:InitiatingHeapOccupancyPercent 用来设置初始值。
- 进行数据采样并动态调整。
- 总会添加一个安全的空挡空间。