Java JVM垃圾回收机制详解

一.前言

       这是一个面试经常会问到的问题,如果面试管问到JVM或者垃圾回收机制时,不妨先说下它的技术背景与意义,这样介绍会让面试官决定是真的在理解与学习这个知识,而不是死记硬背,而且使整段谈话总——分——总那么就用下面这句话,和这篇博客的技术背景来当个开头:

       1.1 看视频学习时,黑马程序员老师的一段话:“可以说,我们Java语言能发展的这么繁荣,也主要得益于运行时数据区的优良自治,它对自己的内存管理有一套非常非常优良的管理机制”。

   1.2 按照套路是要先装装X,谈谈JVM垃圾回收的前世今生的。说起垃圾回收(GC),大部分人都把这项技术当做Java语言的伴生产物。事实上,GC的历史比Java久远,早在1960年Lisp这门语言中就使用了内存动态分配和垃圾回收技术。设计和优化C++这门语言的专家们要长点心啦~~

       (上面这句话原自这篇博客,个人觉得思路非常好,适合这样来回答面试,逻辑清晰:https://www.cnblogs.com/1024Community/p/honery.html)

二. 垃圾回收前需要思考的3件事情(何时,何地,怎么做)

       垃圾回收时需要思考这样3件事情:哪些内存需要回收,什么时候回收,如何回收,即何时,何地,怎么做。本文后续会讲到哪些内存是我们关注的重点,这些内存中什么样的属于要回收的垃圾,怎么判断,以及回收垃圾的算法,以及GC(garbage collection)触发的时间。
(如果面试时越到这样的问题,也先这样总结一下,总分总,这样有条理,面试官也知道接下来你要讲哪些内容了)

三. 哪些内存需要回收

        JVM的内存结构包括5大区域:程序计数器,虚拟机栈,本地方法栈,堆区,方法区。 其中程序计数器,虚拟机栈,本地方法栈这3个区域随线程而生,随线程而灭,因此这几个区域的内存分配和回收都具备确定性,不需要过多的考虑回收的问题,因为方法结束或者线程结束时,内存自然就跟随着回收了。
       而Java堆区和方法区则不一样!!!这两块区域具有很显著的不确定性:只有在程序运行期间,我们才能知道程序究竟会创建哪些对象,会创建多少个对象,这部分的内存的分配和回收是动态的!这部分的内存的分配和回收是动态的!这部分的内存的分配和回收是动态的!
(重要的事情说3遍)

四. 关于堆,如何判断垃圾

        对于堆:堆中主要存在的是对象实例,堆中要回收的垃圾就是指那些“死亡”的对象(死亡,既当一个对象是不可能再被任何途径使用的对象了,也即一个对象没有被其他对象所引用,即代表这个对象“死去”,即为垃圾)。在对堆进行回收前,需要判断哪些对象以及“死去”,哪些还“活着”,主要通过一下两种主要算法判断:

4.1 引用计数法

       引用及算法是垃圾收集器种的早期策略,就是给对象种添加一个引用计数器,每当有一个地方引用这个对象时,计算器就加1,引用失效时,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。但是当两个对象仅仅相互引用,计数器不为0,但它们无其他别的引用,不可能再被访问了,因计数器不为0,也无法回收。
       引用计数法的优缺点如下:

优点: 执行效率高,程序执行受影响较小
缺点:无法检测出循环引用的情况,导致内存泄露

4.2 可达性分析算法

       可达性分析算法是从离散数学种的图论引入的,程序把所有的引用关系看作是一张图,通过一系列的称为“GC Roots”的对象作为起点,从这些结点开始向下搜索,结点所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的。但这里也有特殊情况,即使在可达性分析算法中有不可达的对象,也并非“非死不可”,这时候他们暂时处于“缓刑”的状态,要正真的宣告一个对象死亡,至少要经历两次标记过程:

       第一次标记:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记;
       第二次标记:第一次标记后接着会进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。在finalize()方法中没有重新与引用链建立关联关系的,将被进行第二次标记。

第二次标记成功的对象将真的会被回收,如果对象在finalize()方法中重新与引用链建立了关联关系,那么将会逃离本次回收,继续存活。有点跟不上了。
  
       那么哪些对象可以作为GC Roots呢?如下:
                 虚拟机栈中的引用对象
                 方法区中的常量引用对象
                 方法区中的类静态属性引用对象
                 本地方法栈中的引用对象
                 活跃线程中的引用对象
       可达性算法的分析如下图:

在这里插入图片描述

4.3 一直提到对象的引用,那么关于Java中的引用你了解多少

(这个面试的可说可不说吧,因为引用计数算法和可达性分析算法都是针对强引用而言的
        无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,来判断对象是否“存活”,这都和“引用”有关。在java中,讲引用又分为强引用,软引用,弱引用,虚引用这四种类型,这四种引用程度依次递减。
        a) 强引用
        在java代码中普遍存在的,类似Object obj=new Object()这种类型的引用。只要强引用还在,垃圾收集器永远不会回收掉被引用的对象
        b) 软引用
        用来描述一些有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出之前,会把这些对象列进回收范围之中进行二次回收。如果这次回收后还没有足够的内存,才会抛出内存异常。
        c) 弱引用
        也是用来描述非必需对象的,但是它的强度更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象。
        d) 虚引用
        是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例,它的作用是能在这个对象被收集器回收时收到一个系统通知。

