JVM学习笔记(3)-垃圾收集

1.GC回收的是方法区和堆区的内存空间

jvm把内存区域分为5个部分,线程共有的方法区和堆,线程私有的虚拟机栈、本地方法栈、程序计数器

线程私有的内存空间在线程消亡的时候内存自动回收,垃圾回收(GC:Garbage Collection)的主要是指线程共有的方法区和堆区部分

GC在回收前首先要判断对象是否存活


2.判断对象是否存活

(1)引用计数法:

一种古老的判断对象是否存活的方法,给对象添加一个引用计数器,每当有一个地方引用该对象时,计数器值加1;当引用失效时计数器值减1。当值为0是,表示该对象是不能被使用的。

引用计数法不能解决对象间的循环引用问题,在目前的java虚拟机中没有采用该算法


(2)可达性分析算法:

通过一系列的根节点(GC Roots)对象作为起始点,从这些节点向下搜索,搜索走过的路径称为“引用链”;当一个对象没有任何引用链相连是表明对象不可达,此时对象不可用,是垃圾回收的目标。

可能对象间存在相互引用,但是没有与GC Roots有任何引用链关联,这部分对象也是不可达的可以被回收的


GC Roots

a.虚拟机栈中引用的对象

b.方法区中类静态属性和常量引用的对象(全局对象)

c.本地方法栈中引用的对象


3.引用的类型

在可达性分析算法中,确定一个对象是否存活,主要是看该对象是否与GC Roots根节点是否有引用链可达,这种“引用”有四种形式

(1)强引用(String Reference):代码中普遍存在的,例如:Object o=new Object();这种对象是不会被垃圾收集器收集的

(2)软引用(Soft Reference):描述有用但非必须的对象,对于软引用关联的对象,在系统将要发生内存溢出时,将会把这些对象列入回收范围进行第二次回收,如果这次回收还没有足够的内存将抛出OOM;SoftReference类关联软引用

(3)弱引用(Weak Reference):被弱引用关联的对象只能存活到下一次垃圾收集之。当垃圾收集发生时,无论当前内存空间是否足够,都会回收该部分的内存 ,WeakReferece对象关联弱引用

(4)虚引用(Phantom Reference):一个对象是否有虚引用关联不会对其生存时间构成影响,也无法通过虚引用获得对象的实例。该引用类型的唯一目的就是,这个对象被垃圾回收时收到一个系统通知,PhantomReference关联虚引用


4.不可达对象不一定会被回收

对于没有和GC Roots相关联的不可达对象,是垃圾回收的潜在目标,但是不一定会被回收

对象在垃圾回收时会有两次标记的过程,经可达性分析对象没有与GC Roots对象相关联的引用链,将第一次被标记并且进行一次筛选,筛选的条件该兑现是否有必要要执行finalize方法。当对象没有覆盖finalize方法或该方法已经被调用过(一个对象的finalize方法只会被执行一次),这种情况是没必要执行的,可以被回收。当对象有必要要执行finalize方法(对象覆盖了finalize方法,并且是第一次执行该方法),那么该对象会放在F-Queue队列中,并在稍后由虚拟机自动建立的一个低优先级的垃圾回收线程Finalizer去执行回收(会执行finalize方法,单不会承诺等待执行结束,也即是该方法的执行是随机的);在F-Queue队列中的对象将会进行第二次标记,如果此时还没有与GC Roots节点有引用链相关联则真正被回收,如果此时有引用链相关联则可逃出垃圾回收,也即是finalize方法是对象最后一次逃出垃圾回收的机会


5.回收方法区

方法区也称永久代,主要存放的类信息和一些常量池

这部分区域回收的效率不是很高,单也有回收的必要

对常量池中常量的回收:只要没有引用指向该常量则可以回收

对常量池中类的回收:该类的所有实例已回收;加载该类的ClassLoader已回收;该类对应的java.lang.Class没有任何地方有引用


6.垃圾收集算法

(1)标记-清除算法:

