垃圾收集器与内存分配策略之篇一:简要概述和垃圾收集算法

Posted 烟尘

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了垃圾收集器与内存分配策略之篇一:简要概述和垃圾收集算法相关的知识,希望对你有一定的参考价值。

一、了解垃圾收集的意义

   经过半个多世纪的发展,目前内存的动态分配与内存回收技术已经相当成熟,一切看起来已经进入了"自动化时代",那么我们为什么还要研究和去了解GC和内存分配呢。答案很简单:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时候,我们就需要对这些自动化的技术实施必要的监控和调节。

二、如何判断对象是否可回收

  1.引用计数法

     引用计数法就是给对象添加一个引用计数器,每当有一个地方引用它,计数器值就加1;当引用失效的时候,计数器的值就减1;任何时刻计数器的值为0就是不可用的。客观的说,引用计数法实现简单,判定效率高,在大部门情况下面是一个不错的算法,在Python语言里面有使用,但是java虚拟机里面并没有使用这种算法来管理内存,因为它很那解决对象之间循环引用的问题。

    下面写一组代码案例来加以说明:

    

 

运行结果:

[GC[DefNew: 2884K->441K(4928K), 0.0048536 secs] 2884K->2489K(15872K), 0.0049418 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]

[Full GC[Tenured: 2048K->439K(10944K), 0.0056467 secs] 4537K->439K(15872K), [Perm : 1678K->1678K(12288K)], 0.0057194 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

Heap

 def new generation   total 4992K, used 180K [0x04800000, 0x04d60000, 0x09d50000)

  eden space 4480K,   4% used [0x04800000, 0x0482d370, 0x04c60000)

  from space 512K,   0% used [0x04c60000, 0x04c60000, 0x04ce0000)

  to   space 512K,   0% used [0x04ce0000, 0x04ce0000, 0x04d60000)

 tenured generation   total 10944K, used 439K [0x09d50000, 0x0a800000, 0x14800000)

   the space 10944K,   4% used [0x09d50000, 0x09dbdf30, 0x09dbe000, 0x0a800000)

 compacting perm gen  total 12288K, used 1685K [0x14800000, 0x15400000, 0x18800000)

   the space 12288K,  13% used [0x14800000, 0x149a5588, 0x149a5600, 0x15400000)

No shared spaces configured.

ERROR: JDWP Unable to get JNI 1.2 environment, jvm->GetEnv() return code = -2

JDWP exit error AGENT_ERROR_NO_JNI_ENV(183):  [../../../src/share/back/util.c:838]

可以看到在GC前后内存发生了变化,证明了java虚拟机并没有使用引用计数法。

 2.可达性分析算法

  可达性分析算法的基本思路是:从一个根节点作为起始点,然后从这些根节点向下搜索,搜索所走过的路径成为引用链,当一个对象到达根节点没有任何引用链相连的话,也就是说该对象不可达,则证明此对象是不可用的,GC则会进行内存回收。

如下图 GC Roots作为根节点,对象object5, object6, object7之间虽然有关联,但是因为他和GC Roots不可达,所以被判定为可回收对象。

 

 

 

 

3.四中引用的概念说明

   强引用:是指在程序代码中普遍存在的,类似于Object obj = new Object()这类的引用,只要是强引用存在,垃圾回收器是永远都不能回收的对象。

   软引用:是用来描述一些还有用但不是必需的对象。对于软引用关联的对象,在系统中将要发生内存溢出异常之前,将会把这类对象列进回收范围进行二次的回收。如果二次回收以后,还没有足够的内存,才会抛出内存溢出异常。

   弱引用:也是用来描述非必须对象,但是他的强度只是比软引用更弱一些,被引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论内存是否足够,都会回收掉这些弱引用对象。

   虚引用:他是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间产生任何的影响,也无法通过一个虚引用来获取一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被垃圾收集器回收的时候收到一个系统通知。

4.对象的生存还是死亡

  即使在可达性分析算法中是不可达对象,也并非是非死不可的对象。要宣告一个对象死亡要经历两次标记过程:如果对象在进行可达性分析以后发现没有与GC Roots相连接的引用链,那么他会被第一次标记并进行一次筛选,筛选的条件是次对象有必要执行finalize()方法。当对象没有覆盖finalize()方法的时候或者finalize()方法已经被虚拟机执行过,虚拟机将这2中情况都视为没有必要执行。finalize()方法只会被系统调用一次。

   如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会放在一个叫做F-Quequ的队列之中,并在稍后有一个虚拟机自动建立的、低优先级的Finalize()线程去执行他。这里所谓的执行是指虚拟机会触发这个方法,但不会承诺会等待他执行结束,这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,那么很可能会导致F-Queue队列中的其他对象处于永久的等待,甚至导致整个内存回收系统的崩溃。finalize()方法是对象逃脱死亡的命运的最后一次机会,稍后GC则会对F-Queue队列中的对象进行第二次标记,如果对象能够在重新与引用链中任何一个对象建立关联就可以成功的拯救自己,譬如把自己(this关键字)赋值给类变量或者自己成员变量的某一个属性。那么他在第二次标记的时候就会被移除即将回收的集合,但是如果这个阶段没有被移除即将回收的集合,那么他基本上就被回收了。

通过以下实例代码进行说明:

 
package com.gc.demp;

public class FianlizeEscapeGC {

         public static FianlizeEscapeGC SAVE_SIGN = null;

         public void isAlive(){

                   System.out.println("yes , I am still alive");

         }

         protected void finalize() throws Throwable{

                   //调用finalize 方法 只能被系统调用一次

                   super.finalize();

                   System.out.println(" finalize method executed ");

             //将自己赋值给类变量 在finalize的时候可以拯救自己

                   FianlizeEscapeGC.SAVE_SIGN = this;

         }

         public static void main(String[] args) throws Exception {

                   SAVE_SIGN = new FianlizeEscapeGC();

                   //因为finalize方法只会被系统调用一次 所以第一次拯救成功了

                   SAVE_SIGN = null;

                   System.gc();

                   //因为finalize的优先级比较低 所以暂停0.5秒来等待他

                   Thread.sleep(500);

                    if (SAVE_SIGN !=null){

                             SAVE_SIGN.isAlive();

                    }else {

                             System.out.println("no , I am  dead ");

                    }

                    //这段代码和上面的一样 但是第二次却失败了

             SAVE_SIGN = null;

                   System.gc();

                   //因为finalize的优先级比较低 所以暂停0.5秒来等待他

                   Thread.sleep(500);

                    if (SAVE_SIGN !=null){

                             SAVE_SIGN.isAlive();

                     }else {

                             System.out.println("no , I am  dead ");

                    }

         }

}
 

 

运行结果:

 

 

 需要特别说明是的finalize()方法是java诞生之初为了C++程序员为了能接受他而做的一种妥协,他的运行成本很高,并不建议使用它来拯救对象。

5.回收方法区

   方法区中主要是回收一些废弃的常量和无用的类。

判断无用类的标准:

该类的所有的实例都已经回收,也就是java堆中不存在该类的任何实例。

加载该类的ClassLoader已经回收。

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

虚拟机可以对满足上述标准的无用类进行回收。

三、Hotspot如何发起内存回收

https://www.cnblogs.com/jing99/p/6071808.html

四、垃圾收集算法:

1.标记清除算法

 最基础的算法是标记清除算法.标记清除算法分为两个阶段,标记和清除阶段。首先,先标记处来所有需要回收的对象,在都是标记完成后统一回收所有被标记的对象。之所以说他是最基础的算法,那是因为后续的收集算法都是基于这种思路并对其不足而进行改进的。他的不足主要有两个:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间

 

2.复制算法

   为了解决效率问题,一种被称为复制的收集算法就出现了,他将可用内存按容量划分为大小相同的两块,每一次只使用其中的一块。当这一块的内存用完了,就将还存活的对象复制到另一块上面去,然后再把已使用的内存空间进行一次清理。这样使得每次都是对整个半区进行内存回收,内存分配时候也就不用考虑内存碎片的问题等复杂的情况。

缺点:将内存缩小为原来的一半,内存变小。如果不想浪费50%的空间,就需要额外的空间进行担保,以对象内存中的对象100%存活这种极端情况。

优点:不用考虑空间碎片的问题。

 

 

3.标记整理算法

 根据老年代的特点,提出了标记整理算法。

标记过程和标记清除过程一样,但后续步骤不是直接对可回收对象进行直接清理,而是让所有存活对象像一段移动,然后直接清理掉端边界以外的内存。

 

 

4.分代收集

根据对象不同的存活周期将内存划分为几块。一般是把java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中每天都有大量的对象死去,只有少量存活,那就采用复制算法。在老年代中存活率比较高,没有额外的空间对他进行担保,就必须使用标记清理或者标记整理算法来对他进行收集。

以上是关于垃圾收集器与内存分配策略之篇一:简要概述和垃圾收集算法的主要内容,如果未能解决你的问题,请参考以下文章

垃圾收集器与内存分配策略之篇三:理解GC日志和垃圾收集器参数总结

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

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

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

垃圾收集器与内存分配策略 (深入理解JVM二)

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