深入理解java虚拟机(二)jvm 垃圾收集器

可达性分析

使用可达性分析来判断对象的存活,基本思路就是通过一系列成为“GC Roots”的对象作为起点,从这些起点开始向下搜索,搜索到所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的

比如图中Object5,6,7,虽然之间有关系,但是GC Roots不可达,所以他们将会被判断为可回收的对象。

可作为GC Roots的对象有:虚拟机栈中引用的对象,方法区中类静态属性引用的对象,方法区中常量引用的对象,本地方法栈中JNI引用的对象

可以理解为:

(1)首先第一种是虚拟机栈中的引用的对象,我们在程序中正常创建一个对象,对象会在堆上开辟一块空间,同时会将这块空间的地址作为引用保存到虚拟机栈中,如果对象生命周期结束了,那么引用就会从虚拟机栈中出栈,因此如果在虚拟机栈中有引用,就说明这个对象还是有用的,这种情况是最常见的。

(2)第二种是我们在类中定义了全局的静态的对象,也就是使用了static关键字,由于虚拟机栈是线程私有的,所以这种对象的引用会保存在共有的方法区中,显然将方法区中的静态引用作为GC Roots是必须的。

(3)第三种便是常量引用,就是使用了static final关键字,由于这种引用初始化之后不会修改,所以方法区常量池里的引用的对象也应该作为GC Roots。最后一种是在使用JNI技术时,有时候单纯的Java代码并不能满足我们的需求,我们可能需要在Java中调用C或C++的代码,因此会使用native方法,JVM内存中专门有一块本地方法栈,用来保存这些对象的引用,所以本地方法栈中引用的对象也会被作为GC Roots。

JVM之判断对象是否存活(引用计数算法、可达性分析算法,最终判定)

finalize()方法最终判定对象是否存活:

    即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。
    标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。
  1).第一次标记并进行一次筛选。
    筛选的条件是此对象是否有必要执行finalize()方法。
    当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

  2).第二次标记
    如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。
    Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己----只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。
流程图如下:

垃圾收集算法:

标记-清除算法

分为“标记”和“清除”两个阶段,标记阶段标记出需要回收的对象,在标记完成后统一回收所有被标记的对象。

不足:效率:这两个过程的效率都不高,另一个是空间:会产生大量的碎片

复制算法:

将可用的内存分为大小相等的两块,每次只使用其中一块,当一块用完,将还存活的对象复制到另一块,然后将这一块直接清理掉。简单高效,但是内存缩小了一半。

Hotspot虚拟机认为,大部分对象都是朝生夕死的,所以将内存分为三块,一块较大的Eden区,和两块较小的Survivor区,每次使用Eden和其中一个Survivor区,Eden用来存放新建对象,Survivor存放上次minor GC留下的对象,当回收时,将还存活着的对象一次性复制到另一个Survivor区,Hotspot默认Eden和Servivor大小比例为8:1.

当Survivor区不够用时,需要依赖老年代来进行分配担保。就是指对象直接进入老年代。

标记-整理算法

和标记-清除一样,在清除之后对留下的对象进行整理,

分带收集算法:

将java堆分为新生代和老年代,新生代采用复制算法,老年代采用标记-整理算法。

Hotspot算法的实现:

枚举根节点

从可达性分析中从GC Roots节点找引用为例,可作为GC Roots的节点主要是全局性的引用与执行上下文中,如果要逐个检查引用,必然消耗时间。 
另外可达性分析对执行时间的敏感还体现在GC停顿上,因为这项分析工作必须在一个能确保一致性的快照中进行——这里的“一致性”的意思是指整个分析期间整个系统执行系统看起来就行被冻结在某个时间点,不可以出现分析过程中对象引用关系还在不断变化的情况,该点不满足的话分析结果的准确性就无法得到保证。这点是导致GC进行时必须暂停所有Java执行线程的其中一个重要原因。 
由于目前主流的Java虚拟机都是准确式GC,做一档执行系统停顿下来之后,并不需要一个不漏的检查执行上下文和全局的引用位置,虚拟机应当有办法得知哪些地方存放的是对象的引用。在HotSpot的实现中,是使用一组OopMap的数据结构来达到这个目的的。

安全点 


在OopMap的协助下,HotSpot可以快速且准确的完成GC Roots的枚举,但可能导致引用关系变化的指令非常多,如果为每一条指令都生成OopMap,那将会需要大量的额外空间,这样GC的空间成本会变的很高。 
实际上,HotSpot也的确没有为每条指令生成OopMap,只是在特定的位置记录了这些信息,这些位置被称为安全点(SafePoint)。SafePoint的选定既不能太少,以致让GC等待时间太久,也不能设置的太频繁以至于增大运行时负荷。所以安全点的设置是以让程序“是否具有让程序长时间执行的特征”为标准选定的。“长时间执行”最明显的特征就是指令序列的复用,例如方法调用、循环跳转、异常跳转等,所以具有这些功能的指令才会产生SafePoint。 
对于SafePoint,另一个问题是如何在GC发生时让所有线程都跑到安全点在停顿下来。这里有两种方案:抢先式中断和主动式中断。抢先式中断不需要线程代码主动配合,当GC发生时,首先把所有线程中断,如果发现线程中断的地方不在安全点上,就恢复线程,让他跑到安全点上。现在几乎没有虚拟机实现采用抢先式中断来暂停线程来响应GC。 
主动式中断的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的另外再加上创建对象需要分配的内存的地方。

安全区域

