深入理解JVM学习笔记(4)-垃圾收集器

114 阅读11分钟

1.概述:GC,Java语言的伴生产物。

GC都是需要完成哪些事情呢?

1)哪些内存需要回收? 2)什么时候回收 ?3)如何回收?

我们带着这三个问题去一起探讨一下关于垃圾收集器

2.对象已死?

首先第一个问题,垃圾收集器在对堆进行回收前,第一件事就是要确定这些对象之中哪些还“存活”着,哪些已经“死去”。

2.1 引用计数法

在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。

缺陷:有些比如相互引用的无用对象没法被回收。

2.2 可达性分析

通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”(Reference Chain),如果某个对象到GC Roots间没有任何引用链相连,或者用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。

那哪些对象可以作为GC Roots呢?

·在虚拟机栈(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用的方法堆栈中使用到的参数、局部变量、临时变量等。

·在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。

·在方法区中常量引用的对象,譬如字符串常量池(String Table)里的引用。

·在本地方法栈中JNI(即通常所说的Native方法)引用的对象。

·Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPointExcepiton、OutOfMemoryError)等,还有系统类加载器。

·所有被同步锁(synchronized关键字)持有的对象。

·反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。

2.3 再谈引用

判定对象是否存活和“引用”离不开关系。引用分为强引用、软引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。

2.4 生存还是死亡?

可达性算法判定不可达的对象是非死不可的么?

要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记,随后进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。假如对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,那么虚拟机将这两种情况都视为“没有必要执行”。

对象如何自救?

如果对象要在finalize()中成功拯救自己——只要重新与引用链上的任何一个对象建立关联即可,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量,那在第二次标记时它将被移出“即将回收”的集合;如果对象这时候还没有逃脱,那基本上它就真的要被回收了。但是,它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序,如今已被官方明确声明为不推荐使用的语法。忘掉!忘掉!忘掉!

2.5 回收方法区

方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类型。

如何判定一个类型是否属于“不再被使用的类”?

·该类所有的实例都已经被回收

·加载该类的类加载器已经被回收

·该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

3.垃圾收集算法

3.1 分代收集理论

一般把Java堆分为新生代和老年代。

3.2 标记-清除算法

首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。

缺点:第一个是执行效率不稳定(随着对象数量增多效率降低)。第二个是内存空间的碎片化问题。

3.3 标记-复制算法

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

缺点:复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一点。

3.4 标记-整理算法

标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

4.经典垃圾收集器

如果说收集算法是内存回收的方法论,那垃圾收集器就是内存回收的实践者。

哪个收集器最好呢?

不存在“万能”的收集器,所以我们选择的只是对具体应用最合适的收集器。

4.1 Serial收集器

Serial收集器是最基础、历史最悠久的收集器,这个收集器是一个单线程工作的收集器,它的“单线程”更重要的是强调在它进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。

Serial收集器优于其他收集器的地方,那就是简单而高效,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。

4.2 ParNew收集器

ParNew收集器实质上是Serial收集器的多线程并行版本,除了同时使用多条线程进行垃圾收集之外,其余的行为包括Serial收集器可用的所有控制参数,收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一致。

其中有一个与功能、性能无关但其实很重要的原因是:除了Serial收集器外,目前只有它能与CMS收集器配合工作。

4.3 Paraller Scavenge收集器

Parallel Scavenge收集器也是一款新生代收集器,它同样是基于标记-复制算法实现的收集器,也是能够并行收集的多线程收集器,Parallel Scavenge收集器的特点是它的关注点是达到一个可控制的吞吐量。

停顿时间越短就越适合需要与用户交互或需要保证服务响应质量的程序,良好的响应速度能提升用户体验;而高吞吐量则可以最高效率地利用处理器资源,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的分析任务。

垃圾收集停顿时间缩短是以牺牲吞吐量和新生代空间为代价换取的。

由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称作“吞吐量优先收集器”。

4.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用标记-整理算法。

4.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现。

4.6 CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现的。

运作过程是怎样的呢?

1)初始标记 2)并发标记 3)重新标记 4)并发清除

初始标记、重新标记这两个步骤仍然要“Stop The World"(停止所有线程)。但初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快;并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行;而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间通常会比初始标记阶段稍长一些,但也远比并发标记阶段的时间短;最后是并发清除阶段,清理删除掉标记阶段判断的已经死亡的对象,由于不需要移动存活对象,所以这个阶段也是可以与用户线程同时并发的。

综上所述,从总体上讲,CMS收集器的内存回收过程是与用户线程一起并发执行的。

CMS优点:并发收集、低停顿。

CMS缺点:CMS收集器对处理器资源非常敏感,无法处理”浮动垃圾“,基于标记-清除算法,收集结束时会有大量空间碎片产生。

4.7 G1收集器

G1收集器开创了收集器面向局部收集的设计思路和Region的内存布局形式。一款面向服务服务端应用的垃圾收集器。

G1开创的基于Region的堆内存布局是它能够实现这个目标的关键。虽然G1也仍是遵循分代收集理论设计的,但其堆内存的布局与其他收集器有非常明显的差异:G1不再坚持固定大小以及固定数量的分代区域划分,而是把连续的Java堆划分为多个大小相等的独立区域(Region),每一个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。收集器能够对扮演不同角色的Region采用不同的策略去处理,这样无论是新创建的对象还是已经存活了一段时间、熬过多次收集的旧对象都能获取很好的收集效果。

每个Region的大小取值范围为1MB~32MB,且应为2的N次幂。

虽然G1仍然保留新生代和老年代的概念,但新生代和老年代不再是固定的了,它们都是一系列区域(不需要连续)的动态集合。

根据用户设定允许的收集停顿时间,优先处理回收价值收益最大的那些Region,这也就是“G1”名字的由来。这种使用Region划分内存空间,以及具有优先级的区域回收方式,保证了G1收集器在有限的时间内获取尽可能高的收集效率。

Region里面存在的跨Region引用对象如何解决?每个Region都维护有自己的记忆集。

G1收集器的四个步骤:初始标记,并发标记,最终标记,筛选回收(对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划)

由用户指定期望的停顿时间是G1收集器很强大的一个功能,设置不同的期望停顿时间,可使得G1在不同应用场景中取得关注吞吐量和关注延迟之间的最佳平衡。不过,这里设置的“期望值”必须是符合实际的,不能异想天开,毕竟G1是要冻结用户线程来复制对象的,这个停顿时间再怎么低也得有个限度。

目前在小内存应用上CMS的表现大概率仍然要会优于G1,而在大内存应用上G1则大多能发挥其优势,这个优劣势的Java堆容量平衡点通常在6GB至8GB之间。

5.低延迟垃圾收集器

衡量垃圾收集器的三项最重要的指标是:内存占用、吞吐量和延迟。

5.1 ZGC收集器

目标时在尽可能对吞吐量影响不太大的前提下,实现在任意堆内存大小下都可以把垃圾收集的停顿时间限制在十毫秒以内的低延迟。

ZGC收集器是一款基于Region内存布局的,(暂时)不设分代的,使用了读屏障、染色指针和内存多重映射等技术来实现可并发的标记-整理算法的,以低延迟为首要目标的一款垃圾收集器。

ZGC又是怎么运作的呢?

并发标记,并发预备重分配,并发重分配,并发重映射。

6.选择合适的垃圾收集器

如何选择一款合适自己应用的垃圾收集器呢?

三个因素:1)应用程序的主要关注点是什么?2)运行应用的基础设施如何?3)使用JDK的发行商是什么?