首页 > 代码库 > JVM-垃圾回收

JVM-垃圾回收

1.垃圾回收如何判定

  1.1引用计数法

    引用计数法是给对象添加一个引用计数器,当有对该对象的引用时,计数器加1,引用失效时,计数减1,计数器为0时不能再使用.该对象可以被垃圾回收器回收,但是存在一个问题,就是当两个对象相互进行引用时,它们的计数器最终都不会为0,导致垃圾回收器无法回收它们。

      1.2可达性分析算法

    算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。 

技术分享

 

    Java语言中,可作为GC Roots的对象包括下面几种:
    虚拟机栈(栈帧中的本地变量表)中引用的对象。
    方法区中类静态属性引用的对象。
    方法区中常量引用的对象。
    本地方法栈中JNI(即一般说的Native方法)引用的对象

  1.3判断当前引用类型

    Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、 软引用(Soft Reference)、 弱引用(Weak Reference)、 虚引用(Phantom Reference4种,这4种引用强度依次逐渐减弱 

     强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

     软引用是用来描述一些还有用但并非必需的对象。 对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。 如果这次回收还没有足够的内存,才会抛出内存溢出异常。 在JDK 1.2之后,提供了SoftReference类来实现软引用。

      弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。 在JDK 1.2之后,提供了WeakReference类来实现弱引用。

      虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。 为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 在JDK 1.2之后,提供了PhantomReference类来实现虚引用

  1.4判断对象是否可以回收

   真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。 当对象没有覆盖finalize()方法,或finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为没有必要执行

  1.5针对永久代的垃圾回收

    永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

   废弃常量:没有任何对象是当前String类型的常量,比如说当前String s = "adc",当没有对象引用"adc"时,该常量就可以被垃圾回收器回收

    废弃的无用类:Java堆中不存在该类的任何实例

           加载该类的ClassLoader已经被回收;

           java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。 

2.垃圾回收算法

  2.1标记-清除算法

   算法分标记清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象 

   优点:实现原理简单

   缺点:标记和清理时效率不高;清楚过程中会产生内存碎片。

技术分享

  2.2复制算法

   它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉 

   优点:实现简单,运行高效

   缺点:浪费了一半的内存,需要将内存缩小为原来的一半来复制存活的对象区域。

技术分享

  2.3标记-整理算法 

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

 技术分享

 

 

  2.4分代收集算法   

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

技术分享

 

技术分享

   新的对象分配在Eden 和 Survivor1 这两个空间中如果空间不足, 发起GC, 将 Eden , Survivor1中的存活对象移动到Survivor2中然后将Eden, Survivor1中对象清理掉。 -- 如果Survivor2的空间也不够,则依赖其他内存(老年代)接下来新对象会被分配在Eden 和Survivor2中, 即 Survivor1 变成了完全空闲空间,它和Survivor2的角色进行了转换 , 如此循环下去。

 

3.垃圾收集器

  3.1 Serial/Serial Old收集器 

  Serial收集器是单线程收集器,在进行垃圾回收时,必须暂停其他所有线程

  Serial Old收集器是Serial收集器的老年代版本

  Serial收集器和Serial Old收集器对新生代都采用了复制算法,对老年代采用了标记-整理算法,都可以在Client模式下的虚拟机使用,在Server模式下,Serial Old还可以搭配Parallel Scavenge ,在并发收集发生Concurrent Mode Failure时作为CMS收集器的备用方案使用

技术分享

  3.2 ParNew收集器

  是Serial收集器的多线程版本,也需要暂停用户线程,对象分配与回收策略与Serial收集器基本一致,除了Serial收集器,只有它能与CMS搭配使用。

技术分享

  3.3 Parallel Scavenge/Parallel Old收集器       

    Parallel Scavenge:针对新生代, 多线程, 采用复制算法    

    Parallel Old: 针对老年代, 多线程,标记-整理算法    

           吞吐量优先:用户代码运行时间/(垃圾收集时间+用户代码运行时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99% 

技术分享

  3.5 CMS收集器   

  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器 ,

  应用场合互联网,B/S服务器端,重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验 

  采用标记-清除算法,易产生碎片。

  处理过程:初始标记,并发标记,重新标记,并发清理

 

  初始标记和重新标记是需要暂停用户线程 

技术分享

 

  3.6 G1收集器 

  面向服务器端应用的垃圾回收器,针对老年代和年轻代,采用标记整理算法

  具有以下四个特征:

  并行与并发:G1能充分利用多CPU、 多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。

  分代收集:与其他收集器一样,分代概念在G1中依然得以保留。 虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、 熬过多次GC的旧对象以获取更好的收集效果。

  空间整合:与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。 这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。  

  可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。 

  处理过程:初始标记,并发标记,最终标记,筛选标记

技术分享

 

 

4.内存分配与回收策略

  4.1对象优先在Eden分配 

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

  新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。

  老年代GC(Major GC/Full GC):指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 Major GC的速度一般会比Minor GC慢10倍以上。  

  4.2大对象直接进入老年代 

  大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(笔者列出的例子中的byte[]数组就是典型的大对象) 

  4.3长期存活的对象将进入老年代 

  虚拟机给每个对象定义了一个对象年龄(Age)计数器。 如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。 对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中

  4.4动态对象年龄判定 

  并不总是要求对象的年龄到达一定程度才进入老年代。如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代 

  4.5空间分配担保 

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

技术分享

  此处冒险是指:新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。 与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间

 

该文章大部分参考自:《深入理解Java虚拟机》周志明 著

 

JVM-垃圾回收