垃圾收集器与内存分配策略

  经过半个多世纪的发展、目前内存的动态分配与内存回收技术已经相当成熟,但作为程序猿还是得了解GC和内存分配。当需要排查各种内存溢出、内存泄漏、当垃圾收集成为系统达到更高并发量的瓶颈时,就需要对内存的动态分配与内存回收技术实施必要的监控和调节。

  本文讲叙了内存中垃圾的收集及内存分配策略。相比较而言,垃圾收集更难一些。本文将介绍几种常见的垃圾收集器及常用垃圾收集算法。垃圾收集算法是基于判断对象在内存中是否死亡,只有判断确定出对象已经死亡,才能采取不同的方式进行收集,实现内存的回收。

判断对象死亡常用方法

  引用计数算法(Reference Counting):给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1;当引用失效时,计数器值就-1;任何时刻计数器为0的对象就是不可能再被使用的。实现简单、判定效率高很难解决对象之间的循环引用问题,目前主流Java虚拟机里没有选用计数算法来管理内存。

  可达性分析算法(Reachability Analysis):通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可引用的。即使是不可达的对象,也并非是“非死不可”的,只是暂时处于“缓刑”阶段,真正宣告死亡,至少还是经历再次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,则将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。在Java语言中,可作为GC Roots的对象有四种:(1)虚拟机栈中引用的对象;(2)方法区中类静态属性引用的对象;(3)方法区中常量引用的对象;(4)本地方法栈中JNI引用的对象。

  判断对象是否存活与“引用”相关。引用可分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,其引用强度依次减弱。

垃圾收集算法

  标记—清除算法(Mark-Sweep):分为“标记”和“清除”两个阶段,首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。主要两处不足:一个是效率问题,标记和清除两个过程的效率都不高;二是空间问题,标记清除之后会产生大量不连续的内存碎片。

  复制算法(Copying):将可用内存按容量划分为大小相等的两块,每次只使用其中一块,当这一块用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次性清理掉。可根据实际需求将两块内存按不同比例划分。现在商业虚拟机都采用这种收集算法来回收新生代。在对象存活率较高时,就要进行较多的复制操作,效率会变低。

  标记—整理算法(Mark-Compact):标记的过程与“标记—清除”算法一样,只是在整理阶段,让所有存活的对象都向一端移动,然后直接清理掉端边界以外的的内存。

  分代收集算法(Generational Collection):根据对象存活周期的不同将内存划分为几块,一般将Java堆分为新生代和老年代,再根据各个年代的特点采用最适当的收集算法。

垃圾收集器

垃圾收集器是内存回收的具体实现。基于JDK 1.7 Update 14之后的HotSpot虚拟机所包含的收集器如下图所示:

  Serial收集器:单线程收集器,只使用一个CPU或一条线程去完成垃圾收集工作,在垃圾收集时,必须暂停其他所有工作线程,直接收集结束。对于运行在Client模式下的虚拟机来说是一个很好的选择

  ParNew收集器:Serial收集器的多线程版本。运行在Server模式下的虚拟机中首先的新生代收集器。除了Serial收集器外,目前只有ParNew收集器能与CMS收集器配合工作。

  Parallel Scavenge收集器:也称为“吞吐量优先”收集器,关注点是达到一个可控制的吞吐量(Throughput)。吞吐量=运行用户代码时间/(运行代码时间+垃圾收集时间)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。GC自适应调节策略(GC Ergonomics)是指虚拟机根据当前系统的运行情况收集性能监控信息,动态调整参数-XX:+UseAdaptiveSizePolicy以提供最合适的停顿时间或者最大的吞吐量。

  Serial Old 收集器单线程收集器,使用“标记—整理”算法。主要意义是在于给Client模式下的虚拟机使用。

  Parallel Old收集器:是Parallel Scavenge收集器的老年代版本,使用多线程和“标记—整理”算法,在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。

  CMS收集器(Concurrent Mark Sweep):以获取最短回收停顿时间为目标的收集器。整个过程可分为初始标记(CMS initial mark)、并发标记(CMS concurrent mark)、重新标记(CMS remark)、并发清除(CMS concurrent sweep)4个步骤。整个过程中耗时最长的是并发标记和并发清除过程,但它们都可以与用户线程一起工作。总体来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。有3个明显的缺点:(1)CMS收集器对CPU资源非常敏感;(2)CMS收集器无法处理浮动垃圾(Floating Garbage);(3)收集结束时会有大量的空间碎片产生。

  G1收集器(Garbage-First):当今收集器技术发展最前沿成果之一。在G1之前的收集器收集范围是整个新生代或者老年代,G1不再是这样。G1将整个堆划分为多个大小相等的独立区域(Region),G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region,从而保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。运行分为4个步骤:初始标记(Initial Marking)、并发标记(Concurrent Marking)、最终标记(Final Marking)和筛选回收(Live Data Counting and Evacuation)。具备如下特点:并行与并发、分代收集、空间整合、可预测的停顿。

内存分配与回收策略

  对象优先在Eden分配:大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。

  大对象直接进行老年代:所谓大对象是指需要大量连续内存空间的Java对象,很长的字符串以及数组就是最典型的大对象。

  长期存活的对象将进入老年代:虚拟机给每个对象定义一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当年龄达到一定程度时,就会被晋升到老年代中。晋升阈值可以通过参数设置。

  动态对象年龄判定:为了能更好地适应不同的内存状况,虚拟机并不是永远地要求对象的的年龄必须达到MaxTenuringThreshold才能晋升老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可直接进入老年代。

  空间分配担保:在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,以确保Minor GC是安全的。如果不成立,则查看HandlePromotionFailure设置值是否允许担保失败。若允许,则会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试进行一次Minor GC,但有一定的风险;如果小于,或者HandlePromotionFailure设置不允许冒险,则改为进行一次Full GC。

原文地址:https://www.cnblogs.com/hthuang/p/4662926.html