Java GC算法以及垃圾收集器

概述

jvm中,程序计数器、虚拟机栈、本地方法栈随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地进行着出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知,方法结束或者线程结束时,内存就自然跟随真回收了。因此 垃圾回收主要集中在Java堆和方法区,在程序运行期间,这部分内存的分配和使用都是动态的。

如何判断对象是否存活

引用计数算法:给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值减1;计数为0时回收。主流虚拟机未采用此方法。 主要原因是它很难解决对象间相互循环引用的问题。如objA和objB都有字段instance, 赋值令objA.instance=objB 及 objB.instance=objA
,objA=null,objB=null 引用计数算法无法通知GC回收。但实际上会回收。

可达性分析算法:从GC Roots开始向下搜索,搜索所走过的路径成为引用链,当一个对象到GC Roots没有任何引用链相连事,则证明此对象是不可用的。
GC Roots对象包括:
1)虚拟机栈(栈帧中的本地变量表)中引用的对象
2)方法区中类静态属性引用的对象
3)方法区中类常量引用的对象
4)本地方法栈中JNI(即一般说的Native)引用的对象

再谈引用:JDK1.2后,将引用分为强引用、软引用、弱引用、虚引用
强引用(Strong Reference):永远不会被回收
软引用(Soft Reference):用来描述一些还有用但并非必须的对象(用点像缓存的思想)。内存够用不回收,但不够用需要回收。
弱引用(Weak Reference):无论内存是否够用,都会被回收。
虚引用(Phantom Reference):为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时受到一个系统通知。

生存还是死亡:要真正宣告一个对象死亡,至少经过两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。 所有可以通过重新finalize()拯救自己,但只能拯救一次。不过finalize()不建议使用,可以使用try-finally或者其他方式都可以做的更好、更及时。

垃圾收集算法


标记-清除算法(Mark-Sweep)

最基础的收集算法,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足:1)效率问题,标记和清除两个过程的效率都不高 ;2)空间问题,标记清除之后会产生大量不连续的内存碎片

复制算法

它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。
我们商用的虚拟机都采用这种收集算法来回收新生代,IBM公司专门的研究表明,新生代中的对象98%是“朝生夕死”,所以并不需要按1:1的比例来划分内存,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和启用的一块Survivor。( HotSpot虚拟机默认Eden和Survivor的大小比例是8:1 可通过 -XX:SurvivorRatio=8 配置),当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)

标记—整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法

“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。

HotSpot的算法实现


枚举根节点

在可达性分析中,可以作为GC Roots的节点有很多,但是现在很多应用仅仅方法区就有上百MB,如果逐个检查的话,效率就会变得不可接受。而且,可达性分析必须在一个一致性的快照中进行-即整个分析期间,系统就像冻结了一样。否则如果一边分析,系统一边动态表化,得到的结果就没有准确性。这就导致了系统GC时必须停顿所有的Java执行线程。
目前主流Java虚拟机使用的都是准确式GC,所以当执行系统都停顿下来之后,并不需要一个不漏的检查完所有执行上下文和全局的引用位置,虚拟机应该有办法直接知道哪些地方存放着对象引用。在HotSpot实现中,使用一组称为 OopMap的数据结构来达到这个目的。OopMap会在类加载完成的时候,记录对象内什么偏移量上是什么类型的数据,在JTI编译过程中,也会在特定的位置记录下栈和寄存器哪些位置是引用。这样,在GC扫描的时候就可以直接得到这些信息了。

安全点

可能导致引用关系变化,或者说OopMap内容变化的指令非常多,HotSpot并不会为每条指令都产生OopMap,只是在特定的位置记录了这些信息,这些位置成为“安全点”(SafePoint)。程序执行时只有在达到安全点的时候才停顿开始GC。一般具有较长运行时间的指令才能被选为安全点,如方法调用、循环跳转、异常跳转等。
接下来要考虑的便是,如何在GC时保证所有的线程都“跑”到安全点上停顿下来。这里有两种方案: 抢先式中断(Preemptive Suspension)主动式中断(Voluntary Suspension)
抢先式中断会把所有线程中断,如果某个线程不在安全点上,就恢复让它跑到安全点上。几乎没有虚拟机采用这种方式。
主动式中断思想是设立一个GC标志,各个线程会轮询这个标志并在需要时自己中断挂起。这样,标志和安全点是重合的。

安全区域

Safepoint机制可以保证某一程序在运行的时候,在不长的时间里就可以进入GC的Safepoint。但是如果程序没有分配CPU时间,例如处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求。对于这种情况,只能用安全区域(Safe Region)来解决。
安全区域是指在一段代码片段之中,引用关系不会发生变化。在这个区域中任意地方开始都是安全的。在线程执行到Safe Region中的代码时,就标记自己已经进入了Safe Region,这样JVM在发起GC时就跳过这些线程。在线程要离开Safe Region时,它要检查系统是否已经完成了枚举(或GC过程),如果完成了线程就继续执行,否则就等待。

垃圾收集器


