JVM 垃圾回收(1.7/1.8)的一点笔记(面试必备)

1.如何识别垃圾:

(1)引用计数法

       引用计数法就是通过引用来识别无用对象。我们记录每一个对象的引用个数,若有新的变量引用一个对象时,这个对象的引用个数加1;若一个引用失效时,引用的个数减1,而引用个数为0的对象,即可作为垃圾被回收。这里要注意,若这些垃圾对象的成员变量引用了其他对象,则当垃圾对象被释放时,它的这个引用自然就失效了。

       这个算法实现简单,效率也高,但是,它并没有被用在主流的Java虚拟机中,因为它有一个很大的缺陷——很难解决循环引用的问题。Java中的垃圾回收器基本上不使用这个算法。什么是循环引用,看下面一段代码:

public class Main {
    
    private Object obj;
    
    public static void main(String[] args) {
        Main m1 = new Main();
        Main m2 = new Main();

        // 循环引用
        m1.obj = m2;
        m2.obj = m1;

        m1 = null;
        m2 = null;
    }
}

 

(2)可达性分析法

       可达性分析法是Java垃圾回收中判别无用对象的主要方法。这个方法的步骤是,从根节点对象出发,使用DFS或BFS算法,沿着引用递归遍历,而无法被遍历到的对象,就是无法再被使用的对象,可以被垃圾回收器回收。所谓的根节点,就是我们能够直接使用的引用类型变量,如:

  • 方法中的参数或局部变量;
  • 类的静态成员或非静态成员;
  • 代码中的常量;

      这种方法的效率相对于引用计数来说相对复杂,而且效率较低,但是解决了循环引用的问题,是Java垃圾回收中主要使用的方法。

2 如何释放垃圾(垃圾回收算法):

      释放垃圾指的就是清除无用对象,释放它们所占的内存空间,方便继续使用。这里主要介绍三种方法:

  • 标记—清除算法;
  • 复制算法;
  • 标记—整理算法;

(1)标记—清除算法(Mark-Sweep):

      这个算法分为两个步骤:(1)标记;(2)清除。

  • 标记:标记指的就是我们上面所说的可达性分析,采用之前所说的可达性分析算法遍历对象,所有不可达的对象将被标记为垃圾,等待回收;
  • 清除:这一步很简单,直接释放垃圾对象所占内存空间;

    这个算法有两个问题:

          1. 效率较低,标记和清除这两个步骤的效率都比较低,清除的效率低是因为需要扫描整个内存空间,逐个释放对象所占内存;

          2. 使用这个算法清除垃圾后,将会造成很多内存碎片,所以可能出现剩余内存较多,但是没有较大的连续空间,导致大对象无法被分配空间,而再次触发垃圾回收;

                          

                                                      (摘自《深入理解Java虚拟机》) 

(2)复制算法(Copying):

       为了解决效率较低以及产生内存碎片的问题,有人提出了一个新的算法——复制算法。这个算法的原理是:将内存分为两个相等大小的区域,一块存放对象,一块保留。当存放对象的那块区域无法再分配空间时,将所有仍然存活的对象复制到保留的那块区域中,然后直接释放当前正在使用区域的全部内存。这样一来,仍然存活的对象被放进保留区,而垃圾对象也被释放了。同时,之前被使用的空间被清空后,成了新的保留区,而之前的保留区成了被使用的空间,就这样不断循环使用两个空间。

       堆内存被分为新生代和老年代。在新生代中,每次垃圾回收都可以释放大量的对象,只有少部分存活,所以只有少部分对象要被复制到保留区中,这也意味着复制并不会太耗时。除此之外,直接释放被使用的空间的全部内存,比一段一段释放的效率也要高很多。同时,对象被复制到另外一个区域时,会被整齐地摆放,所以不会出现内存碎片,所以能够更简单地分配空间。所以,复制算法的效率要远远高于标记—清除算法。以下是一张复制算法的演示图:

                               

      问题:

      复制算法将内存区域划分为相等的两部分,这也意味着每次都有一半的空间无法被使用,这未免也太浪费了。

