JavaTutorial12
接下里我们将要讲的内容是JVM的垃圾回收机制。整个讲解的流程是:引用→可达性分析→垃圾回收算法→垃圾收集器。引用类型为可达性分析提供了基础信息 ,即哪些对象通过什么方式被引用着。可达性分析算法基于引用关系,确定哪些对象需要被回收。这一步骤回答了”哪些对象是垃圾”的问题,为下一步实际回收做准备。接着,垃圾回收算法决定如何高效地清理和整理内存空间,包括标记-清除,整理,复制,分代算法等等。最后,我们会讲解不同收集器如何运用算法实现垃圾回收,以及内存分配和回收策略的总结。
Java引用
直接引用
无论是对象的访问定位,还是对象是否可以被回收的判断等,都离不开引用。而Java中虚拟机HotSpot通过直接引用来访问Java对象的。直接引用就是说指针是直接指向对象实例的,如果想要获取到对象的类型数据信息,则需要再调用对象里维护的类型数据指针。
JVM 只规定了reference 类型是一个指向对象的引用,并没有规定这个引用怎么去实现。所以引用类型根据不同 JVM 厂商的实现不同会有差异,主要有两种:
1.直接引用
2.句柄而我们常分析的都是 HotSpot 虚拟机,其引用类型就是直接引用,所以这里理解直接引用就行。
以下是直接引用的示意图:

因此, jvm 的引用类型每个厂商实现不一样,hotspot是通过直接引用访问对象的;引用类型以强弱之分则分为强引用,弱引用,软引用,虚引用。
引用类型
四个引用类型由强到弱的顺序如下:
强引用:最常见的引用类型,只要强引用存在,对象就不会被回收;
软引用:内存不足时才会被回收;(用途:对象缓存)
弱引用:下一次垃圾回收时会被回收,活不过下一次gc(garbage collection),用途也是对象缓存。
虚引用:主要用于跟踪对象被回收的状态,最弱的引用类型。
这些引用方式为可达性分析中引用链的判断方式打下了基础。
可达性分析
通过 一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连, 即GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

对象Object6 ,Object7,Object8,0bject9虽然互有关联,但是它们到GC Roots是不可达的, 因此它们将会被判定为可回收的对象。
那么,在gc roots中有哪些常驻嘉宾呢?
GC Roots对象

总结一下,大概是分为三类:
1.程序运行时的引用,包括本地方法栈中native方法的引用,方法区中静态属性和常量引用的对象,还有java栈中使用到的各种参数和变量;
2.jvm内部引用,包括基本数据类型的Class对象,异常对象,本地代码缓存等;
3.特殊机制:加锁持有对象。
ok,讲完谁要被回收谁不要被回收,接下来讲怎么回收。
垃圾收集算法
以下:
1.是否存活是指可达 or 不可达;
2.清理并不是将内存空间字节清零,而是记录这段内存的起始地址,下次分配内存的时候,会直接覆盖这段内存。
标记-清除
首先找出所有对象,将存活的对象进行标记,然后清理掉未被标记的对象,结束。

标记-整理
首先找出所有对象,将存活的对象进行标记,然后将存活对象整理到一端,然后把其他内存区域直接清理掉。

复制
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。

HotSpot 虚拟机的将新生代内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。
Eden和Survivor属于新生代的区域划分:
Eden:大部分新对象分配的时候都在这个区域分配。
Survivor: Eden 区中的对象,至少存活一次 gc 之后,就会进入survivor A区,再经历一次 gc那就拷贝到 survivor B ;后续就 AB 来回复制,直到对象存活次数达到晋升老年代的条件,就从 survivor 中移出,进入老年代。
分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。一般将堆分为新生代和老年代,新生代每次垃圾收集时都发现有大批对象死去,而每次回收后存活的少量对象,将会逐步晋升到老年代中存放。
新生代:
·绝大多数对象都是朝生夕灭的。
·复制算法。
老年代:
·“大多数”是熬过越多次垃圾收集过程的对象。
·标记-清除 或者 标记-整理 算法。
事实上,所谓的“大对象”在加入堆之后也会被直接放入老年代,原因是之前讲到的,如果使大对象在Survivor之间不断流转会消耗太多内存,导致其浪费。
垃圾收集器
HotSpot 虚拟机中的7个垃圾收集器,图中有连线的说明是可以一起搭配使用的。目前java的服务器端已经全部用G1收集器实现,此处重点掌握CMS和G1收集器的原理即可。