Serial收集器和Serial Old收集器

串行收集器是最古老,最稳定以及效率高的收集器,可能会产生较长时间的停顿,只使用 一个线程去回收。新生代采用 复制算法暂停所有用户线程,老年代采用 标记-整理算法暂停所有用户线程。
参数控制:-XX:+UseSerialGC

ParNew收集器

ParNew收集器其实是Serial收集器的多线程版本。,除了使用多线程进行垃圾收集之外,其余都和Serial收集器一样。这个也是配合这Serial Old使用。 默认 开启的收集线程数与CPU的数量相同
参数控制:-XX:+UseParNewGC
-XX:ParallelGCThreads 限制线程数量

Parallel Scavenge收集器和Parallel Old收集器

JDK8 Server 默认的收集器
Parallel Scavenge收集器类似ParNew收集器,Parallel收集器更关注系统的吞吐量,也经常被成为“吞吐量优先”收集器。可以通过参数(-XX:+UseAdaptiveSizePolicy 默认是true)来打开自适应调节策略,当打开这个参数后,就不需要手工指定新生代的大小(-Xmn)、Eden和Survivor的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。

参数控制:-XX:+UseParallelOldGC 使用Parallel收集器+老年代并行

CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。
从名字(包含“Mark Sweep”)上就可以看出CMS收集器是基于 “标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为4个步骤,包括:  
初始标记(CMS initial mark)
并发标记(CMS concurrent mark)
重新标记(CMS remark)
并发清除(CMS concurrent sweep)
其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。 
      由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行。 老年代收集器(新生代使用ParNew)
优点: 并发收集低停顿 
缺点: 1 )CMS收集器对CPU资源非常敏感 2)CMS收集器无法处理浮动垃圾 3)产生大量空间碎片、并发阶段会降低吞吐量
参数控制: -XX:+UseConcMarkSweepGC  使用CMS收集器
              -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长
             -XX:+CMSFullGCsBeforeCompaction  设置进行几次Full GC后,进行一次碎片整理
             - XX:ParallelCMSThreads  设定CMS的线程数量(一般情况约等于可用CPU数量)

G1收集器

G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。Oracle力推,计划在jdk9设置为默认的收集器
与CMS收集器相比G1收集器有以下特点:
1. 空间整合,G1收集器采用 标记-整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
2. 可预测停顿,这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。
上面提到的垃圾收集器,收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔阂了,它们都是一部分(可以不连续)Region的集合。
G1的新生代收集跟ParNew类似,当新生代占用达到一定比例的时候,开始出发收集。
和CMS类似,G1收集器收集老年代对象会有短暂停顿。

参数总结


参数 描述
-XX:+UseSerialGC Client模式下的默认值,使用Serial + Serial Old的收集器组合
-XX:+UseParNewGC 使用ParNew + Serial Old的收集器进行垃圾回收
-XX:+UseConcMarkSweepGC 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用。
-XX:+UseParallelGC Server模式下的默认值,使用Parallel Scavenge + Serial Old的收集器组合
-XX:+UseParallelOldGC 使用Parallel Scavenge + Parallel Old的收集器组合进行回收
-XX:SurvivorRatio Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1
-XX:PretenureSizeThreshold 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配
-XX:MaxTenuringThreshold 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代
-XX:UseAdaptiveSizePolicy 动态调整java堆中各个区域的大小以及进入老年代的年龄
-XX:+HandlePromotionFailure 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留
-XX:ParallelGCThreads 设置并行GC进行内存回收的线程数
-XX:GCTimeRatio GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效
-XX:MaxGCPauseMillis 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效
-XX:CMSInitiatingOccupancyFraction 设置CMS收集器在老年代空间被使用多少后出发垃圾收集,默认值为68%,仅在CMS收集器时有效,-XX:CMSInitiatingOccupancyFraction=70
-XX:+UseCMSCompactAtFullCollection 由于CMS收集器会产生碎片,此参数设置在垃圾收集器后是否需要一次内存碎片整理过程,仅在CMS收集器时有效
-XX:+CMSFullGCBeforeCompaction 设置CMS收集器在进行若干次垃圾收集后再进行一次内存碎片整理过程,通常与UseCMSCompactAtFullCollection参数一起使用
-XX:+UseFastAccessorMethods 原始类型优化
-XX:+DisableExplicitGC 是否关闭手动System.gc
-XX:+CMSParallelRemarkEnabled 降低标记停顿
-XX:LargePageSizeInBytes 内存页的大小不可设置过大,会影响Perm的大小,-XX:LargePageSizeInBytes=128m

Client、Server模式默认GC
  新生代GC方式 老年代和持久代GC方式
Client Serial 串行GC Serial Old 串行GC
Server Parallel Scavenge 并行回收GC Parallel Old 并行GC

以下是通过java -XX:+PrintFlagsFinal pid 查询的jvm的默认值



相关链接:
深入理解G1收集器 http://blog.jobbole.com/109170/

猜你喜欢

转载自blog.csdn.net/tlk20071/article/details/77933780