原理:用可达性分析算法标记出所有需要回收的对象,在标记完成后进行统一回收

不足:a.效率不高,标记和清除两个过程的效率都不高;b.空间问题,标记清除会产生大量的内存碎片,在以后要分配大内存对象是可能存在内存不够的情况

(2)复制算法:

是为了解决效率不高的问题而产生的

原理:将内存分为两块大小一样的两块,每次只使用其中的一块,当每次回收的时候讲其中的一块内存上存活的对象复制到另一块内存上,然后把已使用过的那一块全部清除掉。这样使得每次都对整个半区进行回收。

具体实现:新生代内存划分为一块较大的Eden区和两块较小的From Survivor区、To Survivor区;其比例默认是8:1:1,这样新生代可用的内存就是80%+10%,只有10%的内存被浪费掉。内存回收时每次使用Eden和一块Survivor区,回收时将Eden和Survivor中存活的对象一次性复制到另一块Survivor中,然后清理掉Eden和刚才用过的Survivor。若是Survivor空间不足则对象通过分配担保机制直接进入老年代

(3)标记-整理算法:

解决空间碎片的问题,同时在对象存活较多的情况下采用复制算法,效率不高

原理:通过可达性分析算法标记需要回收的对象,最后不是进行清除而是进行整理,整理过程是让所有存活的对象向内存的一端移动,然后直接清理掉边界以外的内存

(4)分代手机算法:

根据对象的存活周期将对象分为几块,新生代和老年代;在新生代中由于大多数对象都是朝生夕灭,采用复制算法;对于老年代由于对象的存活率较高采用标记清除或者标记整理算法


7.Stop The World现象

在枚举GC Roots的时候,Java程序必须暂停所有执行线程,这种现象称为Stop The World

在此过程中Java线程会暂停,但是native代码可以执行,但是不能与jvm进行交互


8.垃圾收集器

jdk1.7包含的垃圾收集器组合使用如图所示:



(1)Serial收集器:

Serial单线程收集器,在进行垃圾收集时必须暂停所有的工作线程,知道垃圾收集完成

(2)ParNew收集器:

相当于Serial的多线程版本,可以使用多条线程进行垃圾收集,其余和Serial一样

(3)Parallel Scavenge收集器:

采用复制算法的并行多线程收集器,以吞吐量优先

(4)Serial Old收集器

是Serial收集器的老年代版本,采用标记整理算法

(5)Parallel Old收集器:

是Parallel Scavenge收集器的老年代版本,使用标记整理算法

(6)CMS收集器:

CMS:Concurrent Mark Sweep 是一种以获取最短回收停顿时间为目的收集器,采用标记清除算法实现。

整个过程包含四个步骤:

1)初始标记:

初始标记只是标记一下GC Roots能直接关联的对象,速度很快,有Stop The World现象

2)并发标记:

并发标记就是根据初始标记的直接关联的对象进行寻迹,称为GC Roots Tracing的过程。无Stop The World现象

3)重新标记:

重新标记是修正在并发期间因用户程序继续进行导致标记产生变动的那一部分对象的标记记录;这一阶段比初始标记的时间要长,但是远比并发标记过程要短

有Stop The World现象

4)并发清除:

对并发标记的对象和重新标记的对象进行并发清除

过程示意图如下:


在初始标记和重新标记的过程中有Stop The World现象;耗时最长的阶段在并发标记和并发清除阶段,但是在并发标记和并发清除收集器线程都可以和用户线程一起工作;

从总体来说CMS收集器内存回收过程是与用户线程一起并发执行的。


9.CMS收集器优缺点

CMS收集器是一种优秀的并发收集器,但是也有一定的不足:

(1)CMS收集器对CPU资源敏感,在并发阶段虽然不会导致用户线程停顿,但是会因为占用一部分线程(或者说是CPU资源)而导致引用程序变慢,总吞吐量会降低,CMS默认开启的并发回收线程数是(CPU数量+3)/4,

