你真的了解GC吗?

GC是什么?

在计算机科学中,垃圾回收(英语:Garbage Collection,缩写为GC)是指一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。
——来自维基百科

通俗来讲就是,你家里有 100 平方,你会买各种生活用品和家居回来,这些物品都会占用空间。过了一段时间,你会通过各种情况去判断这些物品还需不需要,然后再去进行清除和整理。

其中:

  • 家里100 平方:指的是总内存空间大小
  • 生活用品和家具所占用的空间:指的是程序所所占用的内存空间
  • 判断物品是否需要:指的是垃圾回收算法
  • 清除和整理:指的是内存回收,以及对内存进行管理

问题来了,无房人士,“家”在哪?

GC在哪运行?

首先,我们先来了解下 class 文件的运行流程:

其中,Java 堆主要用来存放在运行时创建的 Java 对象,当剩下的内存空间不足以存放新建的 Java 对象,这时,就需要 GC 进行内存管理。

GC运行机制

GC 需要进行对象内存回收的之前,需要知道哪些对象的内存可以进行回收。

判断对象内存是否能回收

目前较为流行有两种方式:

引用计数法:

每个 Java 堆中存储的 Java 对象内部都维护着一个 counter 计数器,每当新增一个对象引用指向该对象时,该 counter 则 +1,否则 -1。当 counter 值为 0 的时候,说明该对象可回收。

这种做法的优点是能快速判断该对象是否能回收。但是却有一个致命的缺点,那就是当出现循环的对象引用时,这时就无法回收该对象内存。如下图:

对象 A、B、C 都有一个对象引用指向,他们的 counter 都为 1,但是实际上已经不再使用这三个对象了,但是还是会被判断这三个对象不可回收。

可达性分析算法

JVM 使用该算法。

首先,先确认某些对象是不允许被回收的,也就是一系列的 GC Root 对象,然后再看下 GC Root 对象引用了哪些对象 A,再看这些对象 A 引用了哪些对象 B,以此类推,就会出现一个 GC Root 引用链:

这条 GC Root 引用链上的对象都设定为不可回收,反之,不在 GC Root 引用链上的对象则为可回收。

虽然这个算法在理论上是没有问题,但是在实际开发中,很容易出现该被回收的对象,由于被 GC Root 对象直接或间接引用,无法被回收,导致内存泄漏。

既然有可能会出现内存泄漏,那么我们就要知道什么会成为 GC Root,从而在适当时机释放 GC Root,避免内存泄漏。

可以作为 GC Root 的对象

  • Java 虚拟机栈(局部变量表)中引用的对象
  • 方法区中静态引用指向的对象
  • 仍存活的线程对象
  • Native 方法中 JNI 引用的对象

前三种为应用开发中最常见的对象,但是仅靠描述,可能基础较差的同学还是有点蒙,对此,我再进行相应举例:(至于第四种,我对于 JNI 不够了解,还是不装这逼了🤣)

  • Java 虚拟机栈(局部变量表)中引用的对象
    • 正在执行的方法里面所引用的对象。这个很容易理解,就是代码执行到了哪个方法,这个方法里面的对象都是。
  • 方法区中静态引用指向的对象
    • Java 文件中的 static 对象。所以使用单例直接存储持有 Activity Context 的对象,就会导致该 Activity 资源无法释放。
  • 仍存活的线程对象
    • new Thread().start() 后的对象。由此可以延伸到 AsyncTask、Handler等,只要内部新建并启动线程的都算。使用内部类方式创建线程对象,当宿主对象想要销毁的时候,但由于内部类默认持有外部引用,导致宿主对象无法被回收。

GC回收内存

当标记了哪些对象内存可以被回收,剩下的就是回收操作了。当然,并不是直接回收就行了,毕竟内存就像一张白纸,直接回收内存就像在白纸上挖洞,这些洞大大小小,分部不均,导致后续想再从这些洞中分配大内存就变得十分困难,会频繁触发 GC。因此,GC 回收内存其实也有相应的算法的:

标记清除算法

这个就是最简单粗暴的方式,标记完后直接回收。优点是速度快,但缺点就非常明显:内存碎片化严重,后续想分配大内存困难。

复制算法

把当前的内存空间一分为二,每次只用其中一半,当 GC 的时候,将存活的对象拷贝到另一半内存空间,原有的内存空间直接清除。

缺点就是太浪费内存空间了,相当于可用内存空间减半!

标记压缩算法

将所有存活的对象压缩到内存的一端,然后把剩下的内存空间全部清除。

这种清理完的结果十分优秀,不过就是要频繁移动对象内存,需要消耗过多性能。

分代算法

大佬总是最后登场。

首先,我们先看下其模型:

将内存分为新生代、老年代,其中新生代里面包含 Eden、Survivor0、Survivor1。

当分配对象内存的时候,优先分配到 Eden 区域。

第一次 Monitor GC,将 Eden 存活的对象复制到 Survivor0 中,剩下空间清除:(复制算法)

第二次 Monitor GC,将 Eden 和 Survivor0 存活的对象复制到 Survivor1 中,剩下空间清除:(复制算法)

第三次 Monitor GC,将 Eden 和 Survivor1 存活的对象复制到 Survivor0 中,剩下空间清除:(复制算法)

依次不断循环第二次和第三次 Monitor GC 的操作,直到达到 15 次的操作,或者其中区域到达阈值,这时就会把存活的对象 Copy 到老年代中。(复制算法)

等老年代也到达阈值的时候,就会触发 Full GC,Full GC 包含 Monitor GC 的操作。(老年代区域使用标记压缩算法)

特殊情况

当然,也有特殊情况:

由于老年代的内存空间一般比新生代的大,所以有时申请对象内存空间是,假如新生代无法存放,这时就会直接存储到老年代中。(出道即巅峰🤣)

还有另外一种情况:

老年代中的对象引用了新生代的对象,这时候就出问题了。em…什么问题?可能有些同学还没反应过来,我稍微解释下:

之所以分代算法效率比较高,那是因为它把对象分成了几块区域,像一些比较长命的对象就存放在老年代中,比较短命的对象存放在新生代,这样 GC 的时候,几乎可以忽略老年代的对象,直接清理新生代的对象即可。但是,由于老年代引用了新生代,导致 Monitor GC 的时候需要去扫描老年代,不然怎么知道哪些对象需要保留下来给老年代引用?

其实解决的方式也很简单:

把老年代区域划分为多个区域,并且使用 card table 记录这些需要有没有引用到新生代,有的话,Monitor GC 的时候也要扫描对应的老年代区域。

具体模型如下:


这是我的公众号,欢迎关注支持下,谢谢!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值