GC垃圾回收机制(垃圾检测-回收算法)

垃圾检测 回收算法

垃圾收集器一般必须完成两件事:检测出垃圾(对象死亡不可访问);回收垃圾。怎么检测出垃圾?

垃圾检测一般有引用计数法和可达性分析

引用计数法:给一个对象添加引用计数器,每当有个地方引用它,计数器就加1;引用失效就减1。

好了,问题来了,如果我有两个对象object1和object2,互相引用,除此之外,没有其他任何对象引用它们,实际上这两个对象已经无法访问,即是我们说的垃圾对象。但是互相引用,计数不为0,导致无法回收

public class Main {
    public static void main(String[] args) {
        MyObject object1 = new MyObject();
        MyObject object2 = new MyObject();
         
        object1.object = object2;
        object2.object = object1;
         
        object1 = null;
        object2 = null;
    }
}
 
class MyObject{
    public Object object = null;
}

为了解决这个问题,在Java中采取了 可达性分析法。该方法的基本思想是通过一系列的“GC Roots”对象作为起点进行搜索,如果在“GC Roots”和一个对象之间没有可达路径,则称该对象是不可达的,不过要注意的是被判定为不可达的对象不一定就会成为可回收对象。被判定为不可达的对象要成为可回收对象必须至少经历两次标记过程,如果在这两次标记过程中仍然没有逃脱成为可回收对象的可能性,则基本上就真的成为可回收对象了。

可达性分析算法:以根集对象为起始点进行搜索,如果有对象不可达的话,即是垃圾对象。这里的根集一般包括java栈中引用的对象、方法区常良池中引用的对象

GC Roots 可理解为「堆外指向堆内的引用」。在 Java 技术体系中,固定可作为 GC Roots 的对象包括以下几种:

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象(比如引用类型的静态变量)
  • 方法区中常量引用的对象(比如字符串常量池的引用)
  • 本地方法栈中 Native 方法引用的对象
  • JVM 内部的引用(例如:基本数据类型的 Class 对象、常驻异常、系统类加载器)
  • 同步锁(synchronized 关键字)持有的对象
  • 反应 JVM 内部情况的 JMXBean、JVMTI 中注册的回调、本地代码缓存等
垃圾回收算法

在确定了哪些垃圾可以被回收后,垃圾收集器要做的事情就是开始进行垃圾回收,但是这里面涉及到一个问题是:如何高效地进行垃圾回收。由于Java虚拟机规范并没有对如何实现垃圾收集器做出明确的规定,因此各个厂商的虚拟机可以采用不同的方式来实现垃圾收集器,所以在此只讨论几种常见的垃圾收集算法的核心思想。
  1.Mark-Sweep(标记-清除)算法
  这是最基础的垃圾回收算法,之所以说它是最基础的是因为它最容易实现,思想也是最简单的。标记-清除算法分为两个阶段:标记阶段和清除阶段。标记阶段的任务是标记出所有需要被回收的对象,清除阶段就是回收被标记的对象所占用的空间。具体过程如下图所示:
在这里插入图片描述
优缺点分析:
主要优点:实现简单;
主要缺点:
执行效率不稳定,标记和清除过程效率都不高(标记对象较多时);
内存空间碎片化问题(碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作)。
图片来自:http://www.cnblogs.com/dolphin0520/p/3783345.html
 
 2.Copying(复制)算法
  为了解决Mark-Sweep算法的缺陷,Copying算法就被提了出来。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。具体过程如下图所示:
  在这里插入图片描述

优点:实现简单,运行高效且不易产生内存碎片;
缺点:可用内存缩小为原来的一半,空间浪费太多。

很显然,Copying算法的效率跟存活对象的数目多少有很大的关系,如果存活对象很多,那么Copying算法的效率将会大大降低。

PS:一般不需要按照 1:1 的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1 (即“浪费”了 10% 的新生代空间)。
  3.Mark-Compact(标记-整理)算法
  为了解决Copying算法的缺陷,充分利用内存空间,提出了Mark-Compact算法。该算法标记阶段和Mark-Sweep一样,但是在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。具体过程如下图所示:
在这里插入图片描述
主要问题:

移动存活对象:回收内存时会更复杂(Stop The World);
不移动存活对象:分配内存时会更复杂(空间碎片问题)。

扫描二维码关注公众号,回复: 9456402 查看本文章

