多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[TOC] # 垃圾回收 * 如何判定对象为垃圾对象 - 引用计数法 - 可达性分析法 * 回收策略 - 标记-清除算法 - 复制算法 - 标记-整理算法 - 分代回收算法 * 垃圾回收器 - Serial - Parnew - Cms - G1 # 算法 ## 引用计数算法 在对象中添加一个引用计数器,当有地方引用这个对象的时候,引用计数器的值就+1,当引用失效的时候,计数器的值就-1. 打印gc信息 * -verbose:gc * -xx:+PrintGCDetails ## 可达性分析算法 作为GCRoots的对象 * 虚拟机栈 * 方法区的类属性所引用的对象 * 方法区中常量所引用的对象 * 本地方法栈中引用的对象 ## 标记-清除算法 * 效率问题 * 空间问题,空间碎片 ## 复制算法 * 堆 - 新生代 - Eden伊甸园 - Survivor 存活区 - Tenured Gen - 老年代 将内存分为一大块Eden和其中一块Survivor. 当Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上. 最后清理到刚才的Eden和Survivor. 默认Eden和Survivior比例是8:1. 当Survivor空间不够用的时候需要依赖其他内存(老年代),进行分配担保 ## 标记-整理算法 标记过程和标记-清除算法一样. 后续不是直接对可回收对象进行整理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 ## 分代收集算法 根据对象的存活周期,把堆分为新生代和老年代. 根据不同区域选择最适当的算法. 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量的存活,那就用复制算法,只要付出少量存活对象的复制成本就可以完成收集. 老年代中因为对象存活率高,没有额外空间对他进行分配担保,那就用标记-清理,标记-整理算法 # HotSpot算法实现 # 相关概念 ## 并行和并发 * **并行(Parallel)**:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。 * **并发(Concurrent)**:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。 ## 吞吐量(Throughput) 吞吐量就是**CPU用于运行用户代码的时间**与**CPU总消耗时间**的比值,即 **吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。** 假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。 ## Minor GC 和 Full GC * **新生代GC(Minor GC)**:指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。具体原理见上一篇文章。 * **老年代GC(Major GC / Full GC)**:指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。 ## 收集器配合 展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用。虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。 Hotspot实现了如此多的收集器,正是因为目前并无完美的收集器出现,只是选择对具体应用最适合的收集器。 ![](https://img.kancloud.cn/03/6a/036a050ca122c92a64228625e2151dd1_936x866.png) ## YGC和FGC是什么   YGC :对新生代堆进行gc。频率比较高,因为大部分对象的存活寿命较短,在新生代里被回收。性能耗费较小。   FGC :全堆范围的gc。默认堆空间使用到达80%(可调整)的时候会触发fgc。以我们生产环境为例,一般比较少会触发fgc,有时10天或一周左右会有一次。 ## 什么时候执行YGC和FGC **YGC的时机:** * edn空间不足 **FGC的时机:** * old空间不足; * perm空间不足; * 显示调用System.gc() ,包括RMI等的定时触发; * YGC时的悲观策略; * dump live的内存信息时(jmap –dump:live)。 对YGC的 触发时机,相当的显而易见,就是eden空间不足, 这时候就肯定会触发ygc 对于FGC的触发时机, old空间不足, 和perm的空间不足, 调用system.gc()这几个都比较显而易见,就是在这种情况下, 一般都会触发GC。 最复杂的是所谓的悲观策略,它触发的机制是在首先会计算之前晋升的平均大小,也就是从新生代,通过ygc变成新生代的平均大小,然后如果旧生代剩余的空间小于晋升大小,那么就会触发一次FullGC。sdk考虑的策略是, 从平均和长远的情况来看,下次晋升空间不够的可能性非常大, 与其等到那时候在fullGC 不如悲观的认为下次肯定会触发FullGC, 直接先执行一次FullGC。而且从实际使用过程中来看, 也达到了比较稳定的效果。 # 新生代收集器 ## Serial收集器 **Stop The World** * 单线程垃圾收集器 * 桌面应用 只会使用一个cpu或者一条收集器会完成,中间这段时间会停止其他线程,等待GC完成. 下图展示了Serial 收集器(老年代采用Serial Old收集器)的运行过程: ![](https://img.kancloud.cn/94/07/94076585e6b1a34db3514d26fe3a6687_1408x674.png) ## ParNew收集器 Serial收集器多线程版本,使用多线程进行GC ParNew收集器的工作过程如下图(老年代采用Serial Old收集器): ![](https://img.kancloud.cn/0f/d5/0fd50cfba2d9d9c7e63377749dd8ac3b_1342x396.png) **除了Serial收集器外,目前只有它能和CMS收集器(Concurrent Mark Sweep)配合工作** 默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用**\-XX:ParallerGCThreads**参数设置 ## Parallel Scavenge收集器 是一个**并行**的**多线程新生代**收集器,它也使用**复制算法** CMS等收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是**达到一个可控制的吞吐量(Throughput)** **停顿时间越短就越适合需要与用户交互的程序**,良好的响应速度能提升用户体验。而**高吞吐量**则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合**在后台运算而不需要太多交互的任务**。 除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数**\-XX:+UseAdaptiveSizePolicy**,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为**GC自适应的调节策略(GC Ergonomics)**。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。 控制吞吐量(执行用户代码时间 / (执行用户代码的时间 + 垃圾回收所占用的时间)) * -XX:MaxGFPauseMillis垃圾收集器停顿时间 * -XX:CGTimeRatio吞吐量大小 - (0, 100)吞吐量大小 - 99默认 # 老年代收集器 ## Serial Old收集器 Serial Old 是 Serial收集器的老年代版本,它同样是一个**单线程收集器**,使用**“标记-整理”(Mark-Compact)**算法。 此收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,它还有两大用途: * 在JDK1.5 以及之前版本(Parallel Old诞生以前)中与Parallel Scavenge收集器搭配使用。 * 作为CMS收集器的后备预案,在并发收集发生**Concurrent Mode Failure**时使用 它的工作流程与Serial收集器相同,这里再次给出Serial/Serial Old配合使用的工作流程图: ![](https://img.kancloud.cn/77/d2/77d221b8c70c1f2bc9918eb095bddeff_663x204.png) ## Parallel Old收集器 Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用**多线程**和**“标记-整理”**算法. **吞吐量优先”收集器**终于有了比较名副其实的应用组合,在**注重吞吐量**以及**CPU资源敏感**的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。Parallel Old收集器的工作流程与Parallel Scavenge相同,这里给出Parallel Scavenge/Parallel Old收集器配合使用的流程图: ![](https://img.kancloud.cn/d9/db/d9db0f60efc69829c405b7d10cf2e112_661x188.png) ## CMS收集器 是一种以**获取最短回收停顿时间**为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度 **并发的标记-清除收集器** * 工作过程 - 初始标记:仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”. - 并发标记:进行**GC Roots Tracing**的过程,在整个过程中耗时最长. - 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。此阶段也需要“Stop The World”. - 并发清理 ![](https://img.kancloud.cn/4f/d1/4fd14b863d3b48da4526c963dc1b9b33_688x191.png) ![](https://img.kancloud.cn/88/fb/88fba9e2043e271a09de02a813b6c617_616x400.png) 如果那块内存区域给的不够会出现Concurrent Mode Failure * 优点 - 并发收集 - 低停顿 * 缺点 - 占用大量的cpu资源 - 无法处理浮动垃圾 - 出现Concurrent Mode Failure - 空间碎片: 标记-清除算法 可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。**由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。**这一部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就被称为**“浮动垃圾”**。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。 # G1收集器 步骤 * **初始标记(Initial Marking)**仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改**TAMS(Nest Top Mark Start)**的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要**停顿线程**,但耗时很短。 * **并发标记(Concurrent Marking)**从GC Root 开始对堆中对象进行**可达性分析**,找到存活对象,此阶段耗时较长,但**可与用户程序并发执行**。 * **最终标记(Final Marking)**为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在**线程的Remembered Set Logs**里面,最终标记阶段需要**把Remembered Set Logs的数据合并到Remembered Set中**,这阶段需要**停顿线程**,但是**可并行执行**。 * **筛选回收(Live Data Counting and Evacuation)** 首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段(Safepoint处): ![](https://img.kancloud.cn/d2/c7/d2c7d32ac9d3194b179ce4f0ca6a51fd_642x183.png) **横跨整个堆内存** 在G1之前的其他收集器进行收集的范围都是整个新生代或者老生代,而G1不再是这样。G1在使用时,Java堆的内存布局与其他收集器有很大区别,它**将整个Java堆划分为多个大小相等的独立区域(Region)**,虽然还保留新生代和老年代的概念,但**新生代和老年代不再是物理隔离的了,而都是一部分Region(不需要连续)的集合** **有计划地避免在整个Java堆中进行全区域的垃圾收集** **在后台维护一个优先列表** **优先回收价值最大的Region** **避免全堆扫描——Remembered Set** 为了避免全堆扫描的发生,虚拟机**为G1中每个Region维护了一个与之对应的Remembered Set**。虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作,检查Reference引用的对象是否处于不同的Region之中(在分代的例子中就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable**把相关引用信息记录到被引用对象所属的Region的Remembered Set之中**。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。 * 优势 - 并行与并发 - 分代收集 - 空间整合: 整体来看是基于标记-整理,从局部(两个Region之间)上来看是基于复制算法实现的 - 可预测的停顿 * 步骤 - 初始标记 - 并发标记 - 最终标记 - 筛选回收