五. 关于方法区,如何判断垃圾(废弃常量和无用的类)

(如何判断废弃常量和无用的类,就先不记了,有点绕)

       《Java虚拟机规范》中提到可以不要求虚拟机在方法区中实现垃圾收集。方法区垃圾收集的性价比很低。方法区主要回收的内容有:废弃常量和无用的类。对于废弃常量也可用引用的可达性来判断,对于无用的类,需要同时满足下面3个条件:
       该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
       加载该类的ClassLoader已经被回收;
       该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

六. 垃圾收集算法
6.1 标记—清除算法(老年代回收算法)

       该算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后,统一回收所有被标记的对象(反之,标记存活对象,回收未被标记的也行)。

        标记和清除算法也是优缺点并存:
                1,优点:只有两个步骤,标记和清除,执行起来比较容易。
                2,缺点:标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致在下次分配较大的对象时,无法找到足够连续的内存而不得不提前出发另一次的垃圾收集。针对内存碎片化,后面又提出了标记—整理算法。
在这里插入图片描述

6.2 复制算法(新生代回收算法:Appel式回收,这部分内容放到分代收集里讲解)

        针对上述最基础的标记—清除算法的效率问题,提出了复制算法。它将可用内存按照容量划分为大小相等的两块,每次只使用其中的一块。当这块内存中需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后把已经使用过的内存区域一次全部清理掉,这样做的好处是每次都是对整个半区进行内存回收,内存分配时就不需要考虑内存碎片等复杂的情况。但缺点显而易见:将可用内用缩小为原来的一半(因为每次都要预留一半出来),空间浪费未免太多了点。所以针对这种1:1的不足,后面提出了更好的分配方式,即Appel式回收。在分代收集里面会提到。复制算法如下图(有点看不清,回收前的图里右半边时预留区域,回收后的图里左半边时被清空的):
在这里插入图片描述

6.3 标记—整理算法(老年代回收算法)

        标记—整理算法与标记清除算法非常相似,但是解决了内存碎片化的问题。标记—整理算法采用标记—清除一样的方式进行对象的标记,但不直接对可回收对象进行整理,而是让所有的可用对象都移向一端,然后直接清理掉边界线以外的内存。

       这种移动式的算法也是优缺点并存的**:解决了内存碎片化的问题。但是其成本更高:如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,移动对象并更新会是一种极为负重的操作,而且这种对象移动操作必须全程暂停用户应用程序才能进行。这种停顿也被最初的虚拟机设计者描述为“Stop The World”,即“SWT”。**
在这里插入图片描述

6.4 分代收集算法(现在大部分JVM垃圾收集器所采用的算法)

       分代收集算法的核心思想是根据对象存活的生命周期将内存划分若干个不同都得区域,不同的区域采用不同的回收算法。一般情况下将堆区划分为老年代和新生代,然后还有永久代,永久代在方法区。新生代又进一步划分为Eden和Survivor区,Survivor区又由From区和To区组成。

