此篇博客主要以笔记的形式,记录笔者在B站《深入理解JVM》课程中学到的知识点。课程地址:《黑马程序员JVM完整教程,全网超高评价,全程干货不拖沓》
1.串行回收器(Serial、SerialOld)
- SerialOld 工作在老年代,使用的是标记-整理算法
- Serial 工作在新生代,使用标记-复制算法
- 开启串行垃圾回收器参数
-XX:UseSerialGC = Serial + SerialOld
执行流程图如下(来自课程截图)
如上图: 所有线程运行到安全点后除了垃圾回收线程,其他的线程将会被阻塞。等到垃圾回收处理往后再恢复运行。
为什么要停止其他线程? 因为在垃圾回收的过程中,如果其他的线程没有停止,对象的地址可能会被其他线程所改变,此时垃圾回收将会发生错误。为了避免这种情况发生,需要停止其他线程的运行。
2.并行回收器(Parallel、ParallelOld)
- -XX:+UseParallelGC ~ -XX:+UseParallelOldGC
- -XX:UseAdaptiveSizePolicy
- -XX:GCTimeRatio=ratio
- -XX:MaxGCPauseMillis=ms
- -XX:ParallelGCThreads=n
执行流程图如下(来自课程截图)
3.并发回收器(CMS、ParNew)
CMS
是一款并发、使用标记-清理(Mark-Sweep)算法的gc,是针对老年代进行回收的GC
,设计目标是避免在老年代垃圾收集时出现长时间的卡顿,以回收停顿时间最短为目标的垃圾回收器(即:响应时间优先)。
// CMS与ParNew一起工作运行,当发生并发失败的问题时,将会退化为SerialOld
-XX:+UseConcMarkSweepGC ~ -XX:+UseParNewGC ~ SerialOld
// 线程数
-XX:ParallelGCThread=n ~ -XX:ConcGCThreads=threads
// 内存占用到达一定值后触发垃圾回收,给预留空间
-XX:CMSInitiatingOccupancyFraction=percent
// 在重新标记前,对新生代做一次垃圾回收
-XX:+CMSScavengeBeforeRemark
执行流程图如下(来自课程截图)
CMS的回收器主要分为:初始标记(STW)
、并发标记
、并发预清理
、重新标记(STW)
、并发清理
、并发重置
六个阶段
3.1.初始标记(STW)
在此阶段,CMS会发生STW(stop the world),并标记与GC ROOT
直接关联的老年代对象。要注意的是,为了最大程度减少其他线程STW的时间,在初始标记阶段只标记直接关联的老年代对象,间接关联的老年代对象将会在并发标记总进行。
3.2.并发标记
在此阶段,CMS已经结束STW,恢复了其他线程的运行,此时再开始遍历初始标记中被标记的直接对象,将间接老年代对象也标记出来。
但由于结束了STW,其他的用户线程有可能对线程中的对象做改变,如:
- 新生代对象晋升到老年代
- 老年代的引用发生改变
这时我们为了防止遗漏上述问题的对象,需要对他们进行重新标记,但为了少部分的遗漏对象而重新扫描整个老年代显然是一种需要消耗大量时间的方法,并且CMS是注重响应时间的算法,因此在这里我们需要引入 卡表(card table) 的概念,即将老年代区域分为若干个512kb
的区域,如果某一块区域存在被遗漏的对象,则将该区域标记为脏(dirty),进行重新标记操作时,只需要标记脏区域即可,从而避免扫描整个老年代区域。
如下图: 假设在并发标记的过程中,新生代中对象k
晋升为了老年代,此时对象k
会被CMS算法遗漏,属于脏对象,而其所在的卡片空间会被标记为脏空间,等到重新标记阶段时,回收器会到此对里面的对象进行重新标记的操作。
3.3.并发预清理
因为在下一步操作中将开始进行重新标记,而这一步操作同样需要进行STW
,为了尽可能的减少STW的时间,将卡表中的脏对象进行标记的任务需要放在并发阶段解决。
而并发预清理阶段将会重新扫描前一个阶段标记的脏对象,并将被为脏对象直接或间接引用的对象进行标记,遗漏的对象被标记完成后脏表的标记自然失去了用途,也应该被清除 。
3.4.重新标记(STW)
- 遍历新生代对象,重新标记(新生代会被分块,多线程扫描)
- 根据GC Roots,重新标记
- 遍历老年代的脏对象,重新标记。这里的脏对象,大部分已经在并发预清理阶段被处理过了
3.5.并发清除
如下图: 经历了上述几个步骤了,未被标记的对象G
将会从老年代中被清除。
3.6.并发重置
将清理并恢复在CMS GC过程中的各种状态,重新初始化CMS相关数据结构,为下一个垃圾收集周期做好准备。
4.Garbage First(G1)
定义:
- 2004年论文发布
- 2009 JDK 6u14 体验
- 2012 JDK 7u4 官方支持
- 2017 JDK 9 默认
适用场景:
- 同时注重吞吐量(Throughput)和低延迟(Low lateency),默认暂停目标是200ms
- 超大堆内存,会将堆内存划分为多个大小相等的
Region
- 整体上是标记-整理算法,两个区域之间是标记-复制算法
相关JVM参数:
- -XX:+UseG1GC
- -XX:G1HeapRegionSize=size
- -XX:MaxGCPauseMillis=time
回收阶段主要分为以下三个阶段:
- 新生代垃圾回收(
Young Collection
):会发生STW - 新生代垃圾回收 + 并发标记(
Young Collection + Concurrent Mark
) - 混合收集(
Mixed Collection
)
4.1.Young Collection
G1会将堆内存划分为多个大小相等的Region
,每个区域都可以独立的作为伊甸园Eden
、幸存区Survive
、老年代Old
。(如下图,空白的区域代表空闲的内存空间,E则代表被新生代伊甸园的对象占用的空间)
4.2.Young Collection + CM
- 在Young GC 时会进行 GC ROOT 的初始标记
- 老年代占用堆空间比例达到阈值时,进行并发标记(不会STW),由下面的JVM参数决定:
-XX:InitiatingHeapOccupancyPercent=percent
(默认45%)
4.3.Mixed Collection
会对E
、S
、O
进行全面的垃圾回收(即:伊甸园、幸存区已经老年代)
- 重新标记(Remark)会STW
- 拷贝存活(Evacuation)会STW
- 最大暂停时间通过下面的JVM参数控制:
-XX:MaxGCPauseMillis=ms
如下图:当垃圾回收被触发后, 伊甸园E
中存活的对象将会被复制到 幸存区S
中,同样的,两个幸存区中的存活对象会互相复制置换,知道幸存区中的对象达到一定寿命后仍然没有被清理,从而晋升为 老年代O
。而老年代同样也会发生垃圾回收,被清理完后并存活的老年代对象将会被复制到一块新的老年代区域中。
需要注意的: 在下图老年代的回收中,只回收了两块内存区域,是因为G1需要根据最大的暂停时间(即:-XX:MaxGCPauseMillis=ms
中设置的值)有选择的进行垃圾回收。为了要达到目标,G1会从多块老年代内存区域中选择最有价值的区域进行回收(即:优先清理能够释放更多资源的区域)。
G1与前面提到的CMS一样,在并发标记阶段会存在遗漏的老年代对象,同样的,G1也使用了 卡表(card table) 来处理这个问题,由于在CMS中已经介绍过卡表的工作原理,在这里笔者不再赘述(如下)
5.关于JDK1.8中G1的优化
5.1.JDK 8u20 字符串去重
- 优点:节省大量内存
- 缺点:略微多占用了CPU时间,新生代回收时间略微增加
-XX:UseStringDeduplication
String s1 = new String("hello"); // char[]{'h','e','l','l','o'}
String s2 = new String("hello"); // char[]{'h','e','l','l','o'}
- 将所有新分配的字符串放入一个队列
- 当新生代回收时,G1并发检查是否有字符串重复
- 如果他们值一样,让他们引用同一个
char[]
- 注意,与
String.intern()
不一样- String.intern()关注的是字符串对象
- 而字符串去重关注的是char[]
- 在JVM内部,使用了不同的字符串表
5.2.JDK 8u40 回收巨型对象
所有对象都经过并发标记后,就能知道哪些类不再被使用,当一个类加载器的所有类都不再使用,则卸载它所加载的所有类。(默认启用)
-XX:+ClassUnloadingWithConcurrentMark
5.3.JDK 8u60 回收巨型对象
- 一个对象大于
Region
的一半时,称之为巨型对象 - G1不会对巨型对象进行拷贝
- 回收时被优先考虑
- G1会跟踪老年代所有
incoming
引用,这样老年代incoming
因为为0的巨型对象就可以在新生代垃圾回收时处理掉