程序如果不执行呢?所谓的程序不执行就是没有分配cpu时间,典型的例子就是线程处于sleep状态或者blocked状态,这时候线程无法响应jvm中断请求,走到安全的地方中断挂起,jvm显然不太可能等待线程重新分配cpu时间,对于这种情况,我们使用安全区域来解决。 
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域的任何地方开始GC都是安全的,我们可以把安全区域看做是扩展了的安全点。 
当线程执行到安全区域中的代码时,首先标识自己已经进入了安全区,那样当在这段时间里,JVM要发起GC时,就不用管标识自己为安全区域状态的线程了。当线程要离开安全区域时,他要检查系统是否完成了根节点枚举,如果完成了,那线程就继续执行,否则他就必须等待,直到收到可以安全离开安全区域的信号为止。
 

垃圾收集器

Serial收集器

Serial收集器是最基本、发展历史最悠久的收集器。是单线程的收集器。它在进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集完成。

 

Serial收集器依然是虚拟机运行在Client模式下默认新生代收集器,对于运行在Client模式下的虚拟机来说是一个很好的选择。

ParNew收集器

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

 

ParNew收集器是许多运行在Server模式下的虚拟机中首选新生代收集器,其中有一个与性能无关但很重要的原因是,除Serial收集器之外,目前只有ParNew它能与CMS收集器配合工作。

Parallel Scavenge(并行回收)收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的收集器,又是并行的多线程收集器

该收集器的目标是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Serial Old 收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记整理算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。

Serial Old收集器的工作工程

 

Parallel Old 收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在1.6中才开始提供。

 

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务器的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求

CMS收集器是基于“标记-清除”算法实现的。它的运作过程相对前面几种收集器来说更复杂一些,整个过程分为4个步骤:

(1)初始标记

(2)并发标记

(3)重新标记

(4)并发清除

其中,初始标记、重新标记这两个步骤仍然需要“Stop The World”.初始标记仅仅只标记一下GC Roots能直接关联到的对象,快读很快,并发标记阶段就是进行GC RootsTracing的过程,而重新标记则是为了修正在并发标记期间因用户程序运作而导致标记产生变动的记录。远比并发表及时间短。

 

CMS收集器主要优点:并发收集,低停顿。

CMS三个明显的缺点:

(1)CMS收集器对CPU资源非常敏感。CPU个数少于4个时,CMS对于用户程序的影响就可能变得很大,为了应付这种情况,虚拟机提供了一种称为“增量式并发收集器”的CMS收集器变种。所做的事情和单CPU年代PC机操作系统使用抢占式来模拟多任务机制的思想

(2)CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。在JDK1.5的默认设置下,CMS收集器当老年代使用了68%的空间后就会被激活,这是一个偏保守的设置,如果在应用中蓝年代增长不是太快,可以适当调高参数-XX:CMSInitiatingOccupancyFraction的值来提高触发百分比,以便降低内存回收次数从而获取更好的性能,在JDK1.6中,CMS收集器的启动阀值已经提升至92%。

(3)CMS是基于“标记-清除”算法实现的收集器,手机结束时会有大量空间碎片产生。空间碎片过多,可能会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发FullGC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开关参数(默认就是开启的),用于在CMS收集器顶不住要进行FullGC时开启内存碎片合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但停顿时间变长了。虚拟机设计者还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标识每次进入Full GC时都进行碎片整理)

G1收集器

G1收集器的优势:

(1)并行与并发

(2)分代收集

(3)空间整理 (标记整理算法,复制算法)

(4)可预测的停顿(G1处处理追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经实现Java(RTSJ)的来及收集器的特征)

使用G1收集器时,Java堆的内存布局是整个规划为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region的集合。

G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在真个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获取的空间大小以及回收所需要的时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的又来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽量可能高的灰机效率

G1 内存“化整为零”的思路

在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会遗漏。

如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为一下步骤:

(1)初始标记

(2)并发标记

(3)最终标记

(4)筛选回收

最终标记需要停顿,大大高收集效率。

 

如果你的应用追求低停顿,G1可以作为一个可以尝试的选择,如果追求吞吐量,那G1并不会带来好处

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

G1:https://www.jianshu.com/p/548c67aa1bc0

内存分配策略

对象在新生代Eden区分配,当Eden区空间不够时,jvm将进行一次minor GC,如果GC发现放不进Survivor区,就利用担保,将对象放到老年代,新对象放进Eden区。

大对象直接进入老年代,大对象是指:需要大量连续内存空间的java的对象。大对象是不好的,因为大对象容易导致内存还有不少空间的时候就提前触发垃圾收集以获取足够的连续空间来放大对象。

-XX:PretenureSizeThreshold 设置参数,令大于这个值的对象直接进入老年代。

长期存活的对象进入老年代。每个对象有一个对象年龄计数器,如果对象经过一个minor GC并存活,则年龄+1,当到达一个度(默认15)将会晋升到老年代中。

动态年龄对象判定:如果在Survivor空间种相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无序等到MaxTenuringThreshold中要求的年龄。

空间分配danbao:只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行minor GC,否则进行Full GC

总结:

新建对象进入Eden区,Survivor区存放存活的对象,minor GC后,新生代存活的对象存放在另一个Survivor中,放不下就放在老年代。

到达一定年龄(15)就进入老年代

大对象直接进入老年代

Eden区空间不够存放新对象,就会进行minor GC

老年代空间不够,或者不够存放大对象,会进行Full GC

参考:

https://blog.csdn.net/luzhensmart/article/details/81431212

https://blog.csdn.net/lin_wj1995/article/details/79972039

https://blog.csdn.net/sch20095100/article/details/46591757

https://www.cnblogs.com/chengxuyuanzhilu/p/7088316.html

猜你喜欢

转载自blog.csdn.net/zyh568879280/article/details/88059482