6.4.1 新生代的回收算法

        a) 所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。
        b) 新生代中,每次垃圾回收都有大批的对象死去,少量的存活,因此采用 复制算法,复制哪些少量存活的对象,这样效率比较高。
        c)针对上述复制算法1:1这种内存分配情况,又提出了“Appel式回收”。现在HotSpot虚拟机的Serial,ParNew均采用了这种策略来设计新代的内存布局。因为新生代中百分之98的对象都是“朝生夕灭”的,所以Appel式回收的具体做法是按照8:1:1(能活下来的比较少,Survivor区不用很大)把新生代分为一块较大的Eden(伊甸园)空间和两块较小的Survivor空间(这两块Survivor区域一块称为From区,一块称为To区)。大部分对象在Eden区中生成,回收时先将Eden区存活的对象复制From区,然后清空Eden区,下一次回收时,将Eden区和From区存活的对象放到To区,然后统一清理Eden区和From区,再下一次清理,再由To区转移到From区。新生代的垃圾回收就是在两个Survivor区(始终保证一个Survivor区是空的)之间相互复制存活对象,直到Survivor区满为止。(上述的复制,均指复制算法)。具体复制流程如下图(来源于黑马视频(https://www.bilibili.com/video/BV1AE411E7uj?p=18)视频18)
        d) 如果一块Survivor空间没有足够空间存放上一次新生待手机下来的存活对象,这些对象便会通过分配担保机制直接进入老年代,这对虚拟机来说时安全的。
        e) 新生代发生的GC也叫做Minor GC([ˈmaɪnər],发音一定要标准,英语真重要),当Eden区满了后触发Minor GC。Minor GC发生的频率比较高

回收之前:
在这里插入图片描述
复制时:
在这里插入图片描述
回收之后:
在这里插入图片描述

6.4.2 老年代的回收算法

        a) 在新生代经历了N次(默认是15次)垃圾回收后依然存活的对象,就会被放到老年代。老年代中存放的都是一些生命周期比较长的对象。
        b) 老年代中对象存活率高,没有额外的空间对它们分配担保需要采用”标记—清理算法“或者”标记整理算法“
        c) 老年代的内存大概是 新生代的一倍,老年代发生的GC称为Full GC,也称作Major GC。 但是注意Full GC不只是针对老年代的GC,而是堆所有代,包括新生代,老年代,持久代都进行垃圾回收。

6.4.3 持久代的回收算法 持久代的即方法区,见第6节,一般不怎么讨论。

七. GC是什么时候触发的呢?

        由于对象进行了分代处理,因此垃圾回收的区域和时间都不一样,那么Minor GC和Full GC什么时候触发呢?(Full GC理所当然耗时更多)

7.1 Minor GC(看到很多地方都写Scavenge GC)

        当Eden区满了后触发新生代的Minor GC,将Eden区和非空闲的Survivor区存活的对象复制到另一个空闲的Survivor区当中,并清理Eden和非空闲区。

7.1 Full GC

        注意:Full GC不只是针对老年代的GC,而是堆所有代,包括新生代,老年代,持久代都进行垃圾回收。
        Full GC的触发有4种情况(简单说一下,因具体情况涉及和不同的垃圾收集器有关):
        a) Survivor区满了后就通过Minor GC将对象复制到老年代。老年代也满了的话,就触发Full GC,针对所有代垃圾回收。
        b) 直接调用System.gc()
        c) 如果持久代满了,触发Full GC
        d) 上一次GC后,Heap的各域分配策略动态变化。(heap,[hiːp],堆,发音一定要标准)。

八. 常见的垃圾收集器

        文章的最后说一下,简单的介绍一下常见的垃圾收集器,具体工作细节有点难以记忆,所以就不写出来了:
        a) Serial收集器:新生代收集器,单线程,复制算法。
        b) Serial Old收集器:老年代收集器,单线程,标记—整理算法。
        c) ParNew收集器:新生代,可以看成是Serial的多线程版本
        d) Parellel Scavenge/Parallel Old收集器:并行收集器,分布针对新生代/老年代
        e) CMS收集器:应用最广,现在的主流。高并发,低停顿,追求最短GC回收停顿时间,cpu占用率高等

九.知识点汇总图

在这里插入图片描述

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值