史上最全面详细的JVM垃圾回收讲解


前言

  作为一名合格程序猿,JVM的知识已经是我们必知必会的知识了。如果还有小伙伴已经忘了,可以看一下我之前 的博客快速的回顾一下JVM知识——“JVM面试都被问烂了,你还不懂吗?”。我们都知道Java与C/C++比较大的区别之一就是Java 的自动内存管理,而Java的自动内存管理主要是针对对象内存的回收和对象内存的分配。同时,Java 自动内存管理最核心的功能是堆内存中对象的分配与回收。今天我们就来聊聊Java的垃圾回收(简称GC)。

一、问题引入

张三:老哥,OOM了,怎么办,赶紧帮我看看!

  你是否也像张三一样遇到OOM,你是否也像张三一样遇到OOM手足无措?今天我就带你从问题出发,一步一步的剖析问题产生的根源,让你成为张三口中的老哥。我们先来看一个简单的OOM例子:

package gc;

public class OOM
{
    public static void main(String[] args)
    {
        String s = "";
        while(true)
        {
            s+=s+"888888888888888888888888888888888";
        }
    }
}

最后运行程序,意料之中,OOM出来了。。。。并且它提示我们Java heap space,是我们的堆内存溢出了。
在这里插入图片描述

二、JVM垃圾回收

1、堆内存分析

  在分析OOM之前,我们先分析存放垃圾的容器。在我之前博客JVM中讲到:Java的垃圾主要在堆中产生,因此我们需要详细分析一下JVM的堆内存(如下图)。
在这里插入图片描述
通过上图可看到堆区主要分为三个部分:年轻代、老年代、元空间。


1.1 年轻代

  年轻代主要是用来存放新产生的对象,它分为三个部分:Eden(伊甸园区)、Survivor(幸存区)的0区和1区(又被称为from区和to区)。由于年轻代频繁的创建对象,因此当Eden内存不足的时候,这块区域会触发一次MinorGC进行垃圾回收。

  • Eden(伊甸园区):在大部分情况下,对象都会首先在Eden区域分配(“因此没对象的可以赶紧下手了,国家已经不分配对象了,只能靠自己了” )。那特殊情况呢?特殊情况我们下面再讲。
  • Survivor(幸存区):因为年轻代会频繁的触发MinorGC嘛,一次MinorGC就相当于一场战斗。经历一次战斗(GC)存活下来的对象就会进入S0区或者是S1区,并且对象的年龄还会加1(JVM给每个对象定义了一个年龄计数器,初始年龄为0岁),当对象的年龄达到一定程度(在JVM中默认是15岁,但是可以通过参数-XX:MaxTenuringThreshold来设置),则说明它已经是一名老兵了,就会到老年代去“养老”了。

战斗过程:经过一次MinorGC,Eden 区和"From"区就会被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次 GC 前的“From”,新的"From"就是上次 GC 前的"To”。不管怎样,都会保证名为 To 的 Survivor 区域是空的。Minor GC 会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。

1.2 老年代

  特殊情况来了,如果新创建的对象占用内存很大或需要大量连续内存空间的对象时(比如:字符串、数组),则直接进入老年代。

那为什么要这样呢?
答:为了避免为大对象分配内存时由于分配担保机制带来的复制而降低效率。


  除此之外的对象就是从年轻代移动过来的了,因为老年代中的对象比较稳定,因此在这块区域不会频繁的触发MinorGC,但不是不会发生,以下就是老年代触发MinorGC的条件:

  • 当年轻代的对象即将进入老年代,发现老年代区域的内存空间不足以存放“准老年代”的对象时,这时就会触发一次MinorGC。
  • 当老年代无法找到一块足够大并且连续的内存空间分配给新创建的较大对象时,这时也会触发一次MinorGC为新对象腾出空间。

最后注意:当老年代内存空间也满了装不下时,JVM就会抛出OOM异常。

1.3 元空间

  首先跟大家说明一个一直困扰我很久的问题,就是永久代元空间方法区的关系。

