深入理解JVM(chp3 垃圾收集与内存分配策略)

3.1 概述
Java堆和方法区,这两个部分内存的分配和回收是动态的。

【深入理解JVM(chp3 垃圾收集与内存分配策略)】3.2 判断一个对象是否可被回收

3.2.1 引用计数算法

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

主流jvm不采用引用计数法的原因:
单纯的引用计数很难解决对象之间相互循环引用的问题。

3.2.2 可达性分析算法

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

GC Roots:
1. 固定可作为GC Roots的对象:

2. “临时”可加入对象:

3.2.3引用:
1. 强引用:是指在代码之中普遍存在的引用赋值;只要强引用关系还存在,GC就不会
回收掉被引用的关系。
2. 软引用:描述一些还有用、但非必须的对象。只被软引用关联着的对象,在系统将
要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。SoftReference类实现软引用。
3. 弱引用:描述那些非必须对象,但是它的强度比软引用跟弱一些。被若引用关联的
对象只能生存到下一次垃圾收集发生为止。当GC开始工作,弱引用对象无论如
何都会被回收。WeakReference类来实现弱引用
4. 虚引用:为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回
收时受到一个系统通知。PhantomReference类来实现虚引用。

3.2.4 宣告对象“死亡”

宣告对象彻底死亡:在被标记为不可达后,要经历两次标记过程,

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

如果被判定为确有必要执行finallize()方法,那么该对象将会被放置在一个名为F-Queue的队列中。稍后收集器将对F-Queue中的对象进行第二次小规模的标记,如果对象这时候还没有逃脱,则它就真的要被回收了。
任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的fianlize()方法不会被在此执行。

3.3 垃圾收集算法

3.3.1 分代收集理论
回收类型的划分:
划分的依据是:根据垃圾收集器每次只回收其中某一个或者某些部分的区域。

部分收集(Partial GC):指目标不是完整收集整个Java堆的垃圾收集,
其中又分为:
■新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
■老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS收集器会有单独收集老年代的行为。另外请注意“Major GC”这个说法现在有点混淆,在不同资料上常有不同所指,读者需按上下文区分到底是指老年代的收集还是整堆收集。
■混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有G1收集器会有这种行为。
·整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
3.3.2 标记清除
首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象。

主要缺点:
1. 执行效率不稳定
2 . 内存空间的碎片化

3.3.3 标记-复制(回收新生代)
Appel式回收策略:
把新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾收集时,将Eden和Survivor中仍然存活的对象一次性复制到另外一块Survivor空间上,然后直接清理掉Eden和已用过的Survivor空间。清理掉Ed

Eden:From Survivor:To Survivor = 8:1:1

当Survivor空间不足以容纳一次Minor GC之后存活的对象时,就需要依赖其他内存区域(实际上大多就是老年代)进行分配担保(Handle Promotion)

HotSpot虚拟机中采用标记复制算法新生代收集器由:Serail、ParNew等

3.3.4 标记-整理 (回收老年代)

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

差异在于:前者是一种非移动式的回收算法,后者是移动式。

是否移动回收后的存活对象:
1. 移动存活对象:
由于在老年代中,每次回收都有大量对象存活,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动必须全程暂停用户应用程序才能进行,即 Stop The World 。
2. 不考虑移动和整理存活对象:
需要使用额外措施来解决弥散于堆中的存活对象导致的空间碎片化问题。

对比:
移动,有利于吞吐量。 Paralle Scavenge收集器关注于吞吐量
不移动,有利于延迟。CMS收集器关注于延迟。

3.5 垃圾收集器
深入理解JVM(chp3 垃圾收集与内存分配策略)
文章图片

有连线的表示可以搭配使用

3.5.1 Serial收集器

Serial收集器是一个单线程工作的收集器
深入理解JVM(chp3 垃圾收集与内存分配策略)
文章图片



单线程:
它在进行GC时,必须暂停其他所有工作线程,直到它收集结束。

Serial收集器,是HotSpot虚拟机运行在客户端模式下的默认新生代收集器。

优点:简单高效、内存消耗小
缺点:进行GC时,会Stop the world即暂停用户所有的进程。

3.5.6CMS收集器

在垃圾收集器的语境中的“并发”与“并行”:
1. 并行parallel:
描述的是多条垃圾收集器线程之间的关系,说明同一时间有多条这样的线程在协同工作,通常默认此时用户线程处于等待状态
2. 并发concurrent:
描述的时多条垃圾收集器线程与用户线程之间的关系,说明同一时间垃圾收集器线程与用户线程都在运行。

CMS(concurrent mark sweep)收集器:
以获取最短回收停顿时间为目标的收集器。
基于标记-清除算法实现的

CMS运作过程:
1. 初始标记
标记一下GC Roots能直接关联到的对象,需要Stop the World。