(2)CMS无法处理浮动垃圾,可能出现Concurrent Mode Failure而导致领一次Full GC的情况,由于在并发清理阶段用户线程还在执行,程序必然有新的垃圾不断的产生,这一过程出现在并发标记之后,CMS无法再当次垃圾收集时处理他们,只能在下一次垃圾回收时处理,这部分垃圾称为浮动垃圾。

由于在是并发收集器,在垃圾回收的时候用户线程还要运行,因此虚拟机还需预留足够的内存空间给用户线程,不能像其他收集器那样等老年代几乎填满了再去进行垃圾收集,要预留部分空间提供并发收集时程序运作使用。在JDK1.6中CMS的启动阈值已经提高到92%,要是CMS预留的内存空间无法满足程序需要就会触发“”Concurrent Mode Failur”e失败,这是虚拟机将启动后备预案,临时启用Serial Old收集器进行老年代的收集,这样停顿时间就很长了,所以-XX:CMSInitiatingOccupancyFraction设置太高很容易导致大量“Concurrent Mode Failure”失败,性能反而会降低

(3)CMS是一种标记清除算法实现的,在垃圾收集结束时会有大量空间碎片产生,空间碎片过多会给大对象分配带来麻烦;往往会出现老年代还有很大的空间剩余,但是无法找到足够的连续空间来分配给大对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了参数 -XX:+UseCMSCompactAtFullCollection开发参数(默认是开启的),在收集器顶不住要进行FullGC的时候开启内存碎片的合并整理过程,这个过程是无法并发,空间碎片问题解决了但是停顿时间变长了。虚拟机还提供了另一个参数 

-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后来一次带压缩的(默认值为0,表示每次Full GC对进行整理)


10.G1收集器

G1:Garbage First收集器,是一款面向服务端引用的垃圾收集器

特点:

(1)并发与并行:充分利用多核多CPU硬件优势,来缩短Stop The World停顿时间,部分其他收集器需要停顿Java线程执行GC动作,G1可以以并发的形式让java程序任然能够执行

(2)分代收集:可以不需要其他收集器配合就能独立管理整个GC堆,也有分代收集的概念

空间整合:与CMS的标记-清理不同,G1采用的是标记-整理算法实现,从局部(两个Region之间)是基于复制算法实现;这两种算法意味着G1在运行期间不会产生空间碎片,收集后能提供规整的可用内存

(3)可预测的停顿时间:G1和CMS都是以降低停顿时间为标准,但是G1能提供可预测的停顿时间模型,能让使用明确在一个长度为M毫秒的时间片段内,小号在垃圾收集上的时间不得超过N毫秒


11.G1收集器过程

在G1之前其他收集器进行垃圾收集的范围都是整个新生代和老年代,而G1不再这样。G1垃圾收集器的内存布局和CMS有很大的不同,他将整个Java堆分为大小相等的独立区域(Region),虽然还保留新生代和老年代概念,但新生代和老年大不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。

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

虚拟机进行新生代收集时不需要进行全堆扫描的,Region之间的对象引用以及其他收集器中新生代与老年代之间的对象引用虚拟机都是使用Remembered Set来避免全堆扫描的。G1中的每个Region都有对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作是,会检查Reference引用的对象是否在不同的Region(在分代收集器中时间检查老年代的对象引用了新生代的对象),如果是便把相关引用信息记录到对象所属的Region的Remembered Set中;在进行内存回收时在GC Roots根节点中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。


如果不计算Remembered Set操作,G1收集器运行过程分为以下几个部分:

(1)初始标记:

和CMS的初始标记一样,仅仅是标记GC Roots 根节点

(2)并发标记:

和CMS一样,从根节点GC Roots开始进行可达性分析,并发标记存活的对象,耗时较长

(3)最终标记:

和CMS的重新标记一样,修正并发标记期间产生变动的对象

(4)筛选回收:

首先对各个Region中的回收价值和成本进行排序,根据用户期望的GC停顿时间来指定回收计划,可以并发执行


收集示意图如下:



参考:深入理解java虚拟机