在JDK1.8之前元空间被叫做永久代,其实元空间和永久代本质是类似,都是方法区的实现,但他们最大的区别就是:永久代的存储位置是在JVM的堆(Heap)中。而元空间不在JVM中,使用的是计算机的本地内存。

  既然元空间是方法区的一个实现,因此他存放的东西大致和方法区内存放的东西相同:Class、Method元信息、运行时常量池,包含字符串常量池,静态变量等。

那为什么用元空间来替代永久代呢?

  1. 有了元空间就不再会出现永久代OOM问题了!(但是元空间依然会出现内存溢出)
  2. 永久代会为 GC 带来不必要的复杂度,并且回收效率偏低
  3. 我们都知道JVM虚拟机有很多种,其中有两种就是 HotSpot 与 JRockit,但JRockit没有永久代。为了统一,所以将永久代移除了。

2.垃圾回收

既然我们已经分析完了堆内存空间的结构了,那哪些对象是属于“垃圾”呢?哪些又需要回收呢?

堆中几乎放着所有的对象实例,所谓的“垃圾”其实就是已经“死亡”的对象(即不能再被任何途径使用的对象)。

2.1 判断对象是否已经死亡

判断某个对象是否已经死亡,有以下两种方法:

  1. 引用计数法
      给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
      这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。 所谓对象之间的相互引用问题,如下面代码所示:除了对象 objA 和 objB 相互引用着对方之外,这两个对象之间再无任何引用。但是他们因为互相引用对方,导致它们的引用计数器都不为 0,于是引用计数算法无法通知 GC 回收器回收他们(这里涉及部分对象引用的相关知识)。
public class ReferenceCountingGc {
    Object instance = null;
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;

    }
}

  1. 可达性分析算法
      这个算法其实就是以一系列被称为“GC Roots”的对象作为起点,从这些节点开始向下链接,每个节点所走过的路径称为引用链,当一个节点对象到根节点“GC Roots”没有任何任何引用链相连接的话,则说明此对象是不可用了。(如下图)。
    在这里插入图片描述
    可作为图中“GC Root”的对象包括下面几种:
  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 本地方法栈(Native 方法)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 所有被同步锁持有的对象

注意:不可达对象并非“非死不可”。
即使在可达性分析法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。

2.2 垃圾收集算法

在这里插入图片描述

  1. 标记-清除算法
      标记-清除算法是这几类算法中最基础的算法,后续几种都是在其不足之处改进得到的。该算法分为标记、清除两个步骤,首先标记出所有不需要回收的对象,在标记完后统一回收掉所有没有被标记的对象。
    在这里插入图片描述
    不足之处
      (1) 效率问题
      (2) 空间问题:标记清除后会产生大量不连续的碎片。

  2. 复制算法
      为了解决效率问题,“标记-复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
    在这里插入图片描述

  3. 标记-整理算法
      根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
    在这里插入图片描述

  4. 分带收集算法
      当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
      比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。

2.3 垃圾收集器

如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。

虽然我们对各个收集器进行比较,但并非要挑选出一个最好的收集器。因为直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器。试想一下:如果有一种四海之内、任何场景下都适用的完美收集器存在,那么我们的 HotSpot 虚拟机就不会实现那么多不同的垃圾收集器了。
在这里插入图片描述
因为本人还未研究有关垃圾收集器的相关知识,对此了解不深,因此未给大家总结,望大家多多包涵。有研究过的小伙伴还望不吝赐教,我将洗耳恭听。

三、总结

  以上就是有关JVM垃圾回收的相关内容了,有关OOM的解决方案我将与下一篇文章(JVM调优)一起详细讲述,本人也是整理了许久,查阅了很多知识,在此期间也让我对这些知识点有了更深的认识。如有不足和错误,请大家批评指出,我将认真思考总结。

参考:向大家推荐一个非常好的Java学习平台JavaGuide。https://snailclimb.gitee.io/javaguide/#/

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 12
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 12
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值