• JVM垃圾回收原理解析


    概述

    Java运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随着线程的而生,随线程而死,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收问题。而Java堆和方法区则不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一眼,我们只有在运行期,才能知道会创建的对象,这部分的内存分配和回收,是垃圾回收器所关注的。垃圾回收器需要完成三个问题:那些内存需要回收;什么时候回收以及如何回收。

    那些垃圾需要回收

    垃圾回收的基本思想是考察一个对象的可达性,即从根节点开始是否可以访问到这个对象,如果可以,则说明对象正在被使用,相反如果从根节点无法访问到这个对象,说明对象已经不再使用了,一般来说此对象就是需要被回收的。这个算法为根搜索算法。

    可达性分析

    但是实际中,一个不可达的对象有可能在某种条件下“复活”自己,那么对它的回收就是不合理的。为此给出一个对象可达性状态的定义,并规定了在什么状态下可以安全的回收对象。可达性对象包含了以下三种状态。

    可达的:从根节点开始,按照引用节点,可以搜索到这个对象

    可复活的:对象的所有引用都被释放,但是对象可能在finalize()方法中复活自己。

    不可达的:对象的finalize()方法被调用,并且没有复活,那么就进入不可达状态。不可达的对象不可能会被“复活”,因为finalize()方法只能调用一次。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    /**
     *
     * <p>Description: 1.对象被GC时,可以通过finalize拯救 2.finalize只被调用一次 </p>
     * @date 2019年8月25日
     * @version 1.0
     */
    public class FinalizeTest {
     
      private static FinalizeTest currentObj;
     
      @Override
      protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize invoke");
        //重新引用
        currentObj = this;
      }
     
      public void alive() {
        System.out.println("live");
      }
     
      public static void main(String[] args) throws InterruptedException {
        currentObj = new FinalizeTest();
     
        currentObj = null;
        System.gc();
        //finalize优先级地,先等待
        Thread.sleep(500);
        if(currentObj == null) {
          System.out.println("dead");
        }else {
          currentObj.alive();
        }
     
        currentObj = null;
        System.gc();
        //finalize优先级地,先等待
        Thread.sleep(500);
        if(currentObj == null) {
          System.out.println("dead");
        }else {
          currentObj.alive();
        }
      }
    }

    上面代码有一处一样的断码片段,但是得到的结果却并不相同,一次对象“拯救复活”成功,另一次失败,那么就可以被正常回收。

    可以作为GC Roots包括下面几种:

    • 虚拟机栈(栈帧中的本地表量表)中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中常量引用的对象
    • 本地方法栈中JNI引用(即一般Native的方法)的对象

    四种引用类型

    在JDK1.2之后对引用进行了扩充,分为强引用,软引用,弱引用,虚引用4种,这四种强度一次减弱。通过对引用的扩充,可以依据内存的使用来描述这样的对象:当内存足够,则保留内存中;如果内存空间进行垃圾回收后还是很紧张,则可以抛弃这类对象。很多系统的缓存功能符合这样的应用场景。

    强引用

    在Java中最常见的就是强引用, 把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。

    软引用

    ​ 软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。

    弱引用

    ​ 弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管 JVM 的内存空间是否足够,总会回收该对象占用的内存。

    虚引用

    ​ 虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。 虚引用的主要作用是跟踪对象被垃圾回收的状态。

    什时候回收

    按HotSpot VM的serial GC的实现来看触发条件主要分为以下几种:

    • young GC:当young gen中的eden区分配满的时候触发。注意young GC中有部分存活对象会晋升到old gen,所以young GC后old gen的占用量通常会有所升高。
    • full GC:当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

    HotSpot VM里其它非并发GC的触发条件复杂一些,不过大致的原理与上面说的其实一样。并发GC的触发条件就不太一样。以CMS GC为例,它主要是定时去检查old gen的使用量,当使用量超过了触发比例就会启动一次CMS GC,对old gen做并发收集。

    如何回收

    如何回收主要就涉及到垃圾回收的算法了。下面介绍几种垃圾回收算法的思想。

    标记清除法(Mark-Sweep)

    标记清除算法是现代垃圾回收算法的思想基础。它主要分为两个阶段:标记阶段和清除阶段。在标记阶段,首先通过根节点,标记所有从根节点开始的可达队对象,因此未被标记的对象就是未被引用的垃圾对象。然后在清除阶段,清除所有的未被标记的对象。

    标记清除算法的不足有:效率的问题和标记清除后产生的大量不连续的内存碎片。而内存碎片太多可能会导致在分配大对象时,无法找到连续的内存而不得不提前触发另外一次垃圾回收。

    复制算法(Coping)

    复制算法的核心思想是:将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中存活对象复制到未使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

    如果系统中的待回收的对象很多,复制算法需要复制的存活对象就会相对较少,真正的垃圾回收时刻,复制算法的效率就会很高。而且对象是在垃圾回收过程中的,统一复制到新的内存空间,再清除原来使用的内存,因此可以确保回收后的内存空间是没有碎片的。但是另一方面,复制算法的代价是需要使用更多的内存空间。

    复制算法比较适用于新生代。因为新生代垃圾对象通常多余存活对象,复制算法的效率会比较高。

    标记整理算法(Mark Compact)

    在老年代,大部分的对象都是存活对象。如果依然用复制算法,由于存活的对象多,复制的成本也将提高。因此基于老年代的垃圾回收特性,需要使用其他的算法。标记整理算法是一种老年代的回收算法。它在标记算法的基础上做了一些优化。和标记清除算法一样,它也是从更节点开始,但是并不是清除未标记的对象,而是将存活的对象压缩到内存的一边,之后清除边界外所有空间。这种方法避免了碎片的产生,又不需要过多的内存空间,因此性价比比较高。

    标记整理法的最终效果等同于标记清除算法执行完成后,再进行一次内存碎片的整理,因此也可以把它称为标记清除整理(MarkSweepComact)。

    分代算法(Generational Collecting)

    分代算法是根据对象存活周期不同将内存化为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最合适的收集算法。新生代中的特点是对象朝生夕死,大约90%的新建对象会被回收,因此新生代适合复制算法。当一个对象经过几次回收后依然存活,对象就会被放入老年代的内存空间。在老年代中可以认为对象在一段时间内,甚至在程序的整个生命周期,是常驻内存的,可以对老年代使用标记清除和标记整理算法。

    对于新生代和老年代来说,通常新生代的回收频率很高,但是每次回收的耗时都很短,而老年代回收的频率比较低,但是会消耗更多的时间。

    分区算法(Region)

    一般来说,相同条件下,堆空间越大,一次GC所需要的事件越长,从而产生的停顿也越长。为了更好的靠之停顿时间,将一块大的内存区域分割成多个大小形同的小区域,依据目标的停顿时间,每次回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。分区算法是将整个堆空间划分为连续的不同小区间。每个小区间独立使用,独立回收。

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

  • 相关阅读:
    02 基本介绍
    01 概述 网络分层
    04 可扩展
    Java 注解2
    03 高可用
    重拾安卓_00_资源帖
    JavaUtil_09_通用工具类-01_Hutool
    java支付宝开发-01-沙箱环境接入
    java支付宝开发-00-资源帖
    svn_学习_01_TortoiseSVN使用教程
  • 原文地址:https://www.cnblogs.com/1549983239yifeng/p/14673433.html
Copyright © 2020-2023  润新知