2. 并发标记
从GC Roots的直接关联对象开始遍历整个图的过程
此过程耗时较长但是不需要停顿用户线程,可以与GC线程一起并发执行。

3. 重新标记
修正并发标记阶段,因用户线程继续运作而导致标记产生变动的那一部分对象的标记记录。
此阶段耗时比初始标记阶段要长,但远小于并发标记阶段。
需要Stop the World。

4. 并发清除
清理删除掉标记阶段判断的已经死亡的对象。
由于不需要移动存活对象,故此阶段可以与用户线程同时并发。
深入理解JVM(chp3 垃圾收集与内存分配策略)
文章图片

在整个过程中耗时最长的并发标记和并发清理阶段,GC线程都可以与用户线程一起工作,因此可以从总体上说CMS的内存回收过程是与用户线程并发执行的。

CMS缺点:
1. CM收集器堆处理器资源非常敏感。
CMS默认启动的回收线程数是(处理器核心数量+3)/4,当处理器核心数量不足4个时,可能导致用户程序的执行速度忽然大幅下降。

2. CMS可能产生Full GC
由于CMS无法处理“浮动垃圾”,有可能出现“Concurrent Mode Failure”失败进而导致另一次完全“Stop the World”的Full GC的产生。
Full GC:清理整个堆空间,包括永久代和新生代。
浮动垃圾:
在CMS的并发标记和并发清理阶段,用户线程都还在继续运行的,程序在运行自然就会伴随有新的垃圾对象不断产生,但这一部分垃圾对象是出现在标记过程结束以后,CMS无法在当次收集中处理掉它们,只好留待下一次垃圾收集时再清理掉。这一部分垃圾就成为“浮动垃圾”。
3. CMS在收集结束时会有大量空间碎片产生
由于CMS是基于“标记-清除”算法实现的收集器,因此在收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代有很多剩余空间,但是无法找到足够大的连续空间来分配当前对象,而不得不提前触发一次Full GC。

3.5.7 G1收集器
G1开创了收集器面向局部收集的设计思路和基于Region的内存布局形式。
G1,从JDK9之后成为了服务端模式下的默认垃圾收集器。
G1出现的背景:
统一垃圾收集器接口:
将内存回收的“行为”与“实现”进行分离,便于移除与加入某一款收集器。
停顿时间模型:
能够支持指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集器上的时间大概率不超过N毫秒的目标。
Mixed GC模式:
G1之前的收集器的GC目标范围:
整个新生代(Minor GC)、整个老年代(Major GC), 整个Java堆(Full GC)。

G1采用Mixed GC模式,面向堆内存的任何部分来组成会收集进行回收,衡量的标准是哪块内存中存放的垃圾数量最多,回收收益最大。
G1的特征:
使用Region划分内存空间,以及具有优先级的区域回收方式。
G1使用Region中的一类特殊的Humongous区域,专门用来存储大对象。对于超过了整个Region容量的超级大对象,将会被存放在N个连续的Humongous Region中,Humongous Region作为老年代来看待。

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

G1回收的策略:
G1将Region作为单次回收的最小单元,即每次收集到的内存空间都是Region大小的整数倍。

G1会跟踪各个Region里面的垃圾堆积的“价值”大小,价值即回收所得的空间以及回收所需时间的经验值。

G1会在后台维护一个优先级列表,每次根据用户设定允许的收集停顿时间
( 参数-XX:MaxGCPauseMillis指定,默认200毫秒),优先处理回收价值收益最大的那些Region.

G1的运作过程:
深入理解JVM(chp3 垃圾收集与内存分配策略)
文章图片


1. 初始标记:
标记GC Root能直接关联到的对象,并且修改TAMS指针的值,让下一阶段用户线程并发运行时,能正确地在可用的Region中分配新对象。这个阶段需要停顿线程,此阶段是借用Minor GC的时候同步完成的。

2. 并发标记:
从GC Root开始堆堆中对象进行可达性分析,递归扫描整个堆中的对象图,找出要回收的对象。
3. 最终标记:
对用户线程做一个短暂的暂停,用于处理并发阶段结束后仍遗留下来的最后少量的SATB记录。
4.筛选回收(Live Data Counting and Evacuation):
负责更新Region的统计数据,对各个Region的回收价值和成本进行排序,根据用户所期望的停顿时间来制定回收计划,可以自由选择任意多个Region构成回收集,然后把决定回收的那一部分Region的存活对象复制到空的Region中,再清理掉整个旧Region的全部空间。这里的操作涉及存活对象的移动,是必须暂停用户线程,由多条收集器线程并行完成的。

G1除了并发标记阶段,其余阶段是要完全暂停用户线程的。

    推荐阅读