Serial & Serial Old
Serial收集器是在进行垃圾收集时,必须暂停其他所有工作线程(Stop The World)。Stop The World听起来很牛,其实并不是啥好事,因为它会导致用户线程停止工作,所以有些真实应用来说是无法接受的。

·Serial 翻译为串行,也就是说它以串行的方式执行
·Serial 是新生代的垃圾收集器
·算法:复制算法
·HotSpot虚拟机运行在客户端模式下的默认新生代收集器
Serial Old是 Serial 收集器的老年代版本,使用标记-整理算法,主要意义也是供客户端模式下的HotSpot虚拟机使用。
·老年代收集器
·算法:标记-整理算法
·gc时暂停所有用户线程。
·主要作为客户端模式下的HotSpot虚拟机使用,另外也作为CMS收集器并发收集发生Concurrent Mode Failure时的后备预案使用。
为什么serial用的是复制算法,而serial old用的是标记-整理算法?
Serial(年轻代收集器):
- 非常适合年轻代,因为年轻代中的大多数对象生命周期很短
- 当存活对象相对较少时,将它们复制到新空间效率较高
- 这种方法通过创建全新的、紧凑的空间来避免内存碎片化
Serial Old(老年代收集器):
- 更适合老年代,因为老年代中的对象大多是长寿命的
- 对于存活率高的区域来说更节省内存(复制算法在这种情况下会浪费资源)
- 整理过程对于避免长期内存区域的碎片化是必要的
- 由于老年代中的对象往往会长期存在,原地重新排列它们比复制所有对象更有效率
ParNew
ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外其余的行为包括控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致,在实现上这两种收集器也共用了相当多的代码。

总结:
·垃圾收集时多线程并行
·ParNew是新生代的垃圾收集器
·算法:复制算法
·是 Server 模式下的虚拟机首选新生代收集器主要是因为除了 Serial 收集器只有它能与 CMS收集器(老年代)配合工作。
·使用 -XX:ParallelGCThreads 参数来设置GC线程数。
Parallel Scavenge & Parallel Old
该收集器与ParNew类似,都是多线程的垃圾收集器。其它收集器关注点可能是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
吞吐量 =运行用户代码的时间/(运行用户代码的时间+运行垃圾收集的时间)。如果你仍然觉得这两个指标没什么区别,看到CMS你就懂了。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
通样是新生代使用复制算法的多线程垃圾收集器,Parallel Scavenge 除了能精准控制吞吐量以外,还拥有GC 自适应的调节策略开关。
GC 自适应的调节策略开关:开启开关,就不需要手动指定新生代的大小(-Xmn)、Eden 和Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
XX:+UseAdaptiveSizePolicy。
Parallel Old是Parallel Scavenge收集器的老年代版本,多线程并行收集。目前只能与新生代的ParallelScavenge收集器搭配使用,可以说Parallel Old就是为Parallel Scavenge而生的。在这之前ParallelScavenge收集器只能与老年代的Serial 0ld进行搭配,但是一个多线程,一个单线程,导致吞吐量并没有充分的提升,直到ParallelOld收集器出现。
总结:
·Parallel Old为Parallel Scavenge而生,只能搭配Parallel Scavenge。
·Parallel Old采用多线程。
·算法:标记-整理
·在注重吞吐量以及处理器资源较为稀缺的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器这个组合
·JDK 6时才开始提供
这里的CPU稀缺,不是说 CPU 本身有多少,而是关注的 CPU 有多少比例用于执行用户,这个描述跟看重吞吐量其实差不多是一类意思。(根据公式,看重吞吐量,意味着要减少 GC 整体指行时间,即减少GC 的 CPU 消耗,更多的将 CPU资源向用户代码倾斜,因为 CPU很稀缺,你用户体感差一点就差一点呗)
相对的是不看中吞吐量而更看中用户停顿时间(CMS 这种)。这种虽然用户体感更好,但是这种方式 CPU 用于执行垃圾回收的时间更长,也就是 CPU 浪费在GC 上的资源更多。(如果你觉得 CPU 资源不稀缺,当然可以这么做)
CMS
CMS(Concurrent Mark Sweep)是一款追求最短停顿时间的收集器。

