垃圾收集器

本篇文章将从三点切入:什么是垃圾,什么时候回收,如何回收。

  1.什么是垃圾:

    运行过程中没有任何指针指向的对象,就是需要被回收的垃圾。那么jvm是怎样判别哪个是没有被指向的对象呢?

    1)引用计数算法:

      在对象中添加一个引用计数器,有地方引用时+1,引用失效-1。但Java中不用该算法,是因为其无法解决循环引用的问题。比如两对象互相引用着,那么他们的计数器始找不可能为0,也就不会被当作垃圾。

    2)可达性分析算法:

      通过一些列称为“GC Roots”的根对象作为起始节点集,从这些结点开始根据引用关系向下搜索,能直接或间接的被搜索到的都是存货对象。当某个对象不可达,则被判定为可回收对象。

                                                                    

      那么哪些对象可被当作GC Roots呢?(全局性的引用与执行上下文)

        1.虚拟机栈中局部变量表引用的对象/本地方法栈中Native方法引用的对象。

        2.方法区中类静态属性引用的对象/方法区中常量引用的对象。

        3.虚拟机内部的引用:如基本数据类型对应的class对象,常驻的异常对象,类加载器。

        4.被同步锁持有的对象。

        5.Java堆中的某一块区域发起垃圾收集,其他不同区域作为GC Roots。如老年代引用着新生代中的对象。

  2.什么时候回收:

    其实这个问题要结合着1问题来看:由于类,常量太多了,HotSpot中使用了一组称为OopMap的数据结构来得到合适的GC Roots集。标记GC Roots能直接关联的对象的这个工作我们将其称为根节点枚举,在这个过程中是必须要暂停用户线程的。但是线程不能说停就停,必须到一个安全点才可以进行刚才我们所说的根节点枚举的工作。Java采用的是主动式中断用户线程来实现的,其具体思想史是:设置一标志位,各个线程主动的轮询这个标志位,一旦发现标志位为真我们就在最近的安全点上主动中断挂起。

  3.如何回收:

    这一部分主要说两个内容,一个是如何回收的思想,也就是算法,另一个是算法的具体实现,也就是具体的垃圾回收器。

    算法:

      1):标记-清除算法:

          首先标记出所有需要回收的对象(即可达性算法),标记完成后,统一回收所有被标记的对象。

          缺点:1.若大量对象都需要被回收,那么标记和清除两个过程的执行效率就太低了。

             2.清除后会产生大量不连续的内存碎片。若我们想再放一个大对象但却找不到合适的内存空间,这就不得不又触发一次GC。

             3.因为有内存碎片,所以只能采用空闲列表的方式分配内存。

      2):标记-复制算法:

          将可用内存分为两个区域,每次存放对象仅使用其中的一块。当有对象的区域填满时,就将该区域所有的存活对象移至另一块区域,再将已使用过的一次性清理完。

          优点:1.当大量对象都需要被回收,那么需要复制的只是占少数的对象,并且回收是对半个区进行回收。

             2.无内存碎片,采用指针碰撞的方式分配内存。

          缺点:1.可用内存变为原来的一半。(对于新生代来说,大部分对象都是朝生夕死的,所以不需要1:1来划分新生代空间。将新生代分为Eden,Survior1,Survior2,比例为8:1:1,老年代作为逃生门/存活时间长的对象)。

             2.当大量对象都是存活的,会产生大量的内存间复制开销。

      3):标记-整理算法:

          针对老年代的死亡特征,将回收区域的所有存活对象标记出来,让其向内存空间一端移动,然后直接清除掉边界外的内存。

          优点:无内存碎片。缺点:大量对象存活时,所需STW时间较大。

    实现(垃圾收集器):

      首先了解垃圾收集器的分类,然后再说具体的垃圾收集器。

        根据是否需要STW(Stop The World)分为独占式和并发式:独占式就是只有垃圾收集器工作,而并发式则是用户线程和垃圾收集器交替工作。

        独占式又可分为串行和并行:串行就是仅有一条垃圾收集器工作,而并行则是多条垃圾收集器工作。这两个都有STW.

        根据内存空间又可分为:新生代垃圾回收器或老年代垃圾收集器。

      接下来就是具体的垃圾收集器了:

        1.Serial:是新生代中的收集器,采用单线程串行收集,复制算法进行垃圾回收。没有线程交互的开销,可获得最高的单线程收集效率。是客户端模式很好的选择

        2.ParNew:是新生代中的收集器,采用多线程并行收集,复制算法进行垃圾回收。除了串并行,与Serial无太对差别。

        3.Parallel:是新生代中的收集器,采用多线程并行收集,复制式算法进行垃圾回收。它最大的特点式可控制吞吐量,自适应调节策略

        4.Serial Old:是老年代中的收集器,采用单线程串行收集,标记-整理算法。可与Serial,ParNew(9之前),Parallel(6前)搭配使用。

        5.Parallel Old:是老年代中的收集器,多线程并行收集。6之前Parallel只能与Serial Old搭配使用,因为一个是并行一个是串行,所以并不能实现吞吐量的最大化。

        6.CMS:是老年代中的收集器,多线程并发,标记-清除算法。因为清除时没有内存碎片的整理,所以没有STW。以下是其具体步骤。

    

                

        还需要注意的是,并发清除是并行的,该阶段还会有新的对象进来,所以不能让老年代放慢了才开始回收。在JDK5下默认使用68%就激活CMS,6提升至92%。但仍存在预留空间不够新对象存储,就会出现并发示标,此时就启动后备方案(Serial Old)。

        不可避免的还有内存碎片的问题:多次进行CMS后内存不够则触发FULL GC,在下一次Full GC前进行一次碎片整理。

      7.G1

        两个代都可以起作用。原因是其将Java堆分为多个大小相同的Region,注意此时堆仍然有分代的思想,不过是不要求每个代的内存的是连续的。G1跟踪各个Region里垃圾堆价值的大小,在后台维护一个优先级表,每次根据用户设置的停顿时间优先处理价值大的Region。

        根据以上的描述我们可以得出G1的三个优点:

          1.因为其设置的的是Region优先级表,所以可以同时兼顾新生代和老年代。

          2.每次处理的都是Region的整数倍,就是可预测的停顿时间。

          3.优先处理价值大的Region提高了执行效率。

        具体步骤:

          1.初始标记:对用户线程做短暂暂停,标记GC Roots能直接关联到的对象,并修改TAMS指针。

          2.并发标记:从GC Roots进行可达性分析,与用户线程并发执行,并重新处理SATB记录下的在并发时有引用变动的对象。

          3.最终标记:对用户线程做短暂暂停,处理并发时有变动的对象。

          4.筛选回收:对用户线程做短暂暂停,对Region的回收价值和成本进行排序,根据用户希望的停顿时间选择多个Region回收集,把决定回收Region内的存活对象移至空Region,再清理。

        那G1是如何解决以下问题呢?

          1.Region里存在跨Region引用的对象:如老年代引用了新生代对象

            使用记忆集避免全堆作为GC Roots扫描,G1中使用的是卡表,存储了哪个Region引用了当前Region里的对象,就避免了全局扫描

          2.并发标记中如何保证用户线程改变对象引用关系/添加新的对象时,对堆里的对象正确标记。即怎样缺点第三步的变动对象?

            首先我们先来分析错误是如何产生的。(三色标记)

              黑色:该对象已经被标记过且其所有的引用也已经被扫描了。

              灰色:该对象已经被扫描了,但其引用至少还有一个未被扫描。

              蓝色:未被扫描的对象。

                                   

        当完成所有的扫描之后,结果应如图所示

                       

        那么怎样能出现错误呢?

                           

        那么扫描完成后结果将是这样:

        (改错:视为)

        我们可以看出,出现这种错误必须有两点:1.插入例如一条或多条从黑色对象到蓝色对象的引用。2.删除了全部灰色对象到该蓝色对象的直接或间接引用。只要打破两者间的一个那么错误就不会出现。

          增量更新:将该黑色对象插入的引用记录下来,并发扫描结束后,重新扫描时将记录的黑色对象作为GC Roots再一次扫描。(CMS)

          原始快照:将该灰色对象删除的引用记录下来,并发扫描结束后,重新扫描时将记录的灰色对象作为GC Roots再一次扫描。(G1)

        以上只解决了改变引用,没有解决添加对象:

          为每个Region设计两个名为TAMS指针,默认添加对象只能在这两个指针之间添加,且默认该区域不会作为回收范围。

 

posted @ 2020-12-08 20:34  yr66  阅读(185)  评论(0编辑  收藏  举报