方法改进:

       IBM公司的研究表明,98%的对象存活时间都非常的短暂,所以,完全没有必要保留一半的空间供复制使用。在实际实现中,会将空间划分为三块区域,一块较大的Eden空间,以及两块较小的Survivor空间。在为新对象分配空间时,首先会将其分配到Eden空间中,若Eden空间无法再分配空间时,将会触发垃圾回收,此时,会将Eden空间中的存活对象复制到其中一块Survivor空间中,然后清空Eden空间。当Eden空间再一次因无法分配空间而触发垃圾回收时,则会将Eden空间中的存活对象,以及上一次被复制进Survivor空间中的存活对象,都复制到另一块Survivor空间中,然后将Eden和上一块Survivor清空。也就是说,交替地使用两块Survivor空间,来存放垃圾回收中任然存活的对象。而在具体实现中,这三个空间的比例一搬是8:1:1,即是说只有10%的空间无法被使用。

这个算法在大部分对象的生命周期都短时,效率会非常高,但是若大部分对象的生命周期都很长,将不再适用,所以这个算法一般只被用在新生代中

如果在某次垃圾回收过后,仍然有大量的对象存活,此时一个Survivor空间不够存放这些对象怎么办?

存入老年代。老年代为这个算法提供了担保,但是在大部分情况下,Survivor都是能够满足需求的。

(3)标记—整理(Mark-Compact)

       由于老年代中的对象一般存活时间都比较长,所以并不适合在老年代使用上面的复制算法进行垃圾回收。而有人根据老年代的特点,提出了标记—整理算法,算法也分为标记和整理两个步骤,标记这个步骤和第一个算法是一样的,所谓的整理,就是将内存中还存活的对象向一边移动,直至这些对象相互靠拢,整齐排列,然后直接清除不属于这一部分的全部内存。标记—整理的好处是解决内存碎片的问题

                                                                                                             

 

(4)分代收集算法

      分代收集算法并不是什么新思想,而是对上面三种算法的综合使用。前面也提过,为方便垃圾回收,一般将堆内存分为新生代和老年代两个部分。

  • 对于新生代而言,这一块区域中的对象存活时间短,每一次垃圾回收都能回收大部分内存,所以适合使用复制算法,同时以老年代作为这个算法的担保空间;
  • 对于老年代而言,每次垃圾回收只能释放小部分空间,若使用复制算法,每次将需要做大量复制,而且此时Survivor需要较大的空间,所以不适合使用复制算法,因此在老年代中,一般使用标记—清除或者标记—整理算法;

                                                                     