分为以下四个流程:
初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要“Stop The World”。
并发标记: 进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要”Stop The World”,可以与用户线程并发。
重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”。比初始标记时间长,比并发标记时间短。
并发清除: 清除掉判定为死亡的对象,不需要“Stop The World”,可以与用户线程并发。
优点: 并发收集、低停顿。
问题总结:
**吞吐量低:**CMS追求用户线程停顿时间少,停顿时间少就只能与用户线程并发执行部分阶段,导致整个垃圾回收需要执行的整体时间会更长(停顿之后专心垃圾收集肯定是最快的),所以吞吐量会降低。
“浮动垃圾”问题:“并发清除”阶段,由于gc线程是与用户线程并发的,这个期间用户还会产生新的垃圾,所以一般会预留出一部分内存,不能等到老年代快满的时候才去收集,如果预留的内存不足以存放这部分浮动垃圾的话,就会出现Concurrent Mode Failure。 前面讲过,出现这个错误之后,虚拟机将临时启用 Serial Old 来替代 CMS
标记-清除算法:因为没有整理的过程,所以垃圾收集完之后,会有很多空间碎片,导致需要分配大块连续内存的时候,空间不足。
总而言之,这个老年代收集器用吞吐量和垃圾回收的处理时间换取了用户体验。
G1
Garbage First(简称G1)收集器,意为垃圾优先,哪一块的垃圾最多就优先清理它。从名字就可以看出G1的一个特性,那就是G1能对不同区块的内存进行回收价值和成本排序,即价值越高成本越低的区块会被先回收。另外我们还能为G1设定性能指标,例如任意1秒内暂停时间不超过 10 毫秒,G1会尽力去达成这个目标。
G1开创了收集器面向局部收集的设计思路和基于Reqion的内存布局形式。JDK8Update 40这个版本以后的G1收集器被Oracle官方称为“全功能的垃圾收集器”。JDK 9发布之 日,G1宣告取代ParallelScavenge加Parallel Old组合,成为服务端模式下的默认垃圾收集器。
G1依然还是采用了分代设计,但是之前的一些垃圾收集器有很大差别,不会在为新生代,老年代等分配规定大小的区域,而是将整个堆分成一个个大小固定的Region区域,每一个Region都可以是新生代,老年代,Eden空间,Survivor空间的角色。所以Region成为了垃圾收集的最小单元,每一次回收都会是Region的整数倍大小。

Region特性和关键问题总结:
所有的 Eden 区和 Survivor 区合起来就是年轻代,所有的 Old 区拼在一起那就是老年代。
G1每次收集时只会收集部分region,每次收集时,会先估算每个小块存活对象的总数,回收时垃圾最多的小块会被优先回收。
Region里面存在的跨Region引用对象如何解决?
使用记忆集(看1.2.3)避免全堆作为GC Roots扫描,G1它的每个Region都维护有自己的记忆集,这些记忆集会记录下别的Region 指向自己的指针,并标记这些指针分别在哪些卡页的范围之内。
- 在并发标记阶段如何保证收集线程与用户线程互不干扰地运行?
回收过程中改变对象引用关系:必须保证其不能打破原本的对象图结构,导致标记结果出现错。误。G1 收集器则是通过原始快照(SATB)算法来实现的。
回收过程中新创建对象:G1为每一个Region设计了两个名为TAMS(Top at Mark Start)的指针把Region中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上。
记忆集(Remembered Sets)的作用是:
- 记录从外部区域指向本区域内所有对象的引用关系
- 解决在部分区域垃圾回收时如何找到跨区域引用的问题
- 避免全堆扫描带来的性能问题
当需要对某个区域进行垃圾回收时,如果采用全堆扫描来寻找引用关系,性能会非常差。记忆集通过维护一个数据结构,记录了所有从外部指向当前回收区域的引用。这样在进行可达性分析时,只需查询记忆集就能知道哪些外部对象引用了当前回收区域的对象,而不需要扫描整个堆。
G1回收的四个步骤:

·初始标记:仅仅只是标记一下GC Roots能直接关联到的对象。(需要停顿).
·并发标记:从GC Roots开始进行可达性分析,完成对象图的扫描,判断存活对象和可回收对象。做后再处理下SATB记录的有引用变动的对象(无需停顿)
·最终标记:对用户线程做另一个短暂的停顿,用于处理并发阶段结束后仍遗留下来的最后那少量的SATB记录。(需要停顿)
·筛选回收:统计各个Region的回收价值和成本并进行排序,根据用户所期望的停顿时间来制定回收计划,筛选任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个1 Region的全部空间。(需要停顿)
对于每一个region来说,g1采用的是复制算法。
内存分配和回收总结
首先需要先介绍,什么是minor和full gc?
1 | Minor GC: |
