摘要
前一节,我们讲解了内存回收的方法论,从基本的回收对象是否存活引入了:直接引用计数法(对象添加引用计数器)、可达性分析法(引用对象从GC Root出发。通过引用链查找),正因为直接引用计数法无法解决循环引用问题,引入可达性分析法。然后引入垃圾对象回收的算法方法论:最简单的是“标记-清除法”:使用可达性分析法将内存对象标记为垃圾对象,然后清除垃圾对象,标记-清除法在对象存活时间比较长的内存区域效率低下:会消耗大量时间标记清除大量内存对象,标记清除法清除垃圾对象的时候会导致不连续的内存空间,引出空间碎片化问题。所以为了解决上面问题引出了标记-复制算法:将空间分为两部分:正在使用的其中一块空间,当对象标记为垃圾对象,需要出发GC时候,将存对象一次性移到另一块内存,并且使用移动内存指针方式让另一块内存空间连续话,然后gc掉第一块所有内存,解决了内存碎片化问题,在内存对象存活周期短的区域,只需要移动少量的存活对象到另一块区域,效率高,所以适合在堆内存里面的新生代;对于老年代;我们需要引出:标记-整理算法:将垃圾对象进行标记,然后gc时候通过指针将内存对象连续化。这一讲我们来研究具体的内存回收实现:垃圾回收器。
我们主要分析常用的虚拟机(HotSpot)的垃圾回收器。
展示了七种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明它们可以搭配使用,图中收集器所处的区域,则表示它是属于新生代收集器抑或是老年代收集器。
思维导图
内容
我们从简单到复杂主要分析5种垃圾回收器:Serial收集器、ParNew收集器、Parallel Scavenge收集器、CMS收集器、G1收集器.
Serial收集器
原理/是什么:单线程垃圾回收器,进行垃圾回收时候,用户线程全部停止,直到垃圾回收结束。
图解:
单线程工作的收集器,但它的“单线程”的意义并不仅仅是说明它只会使用一个处理器或一条收集线程去完成垃圾收集工作,更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。“Stop The World”这个词语也 许听起来很酷,但这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可知、不可控的情况 下把用户的正常工作的线程全部停掉,这对很多应用来说都是不能接受的。
特点:
缺点:“Stop The World”:它进⾏垃圾收集时,必须暂停其他所有的⼯作线程,直到它收集结束。在⽤户不可⻅的情况下把⽤户正常⼯作的线程全部停掉。(从Serial收集器到Parallel收集器,再到Concurrent Mark Sweep(CMS)和Garbage First(G1)收集器,最终至现在垃圾收集器的最前沿成果Shenandoah和ZGC 等,我们看到了一个个越来越构思精巧,越来越优秀,也越来越复杂的垃圾收集器不断涌现,用户线 程的停顿时间在持续缩短,但是仍然没有办法彻底消除)
优点:多⽤于桌⾯应⽤,是客户端Client模式下的虚拟机。桌⾯应⽤暂用内存⼩,进⾏垃圾回收的时间⽐较短,只要不频繁发⽣停顿就可以接受。(因为是单线程的,所以消耗cpu跟内存相对比较小)
ParNew收集器
回顾:
上一节我们讲解了垃圾回收器里面第一种垃圾回收器Serial。Serial垃圾回收器采用单线程垃圾回收,他的性能是比较差的。为了解决Serial的性能问题,我们引入了另一种垃圾回收器:ParNew垃圾回收器。
原理:ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之 外,其余的行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX: PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规 则、回收策略等都与Serial收集器完全一致
图解:
1、ParNew收集器除了支持多线程并行收集之外,其他与Serial收集器相比并没有太多创新之处,但它 却是不少运行在服务端模式下的HotSpot虚拟机,尤其是JDK 7之前的遗留系统中首选的新生代收集 器,其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS 收集器配合工作。
2、CMS作为老年代的收集器,却无法与JDK 1.4.0中已经存在的新生代收集器Parallel Scavenge配合工作[1],所以在JDK 5中使用CMS来收集老年代的时候,新生代只能选择ParNew或者 Serial收集器中的一个。ParNew收集器是激活CMS后(使用-XX:+UseConcMarkSweepGC选项)的默 认新生代收集器,也可以使用-XX:+/-UseParNewGC选项来强制指定或者禁用它。
3、G1是一个面向全堆的收集器,不 再需要其他新生代收集器的配合工作。所以自JDK 9开始,ParNew加CMS收集器的组合就不再是官方 推荐的服务端模式下的收集器解决方案了。官方希望它能完全被G1所取代,甚至还取消了ParNew加 Serial Old以及Serial加CMS这两组收集器组合的支持(其实原本也很少人这样使用)
特点:
---1、ParNew 收集器除了多线程收集之外,其他与 Serial 收集器相⽐并没有太多创新之处,但它却是许多运⾏在 Server 模式下(之前我们的Serial是运行在client端的)的虚拟机中⾸选的新⽣代收集器,其中有⼀个与性能⽆关但很重要的原因是,除了 Serial 收集器外,⽬前只有它能与CMS收集器配合⼯作(ParNew一般跟CMS一起来使用;我们的ParNew一般用来回收新生代,而CMS一般用来回收老年代)。
---2、使⽤-XX: ParallelGCThreads 参数来限制垃圾收集的线程数。(这个设置有一个参照点:大家知道我们的cpu有一个核数,这个核数代表了我们cpu同时能处理多少个线程的数量,如果cpu核数是8的话,建议将我们的ParallelGCThreads参数值设置为8)
---3、多线程操作存在上下⽂切换的问题,所以建议将-XX: ParallelGCThreads设置成和CPU核数相同,如果设置太多的话就会产⽣上下⽂切换消耗。(所以并不是我们这个ParallelGCThreads参数越大越好、他会有上下文切换的失效)
收集器的上下文语境中:并发与并行
并发:描述GC线程跟用户线程之间关系:GC线程跟用户线程同时运行。
并行:描述GC线程间关系:多个GC线程同时运行,用户线程暂停。
Parallel Scavenge收集器
回顾:上一节我们讲解了ParNew垃圾回收器,ParNew垃圾回收器是在Serial基础上实现的一个多线程的扩充。多线程的垃圾回收器除了ParNew之外,还有Parallel Scavenge垃圾回收器。
是什么:控制的吞吐量的ParNew收集器(也可以叫做:基于标记-复制算法实现的多线程吞吐量优先的垃圾回收器)
特点: 1、多线程垃圾回收 2、关注吞吐量 3、参数可调。
与其他垃圾回收器区别: 关注点:其他垃圾回收线程关注缩短垃圾收集时用户线程的停顿时间。Parallel Scavenge收集器的目标则是达到一个可控制的吞吐 量(Throughput)
吞吐量: 吞吐量=用户线程执行时间/用户线程执行时间+GC线程执行时间
CMS收集器
回顾: 上一节我们讲解了两个概念,一个是并发一个是并行,并且我们讲解了ParNew跟Parallel Scavenge收集器,他们都是并行的垃圾回收器,当工作线程运行到一半时候会被阻断运行GC线程,GC垃圾回收之后会再次运行工作线程。除了并行之外,还有一种垃圾回收器他是可以并发执行的。CMS垃圾收集器。
是什么?: CMS(Concurrent Mark Sweep)基于标记-清除算法实现的一种以获取最短回收停顿时间为目标的收集器。目前很 大一部分的Java应用集中在互联网网站或者基于浏览器的B/S系统的服务端上,这类应用通常都会较为 关注服务的响应速度,希望系统停顿时间尽可能短,以给用户带来良好的交互体验。
图解4步骤:
出CMS收集器是基于标记-清除算法实现的整个过程分为四个步骤,包括: 1)初始标记(CMS initial mark):是标记GC Roots能直接关联到的对象,速度很快.
2)并发标记(CMS concurrent mark):从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;
3)重新标记(CMS remark):为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录;这个阶段的停顿时间通常会比初始标记阶段稍长些,但也远比并发标记阶段的时间短;
4)并发清除(CMS concurrent sweep):清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。
初始标记、重新标记这两个步骤仍然需要“Stop The World”.
由于在整个过程中耗时最长的并发标记和并发清除阶段中,垃圾收集器线程都可以与用户线程一 起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的阶段。
特点:
优点:并发收集、低停顿。
缺点:
1、对CPU资源⾮常敏感(特别是单核机器-并发会占用更多资源)
2、⽆法处理浮动垃圾,并发清除时候已经产生了一些垃圾;浮动垃圾:程序在进⾏并发清除阶段⽤户线程所产⽣的新垃圾。
3、标记-清除暂时空间碎⽚。
G1收集器.
回顾:上一节我们讲解CMS垃圾收集器,这一节我们讲解更加高效的垃圾收集器:G1收集器。
是什么?: G1是⼀款⾯向服务端应⽤的垃圾收集器。是基于标记-整理法;首先他对性能的要求会特别高。
流程步骤:
·初始标记(Initial Marking): 仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS 指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要 停顿线程,但耗时很短,而且是借用进行Minor GC的时候同步完成的,所以G1收集器在这个阶段实际 并没有额外的停顿。
·并发标记(Concurrent Marking): 从GC Root开始对堆中对象进行可达性分析,递归扫描整个堆 里的对象图,找出要回收的对象,这阶段耗时较长,但可与用户程序并发执行。当对象图扫描完成以 后,还要重新处理SATB记录下的在并发时有引用变动的对象。
·最终标记(Final Marking): 对用户线程做另一个短暂的暂停,用于处理并发阶段结束后仍遗留 下来的最后那少量的SATB记录。
·筛选回收(Live Data Counting and Evacuation): 负责更新Region的统计数据,对各个Region的回 收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region 构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧 Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行 完成的。从上述阶段的描述可以看出,G1收集器除了并发标记外,其余阶段也是要完全暂停用户线程的, 换言之,它并非纯粹地追求低延迟,官方给它设定的目标是在延迟可控的情况下获得尽可能高的吞吐 量,所以才能担当起“全功能收集器”的重任与期望
特点: 跟CMS相比他有什么特点呢?
G1会将内存块分成多个Region.Region就是一个区域。(我们之前在将CMS、Serial垃圾收集器的时候,我们都采用了新生代、老年代分区的收集方法,而在G1的时候,对新生代、老年代就不是特别敏感了、他将我们的每一块内存分成了Region,内存区域快会分成多个Region。我们垃圾回收时候维护的是Region里面的信息。)区分Region有什么用呢?Region里面有与之对应的RememberSet。当进⾏内存回收时,在 GC 根节点的枚举范围中加⼊ Remembered Set 即可保证不对全堆扫描也不会有遗漏 检查Reference引⽤的对象是否处于不同的Region。
优势:
1、空间整合: Region内基于“标记⼀整理”算法实现为主(避免空间垃圾碎片)和Region之间采⽤复制算法(Region之间存活对象比较少,复制算法效率高)实现的垃圾收集。
2、可预测的停顿:(原因:因为我们操作的是region,垃圾回收信息都存放在region里面,所以他是可以预测停顿时间的)这是 G1 相对于 CMS 的另⼀⼤优势,降低停顿时间是 G1 和 CMS 共同的关 注点,但 G1 除了追求低停顿外,还能建⽴可预测的停顿时间模型。
3、在 G1 之前的其他收集器进⾏收集的范围都是整个新⽣代或者⽼年代,⽽ G1 不再是这样。使⽤ G1 收集器时,Java 堆的内存布局就与其他收集器有很⼤差别,它将整个 Java 雄划分为多个⼤⼩相等的独⽴区域(Region),虽然还保留有新⽣代和⽼年代的概念,但新⽣代和⽼年
代不再是物理隔髙的了,它们都是⼀部分 Region(不需要连续)的集合。
4、G1 收集器之所以能建⽴可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆
中进⾏全区域的垃圾收集。G1 跟踪各个 Regions ⾥⾯的垃圾堆积的价值⼤⼩(回收所获得的空间⼤⼩以及回收所需时间的经验值),在后台维护⼀个优先列表,每次根据允许的收集时间,优先回收价值最⼤的 Region(这也就是 Garbage- Firsti 名称的来由)。这种使⽤Region 划分内存空间以及有优先级的区域回收⽅式,保证了 G1 收集器在有限的时间内可以获取尽可能⾼。
补充:
现在我们很多公司还是在使用CMS垃圾回收器,很少会用JDK1.9、JDK.10.所以我们还很少用G1。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。