Java垃圾收集算法和垃圾收集器

一、判断对象是否存活的算法

    1. 引用计数法

            引用计数器的实现非常简单,对于一个对象A,,只要任何一个对象引用了A,则A的计数器就加1,当引用失效时,计数器就减1.只要对象A的引用计数器的值为0,则对象A就不可能再被使用。

其实现也特别简单,只需要为每个对象配备一个整型计数器即可。但是引用计数器有两个严重的问题:

  • a. 无法处理循环引用的情况,两个不可达对象之间的循环引用。
  • b. 在每次因引用产生和消除的时候,需要伴随一个加法操作和减法操作。对系统性能有一定的影响。

   2.可达性分析算法

            通过一系列的名为“GC Root”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Root没有任何引用链相连时,则该对象不可达,该对象是不可使用的,垃圾收集器将回收其所占的内存。  

  2.1 在Java语言中,可作为GC Root的对象包括以下几种对象:

  • a. Java虚拟机栈(栈帧中的本地变量表)中的引用的对象。
  • b.方法区中的类静态属性引用的对象。
  • c.方法区中的常量引用的对象。
  • d.本地方法栈中JNI本地方法的引用对象。

 二、方法区回收

     Java方法区在Sun HotSpot虚拟机中被称为永久代,很多人认为该部分的内存是不用回收的,java虚拟机规范也没有对该部分内存的垃圾收集做规定,但是方法区中的废弃常量和无用的类还是需要回收以保证永久代不会发生内存溢出。

判断废弃常量的方法:如果常量池中的某个常量没有被任何引用所引用,则该常量是废弃常量。

判断无用的类:

  • a.该类的所有实例都已经被回收,即java堆中不存在该类的实例对象。
  • b.加载该类的类加载器已经被回收。
  • c.该类所对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射机制访问该类的方法。

虚拟机满足了上面的3个条件可以对类进行回收,不过并不是一定回收,是否对类进行回收可以通过下面参数配置

参数控制:

    -Xnoclassgc   这个参数配置可以决定对类的回收

  -verbose:calss  可以跟踪类的加载和卸载

   -XX:TraceClassLoading

   -XX:TraceClassUnloading

三、垃圾收集算法

    1.标记清除法(Mark-Sweep)

        标记清除算法将垃圾回收分成两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从跟节点开始的可达对象。因此没有被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有未被标记的对象。

      标记清除算法是问题:第一效率低,标记和清除效率都低;第二产生空间碎片,造成大量不连续的空间问题。

如图:

    2.复制算法

        为了解决效率问题,它将内存分为容量为大小相等的两块,每次只使用其中的一块,当这块内存快用完了,就将还存活这的对象复制到另外一窥按上面,然后把已经使用过的内存一次性清理,这样就不用关系内存碎片的问题。一般用于存活对象少、垃圾对象多的情况,一般是新生代

     问题:内存缩小为原来的一半

     如图:

    3.标记整理算法              

       标记整理算法是一种老年代的回算法。它在标记清除算法的基础上做了一些优化。和标记清除算法一样,标记整理算法也需要从根节点开始,对所有可达节点做一次标记,将所有存活对象压缩到内存的另外一端,并保持它们之间的引用关系。这样既避免了内存碎片,又不需要两块相同的内存空间。

一般老年代的垃圾回收需要时间比较长是因为,老年代中存活对象比较多,而使用标记压缩算法需要将所有的存活对象压缩到内存的一端。

如图:

    4.分代收集算法       

    当前的商业虚拟机的垃圾收集都采用这种算法,分代算法将内存区间根据对象的特点分成几块,根据每块内存区间的特点,使用不同的算法,以提高垃圾回收的效率。

一般来说新生代主要使用复制算法,老年代使用标记清除算法或者标记整理算法。

新生代中将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor,当回收时将还存活着的对象一次性复制到另外一块Survivor,最后清理掉Eden和刚才的Survivor。

HotSpot默认Eden和Survivor比例是8:1;也就是新生代中可用内存为整个新生代容量的90%。只要10%被浪费,当然当Survivor不够的时候就需要依赖老年代进行分配担保。

四、垃圾收集器

    现在没有任何一种垃圾收集器是万能的,所以对于不同的应用选择最合适的收集器是最好的。

总的垃圾收集器图:

上面的收集器Young genneration是新生代,下面的Tenured generation 是年老代,连了线的表示可以组合使用

新生代收集器:   

