美文网首页
Java 内存回收算法

Java 内存回收算法

作者: wuchao226 | 来源:发表于2019-08-06 17:22 被阅读0次

    一、判断Java中对象存活的算法

    1、引用计数器算法:

    Java 堆 中每个具体对象(不是引用)都有一个引用计数器。当一个对象被创建并初始化赋值后,该变量计数设置为1。每当有一个地方引用它时,计数器值就加1。当引用失效时,即一个对象的某个引用超过了生命周期(出作用域后)或者被设置为一个新值时,计数器值就减1。任何引用计数为0的对象可以被当作垃圾收集。当一个对象被垃圾收集时,它引用的任何对象计数减1。

    • 优点:
      引用计数收集器执行简单,判定效率高,交织在程序运行中。对程序不被长时间打断的实时环境比较有利。
    • 缺点:
      难以检测出对象之间的循环引用(A对象引用B对象,B对象又引用A对象,但是A,B对象已不被任何其他对象引用)。同时,引用计数器增加了程序执行的开销。所以在JDK1.1之后,这个算法已经不再使用了。
    2、可达性分析算法

    可达性分析算法也叫根搜索算法,通过一系列的称为 GC Roots 的对象作为起点,然后向下搜索。搜索所走过的路径称为引用链 (Reference Chain), 当一个对象到 GC Roots没有任何引用链相连时, 即该对象不可达,也就说明此对象是 不可用的。

    Java中, 可作为GC Roots的对象包括以下四种:

    1. 虚拟机栈(栈帧中的本地变量表)中引用的对象
    2. 方法区中静态引用指向的对象。也就是类中的 static 修饰的变量所引用的对象
    3. 方法区常量引用的对象
    4. 本地方法栈JNINative方法)的引用的对象
    5. 仍处于存活状态中的线程对象

    如下图所示: Object5、Object6、Object7 虽然互有关联, 但它们到GC Roots是不可达的, 因此也会被判定为可回收的对象。

    二、对象引用分类

    1. 强引用(Strong Reference)
    在代码中普遍存在的,类似Object obj = new Object()这类引用,只要强引用还在,垃圾收集器永远不会回收掉被引用的对象。

    2. 软引用(Sofe Reference)
    有用但并非必需 的对象,可用SoftReference类来实现软引用。在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

    3. 弱引用(Weak Reference)
    非必需的对象,但它的强度比软引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,JDK提供了WeakReference类来实现弱引用。无论当前内存是否足够,用软引用相关联的对象都会被回收掉。

    4. 虚引用(Phantom Reference)
    虚引用也称为幽灵引用或幻影引用,是最弱的一种引用关系,JDK提供了PhantomReference类来实现虚引用。为一个对象设置虚引用的唯一目的是:能在这个对象在垃圾回收器回收时收到一个系统通知。

    三、JVM 垃圾回收算法

    1、标记-清除算法

    标记-清除(Mark-Sweep)算法是现代垃圾回收算法的思想基础。标记-清除算法将垃圾回收分为两个阶段:标记阶段和清除阶段。一种可行的实现是,在标记阶段,首先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象(好多资料说标记出要回收的对象,其实明白大概意思就可以了)。然后,在清除阶段,清除所有未被标记的对象。

    • 优点:
      实现简单,不需要进行对象进行移动。

    • 缺点:
      标记、清除过程效率低,产生大量不连续的内存碎片,提高了垃圾回收的频率。

    如图:


    2、标记整理算法

    标记整理算法 采用和 标记-清除算法 一样的方式进行对象的标记,但后续不直接对可回收对象进行清理,而是将所有的存活对象往一端空闲空间移动,然后清理掉端边界以外的内存空间。

    • 优点:
      解决了标记-清理算法存在的内存碎片问题。
      没有内存碎片后,对象创建内存分配也更快速了(可以使用TLAB进行分配)。

    • 缺点:
      效率问题,(同标记清除算法)标记和整理两个过程的效率都不高;
      仍需要进行局部对象移动,一定程度上降低了效率。

    如图:

    3、复制算法

    复制算法可以解决效率问题,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块,当这一块内存用完了,就将还存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉,这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可(还可使用TLAB进行高效分配内存)。

    • 优点:
      效率高,没有内存碎片(按顺序分配内存即可,实现简单、运行高效,不用考虑内存碎片。)
    • 缺点:
      可用的内存大小缩小为原来的一半,对象存活率高时会频繁进行复制,效率将会变低。

    如图:

    图的上半部分是未回收前的内存区域,图的下半部分是回收后的内存区域。通过图,我们发现不管回收前还是回收后都有一半的空间未被利用。

    4、分代收集算法

    当前商业虚拟机都是采用分代收集算法,它根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为年轻代、老年代 和 永久代。然后根据各个年代的特点采用最适当的收集算法,在新生代中,每次垃圾收集都发现有大批对象死去,只有少量存活,就选用复制算法,而老年代因为对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记清理”或者“标记整理”算法来进行回收。

    如图:


    图的左半部分是未回收前的内存区域,右半部分是回收后的内存区域。

    对象分配策略:

    1. 对象优先在Eden区域分配,如果对象过大直接分配到Old区域。
    2. 长时间存活的对象进入到Old区域。

    改进复制算法:
    现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor 。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
    HotSpot 虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。

    年轻代、老年代 和 永久代,如下图所示:

    图1
    新生代(Young generation)

    绝大多数最新被创建的对象会被分配到这里,由于大部分对象在创建后会很快变得不可达,所以很多对象被创建在新生代,然后消失。对象从这个区域消失的过程我们称之为 minor GC

    新生代 中存在一个Eden区和两个Survivor区。新对象会首先分配在Eden中(如果新对象过大,会直接分配在老年代中)。在GC中,Eden中的对象会被移动到Survivor中,直至对象满足一定的年纪(定义为熬过GC的次数),会被移动到老年代

    可以设置新生代老年代的相对大小。这种方式的优点是新生代大小会随着整个大小动态扩展。参数 -XX:NewRatio 设置老年代新生代的比例。例如 -XX:NewRatio=8 指定 老年代/新生代8/1. 老年代占堆大小的 7/8新生代 占堆大小的 1/8(默认即是 1/8)。
    例如:

    -XX:NewSize=64m -XX:MaxNewSize=1024m -XX:NewRatio=8
    
    老年代(Old generation)

    对象没有变得不可达,并且从新生代中存活下来,会被拷贝到这里。其所占用的空间要比新生代多。也正由于其相对较大的空间,发生在老年代上的GC要比新生代要少得多。对象从老年代中消失的过程,可以称之为major GC(或者full GC)。

    永久代(permanent generation)

    像一些类的层级信息方法数据方法信息(如字节码变量大小),运行时常量池(JDK7之后移出永久代),已确定的符号引用虚方法表等等。它们几乎都是静态的并且很少被卸载和回收,在JDK8之前的HotSpot虚拟机中,类的这些永久的 数据存放在一个叫做永久代的区域。

    永久代一段连续的内存空间,我们在JVM启动之前可以通过设置-XX:MaxPermSize的值来控制永久代的大小。但是JDK8之后取消了永久代,这些元数据被移到了一个与堆不相连的称为元空间 (Metaspace) 的本地内存区域

    小结
    JDK8堆内存一般是划分为年轻代老年代不同年代 根据自身特性采用不同的垃圾收集算法

    对于新生代,每次GC时都有大量的对象死亡,只有少量对象存活。考虑到复制成本低,适合采用复制算法。因此有了From SurvivorTo Survivor区域。

    对于老年代,因为对象存活率高,没有额外的内存空间对它进行担保。因而适合采用标记-清理算法标记-整理算法进行回收

    参考:

    JVM垃圾回收算法
    JVM系列(五) - JVM垃圾回收算法

    相关文章

      网友评论

          本文标题:Java 内存回收算法

          本文链接:https://www.haomeiwen.com/subject/zmwidctx.html