3.垃圾收集的具体实现:垃圾收集器

      JDK7/8后,HotSpot虚拟机所有收集器及组合(连线),如下图:

         

  1. 图中展示了7种不同分代的收集器:

    Serial、ParNew、Parallel Scavenge、Serial Old、Parallel Old、CMS(Concurrent Mark Sweep)、G1;

  2. 而它们所处区域,则表明其是属于新生代收集器还是老年代收集器:

    新生代收集器:Serial、ParNew、Parallel Scavenge;

    老年代收集器:Serial Old、Parallel Old、CMS;

     整堆收集器:G1;

  3. 两个收集器间有连线,表明它们可以搭配使用:

    Serial/Serial Old、Serial/CMS、ParNew/Serial Old、ParNew/CMS、Parallel Scavenge/Serial Old、Parallel Scavenge/Parallel Old、G1;

  4. 其中Serial Old作为CMS出现"Concurrent Mode Failure"失败的后备预案();

 

    主要介绍CMS 和 G1 收集器:

         CMS收集器:

   CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于并发“标记清理”实现,在标记清理过程中不会导致用户线程无法定位引用对象。仅作用于老年代收集。它的步骤如下:

  1.  初始标记(CMS initial mark):独占CPU,stop-the-world, 仅标记GCroots能直接关联的对象,速度比较快;
  2. 并发标记(CMS concurrent mark):可以和用户线程并发执行,通过GCRoots Tracing 标记所有可达对象;
  3. 重新标记(CMS remark):独占CPU,stop-the-world, 对并发标记阶段用户线程运行产生的垃圾对象进行标记修正,以及更新逃逸对象;
  4. 并发清理(CMS concurrent sweep):可以和用户线程并发执行,清理在重复标记中被标记为可回收的对象。

         CMS的优点:

  • 支持并发收集.
  • 低停顿,因为CMS可以控制将耗时的两个stop-the-world操作保持与用户线程恰当的时机并发执行,并且能保证在短时间执行完成,这样就达到了近似并发的目的.

         CMS的缺点:

  • CMS收集器对CPU资源非常敏感,在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分CPU资源,如果在CPU资源不足的情况下应用会有明显的卡顿。
  • 无法处理浮动垃圾:在执行‘并发清理’步骤时,用户线程也会同时产生一部分可回收对象,但是这部分可回收对象只能在下次执行清理是才会被回收。如果在清理过程中预留给用户线程的内存不足就会出现‘Concurrent Mode Failure’,一旦出现此错误时便会切换到SerialOld收集方式。
  • CMS清理后会产生大量的内存碎片,当有不足以提供整块连续的空间给新对象/晋升为老年代对象时又会触发FullGC。且在1.9后将其废除。

        使用场景

    它关注的是垃圾回收最短的停顿时间(低停顿),在老年代并不频繁GC的场景下,是比较适用的。

        G1收集器

       G1收集器的内存结构完全区别于CMS,弱化了CMS原有的分代模型(分代可以是不连续的空间),将堆内存划分成一个个Region(1MB~32MB, 默认2048个分区),这么做的目的是在进行收集时不必在全堆范围内进行。它主要特点在于达到可控的停顿时间,用户可以指定收集操作在多长时间内完成,即G1提供了接近实时的收集特性。它的步骤如下:

  1. 初始标记(Initial Marking):标记一下GC Roots能直接关联到的对象,伴随着一次普通的Young GC发生,并修改NTAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,此阶段是stop-the-world操作。
  2. 根区间扫描,标记所有幸存者区间的对象引用,扫描 Survivor到老年代的引用,该阶段必须在下一次Young GC 发生前结束。
  3. 并发标记(Concurrent Marking):是从GC Roots开始堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行,该阶段可以被Young GC中断。
  4. 最终标记(Final Marking):是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,此阶段是stop-the-world操作,使用snapshot-at-the-beginning (SATB) 算法。
  5. 筛选回收(Live Data Counting and Evacuation):首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,回收没有存活对象的Region并加入可用Region队列。这个阶段也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。

           G1的特点

  • 并行与并发:G1充分发挥多核性能,使用多CPU来缩短Stop-The-world的时间,
  • 分代收集:G1能够自己管理不同分代内已创建对象和新对象的收集。
  • 空间整合:G1从整体上来看是基于‘标记-整理’算法实现,从局部(相关的两块Region)上来看是基于‘复制’算法实现,这两种算法都不会产生内存空间碎片。
  • 可预测的停顿:它可以自定义停顿时间模型,可以指定一段时间内消耗在垃圾回收商的时间不大于预期设定值。

         使用场景

       G1 GC切分堆内存为多个区间(Region),从而避免很多GC操作在整个Java堆或者整个年轻代进行。G1 GC只关注你有没有存货对象,都会被回收并放入可用的Region队列。G1 GC是基于Region的GC,适用于大内存机器。即使内存很大,Region扫描,性能还是很高的。

        Remembered Set

我们之前说过,G1在回收每个Region上的垃圾时,每个Region之间又有相互依赖引用关系,想要做到对全部Region进行扫描清理,那么不得不做一次全堆扫描。这样就降低了垃圾回收的效率。所以HotSpot引入了Remembered Set来专门存储于管理对象的引用依赖关系,这样当每次回收时,只需要根据Remembered Set上面的对应关系找到相对的区域进行清理,这样就可以避免扫描整个堆内存又不会遗漏某一个区域。

4. GC分类:

1.Minor GC:

  • 针对新生代。
  • 指发生在新生代的垃圾收集动作,因为java对象大多都具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
  • 触发条件:Eden空间满时。

2.Major GC/Full GC:

  • 针对老年代。
  • 指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge 收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。
  • 触发条件:Minor GC 会将对象移到老年代中,如果此时老年代空间不够,那么触发 Major GC。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值