分代收集算法:
这是当前商业虚拟机常用的垃圾收集算法。分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

为什么要运用分代垃圾回收策略?在java程序运行的过程中,会产生大量的对象,因每个对象所能承担的职责不同所具有的功能不同所以也有着不一样的生命周期,有的对象生命周期较长,比如Http请求中的Session对象,线程,Socket连接等;有的对象生命周期较短,比如String对象,由于其不变类的特性,有的在使用一次后即可回收。试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,那么消耗的时间相对会很长,而且对于存活时间较长的对象进行的扫描工作等都是徒劳。因此就需要引入分治的思想,所谓分治的思想就是因地制宜,将对象进行代的划分,把不同生命周期的对象放在不同的代上使用不同的垃圾回收方式。

如何划分?将对象按其生命周期的不同划分成:年轻代(Young Generation)、年老代(Old Generation)、持久代(Permanent Generation)。其中持久代主要存放的是类信息,所以与java对象的回收关系不大,与回收息息相关的是年轻代和年老代。这里有个比喻很形象

“假设你是一个普通的 Java 对象,你出生在 Eden 区,在 Eden 区有许多和你差不多的小兄弟、小姐妹,可以把 Eden 区当成幼儿园,在这个幼儿园里大家玩了很长时间。Eden 区不能无休止地放你们在里面,所以当年纪稍大,你就要被送到学校去上学,这里假设从小学到高中都称为 Survivor 区。开始的时候你在 Survivor 区里面划分出来的的“From”区,读到高年级了,就进了 Survivor 区的“To”区,中间由于学习成绩不稳定,还经常来回折腾。直到你 18 岁的时候,高中毕业了,该去社会上闯闯了。于是你就去了年老代,年老代里面人也很多。在年老代里,你生活了 20 年 (每次 GC 加一岁),最后寿终正寝,被 GC 回收。有一点没有提,你在年老代遇到了一个同学,他的名字叫爱德华 (慕光之城里的帅哥吸血鬼),他以及他的家族永远不会死,那么他们就生活在永生代

年轻代:是所有新对象产生的地方。年轻代被分为3个部分——Enden(翻译:乐园)区和两个Survivor(翻译:幸存者)区(From和to)当Eden区被对象填满时或jvm空闲,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区(假设为from区)。当from区填满就会执行Minor GC,同样会检查存活下来的对象,并把它们转移到另一个survivor区(假设为to区)。这样在一段时间内,总会有一个空的survivor区(from区或to区,在填满时会进行Minor GC,存活下来的对象会转移到另一个区from或to)。经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。需要注意,Survivor的两个区是对称的,没先后关系,from和to是相对的。

年老代:在年轻代中经历了N次(默认15次)回收后仍然没有被清除的对象,就会被放到年老代中,可以说他们都是久经沙场而不亡的一代,都是生命周期较长的对象。对于年老代和永久代,就不能再采用像年轻代中那样搬移腾挪的回收算法,因为那些对于这些回收战场上的老兵来说是小儿科。通常会在老年代内存被占满时将会触发Full GC,回收整个堆内存。

持久代:用于存放静态文件,比如java类、方法等。持久代对垃圾回收没有显著的影响。 (在jdk1.8之后,移除了持久带,取而代之的是 Metaspace(元空间)元空间的本质和永久代类似,都是对JVM规范中方法区的实现。不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。因此,默认情况下,元空间的大小仅受本地内存限制)

补充:
Eden和Survivor的比例分配等
默认比例8:1。
大部分对象都是朝生夕死。
复制算法的基本思想就是将survivor区内存分为两块(from/to),每次只用其中一块,当这一块内存用完就会进行GC垃圾回收,将还活着的对象复制到另外一块上面。复制算法不会产生内存碎片。

jvm中一次完整的GC流程(从ygc到fgc)是怎样的,重点讲讲对象如何晋升到年老代

对象优先在年轻代区中分配,若没有足够空间,Minor GC;大对象(需要大量连续内存空间)直接进入年老代;长期存活的对象进入持久代。如果对象在年轻代出生并经过第一次MGC后仍然存活,年龄+1,若年龄超过一定限制(15),则被晋升到年老代

发布了32 篇原创文章 · 获赞 53 · 访问量 2486

猜你喜欢

转载自blog.csdn.net/qq_41714882/article/details/103819619