1.串行回收:serial收集器     

     Serial收集器作用域新生代中,采用复制算法、串行回收和 Stop-the-World机制的方式执行内存回收。Serial收集器缺省也作为HotSpot中Client模式下的新生代垃圾收集器

     Serial收集器还提供用于执行老年代垃圾收集的serialold 收集器。Serial old收集器同样也采用串行回收和stop-th-world机制,只不过内存回收算法采用的是标记-压缩算法。

    优点:如果是单个CPU的宿主环境中,使用serial收集器+serial old收集器的组合执行Client模式下的内存回收将会是不错的选择。基于串行回收的垃圾收集器适用于大多数对暂停时间要求不高的Client模式下的JVM,CPU不需要频繁地做任务切换,因此可以有效避免多线程交互过程中产生的一些额外开销。

    缺点:降低程序的吞吐量

    2.并行回收:ParNew收集器         

          是serial收集器的多线程版本

ParNew收集器在新生代中采用并行回收、复制算法和stop-the-world机制,可以和CMS和Serial Old(作为老年代)联合使用

优点:在多CPU的宿主环境下,可以充分利用多CPU、多核心等物理硬件资源优势、可以迅速的完成垃圾回收,提升程序的吞吐量。

    3.程序吞吐量优先:Parallel Scavenge收集器

和ParNew收集器一样,在新生代中采用并行回收、复制算法的收集器

Parallel收集器的主要优势是可以控制程序的吞吐量大小,因此它可以被为吞吐量优先的垃圾收集器。

        吞吐量 =运行用户代码时间/(运行用户代码时间+垃圾收集时间)

Parallel old收集器采用并行回收、标记-整理算法和stop-the-world机制,Parallel收集器+parallel  old收集器的组合执行Server模式下的内存回收将会是不错的选择。

  老年代收集器:

       4.单线程收集器 :Serial Old收集器

           它是Serial收集器的老年版本,同样是一个单线程收集器,使用“标记-整理”。可以和其他三个新生代的收集器联合使用

        也可以作为CMS老年代收集器的备用收集器

       5.多线程注重吞吐量的收集器:Parallel old收集器

           它是Paralle Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法,

        6.CMS收集器

       基于低延迟的考虑,JVM的设计者们提供了基于并行回收的CMS收集器,它是一款优秀的老年代垃圾收集器。CMS天生为并发而生,低延迟是它的优势,采用标记-清除算法,并且也会因为stop-the-world机制而出现短暂的暂停。

它标记清除分为四个阶段:

  • 初始标记阶段----标记是否可达,stop-the-world
  • 并发标记阶段-----将之前不可达的对象标记为垃圾对象
  • 重新标记阶段------由于上一阶段标记过程中,程序的工作线程会和垃圾收集器的线程同时运行或者交叉运行,因此在并发标记阶段无法保证之前被标记为垃圾的无用对象的引用关系遭到更改,因此再次标记阶段会采用stop-the-world机制而再次标记,系统也因此会再次出现短暂的暂停,以确保所有的垃圾对象都被成功且正确的标记。
  • 并发清除阶段:执行内存回收,释放掉无用对象所占用的内存空间。

区域化分代式:G1(Garbage-First)收集器

G1是一款基于并行和并发,低延迟,以及暂停时间更加可控的区域区域划分式垃圾收集器。

其革命性意义:

       在空间设计上,G1重新塑造了整个java堆区,虽然也是基于分代的概念执行内存分配和垃圾回收,但是G1并没有采用传统物理隔离的新生代和老年代布局方式(仅在逻辑上划分为新生代和老年代),而是将java堆区划分成约2048个大小相同的独立region块。每个region块之间是不连续的,,其大小根据堆空间的实际大小划分,整体被控制在1MB到32MB之间,这样划分的好处在于可以更好的提升GC的回收效率,和缩短stop-the-world机制的暂停时间以获取更大的程序吞吐量,甚至能够真正做到精确控制程序的的暂停时间等。由于G1收集器并非全堆扫描,只优于回收占用内存较大的一些region块,所以G1收集器的暂停时间会更加可控。

G1收集器的执行过程主要可以划分为6个阶段:

  • 初始标记阶段:stop-the-world,标记root-region
  • 根区域扫描阶段:扫描root-region中引用老年代的一些region块
  • 并发标记阶段:主要任务是找出整个java堆区中的存活对象,由于该阶段并不会导致程序出现暂停,因此在执行的过程中允许被新生代内存回收打断
  • 再次标记阶段:和初始标记阶段同样都是基于stop-the-world机制的,该阶段的只要任务就是完成整个堆区中存活对象的标记,
  • 清除阶段:由三部分构成,首先会计算出所有的活跃对象并完全释放一些自由度region块,然后处理Remember Set,这两部分操作将会暂停程序中的应用程序,然后并发重置空闲的一些Region块,将它们放回至空闲列表中。最后的拷贝阶段也是基于stop-the-world机制的
  • 拷贝阶段:该阶段的主要任务是将存活对象复制到未使用过的Region块

    

猜你喜欢

转载自blog.csdn.net/weixin_40792878/article/details/82192349