您现在的位置是:首页 > 正文

java垃圾标记算法和垃圾回收算法总结

2024-02-01 01:56:29阅读 6

标记算法用于区分存活对象和死亡对象(垃圾标记阶段),垃圾回收算法用于执行垃圾的回收(清除阶段)。

1.两种主流的垃圾标记算法

1.1 引用计数法

       对于一个对象A,只要有任何一个对象引用了A,则A的用用计数器就加1,当引用失效时,引用计数器就减一。只要计数器的值为0,说明该对象A不再被使用,即引用失效。该算法的优势在于,不用等到内存不够用时才进行垃圾回收,完全可以在赋值操作的同时检查计数器是否为0,如果是的话就可以立即回收。

      但是引用计数器有一个严重的问题,即无法处理循环引用的情况。比如对象A和对象B,它们互相持有对方的引用,此时,对象A和对象B的应用计数器都不为0,但是系统中却不存在第三个对象引用A或B,也就是说,A和B是应该被回收的垃圾对象,但是由于垃圾对象的互相引用,从而使垃圾回收器无法识别,引起内存泄漏。

      引用计数器也有两个不足之处,一是它需要单独的字段来存储计数器,增加了内存空间的开销;其次,每次赋值操作都需要更新计数器,增加了时间的开销。优势在于,便于识别垃圾对象,只要计数器为0,就可以作为垃圾回收掉。但是由于它不能解决循环引用的缺陷,在java的垃圾回收器中没有使用这类算法。

1.2 根搜索算法

      HotSpot和大部分JVM都是使用根搜索算法作为垃圾标记算法的实现。根搜索算法是以根对象集合为起始点,按照从上至下的方式搜索根对象集合所连接的目标对象是否可达(使用根搜索算法后,内存中的存活独享都会被根对象集合直接或间接连接着),如果目标对象不可达,就意味着该对象已经死亡,就可以在对象头部标记为垃圾对象。在根搜索算法中,只有能够被根对象集合连接或者间接连接的对象才是存活对象。在HotSpot中,根对象集合中包含了5个元素,Java栈内的对象引用、本地方法栈内的对象引用、运行时常量池中的对象引用、方法区中类静态属性的对象引用、一个与类对应的唯一数据类型的Class对象。换句话说,这5个元素为方法局部变量、常量池中对象应用、静态属性的引用、类的字节码文件。

      在根搜索算法中,不可达对象不一定会被回收,他们只是暂时被标记为垃圾对象。如果要它真正死亡,至少需要经历两次标记过程。如果对象在根搜索算法后发现没有与GC Roots(根集合)相连接的引用链,那么它就会被第一次标记并且进行一次筛选,筛选条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过了(finalize方法只可能被调用一次),虚拟机将这两种情况都视为“没有必要执行”。如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为F-Queue的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象在finalize()方法中执行缓慢,或者发生了死循环,将会导致F-Queue队列中其他的对象一直处于等待状态,甚至导致整个内存回收系统崩溃。finalize()方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,如果对象要在finalize()中逃脱死亡,只要重新与引用链中的任何一个对象建立关联即可,那么在第二次标记时它将会被移出“即将回收”的集合。如果对象这个时候还没有逃脱死亡,那它就会被标记为真正需要回收的对象。

2. 主流的垃圾回收算法

2.1 标记-清除算法(Mark-Sweep)

       标记清除算法将垃圾回收分成两个阶段进行。首先是标记阶段,收集器从应用程序的根对象开始进行遍历,对所有可以访问到的对象打上一个标记,一般在对象头部(header)中,将它标记为可达对象。然后进入清除阶段,收集器对堆内存从头到尾进行线性的遍历,如果发现某个对象没有被标记为可达对象(通过读取对象头来判断),这个对象就被认为是垃圾对象,就将其回收。

      标记清除算法存在一个比较大的弊端,就是会产生大量的空间碎片,因为回收之后就结束了,并没有进行空间的压缩、优化,所以很容易产生这种情况:举个例子,一段连续的内存,前半段是已经被使用的,中间是未被使用的,后半段又是被使用的,当进行对象内存分配的使用,正好发现中间未被使用的那一段放不下我这个对象,需要去额外查找、申请其他的内存,这一点在进行大对象内存分配的时候尤其明显,工作效率远远低于连续的空间。

2.2 复制算法(Copying)

      复制算法的出现其实就是为了解决标记清除算法的缺陷。它首先将内存空间分为两块,每次只会使用其中的一块,在垃圾回收时,将赈灾使用的那一个块内存中还存活的对象复制到未被使用的内存块中,然后清除正在使用的那一块内存中的所有对象,之后交换两个内存块的角色,最后完成垃圾回收。

      复制算法需要复制存活的对象,如果存活的数量比较大,也会出现比较严重的工作效率问题。其实真正使用的时候,存活的对象数量是比较小的,因此,他的效率还是很高的,而且回收过程中,对象都是统一被复制到另一块新的内存空间中去的,因此可以确保回收喉 的内存空间是连续的,不会产生碎片。

      该算法的缺点就是只能使用一半的可用内存,它要确保存在一块内存空间是未被使用的(全新的),分配都在另一半内存空间进行。

2.3 标记-压缩算法(Mark-Compact)

      标记压缩算法也是在标记清除算法上进行改良而出现的。它还结合了复制算法执行高效的优点,去除了复制算法只能使用一半的缺点。首先和标记清除算法一样,它需要进行标记处内存中的垃圾对象,然后将所有的存活对象都复制到一个规整、连续的内存空间中,然后执行GC回收无用的对象。最后的情况就是,已经被使用的内存和未被使用的内存各自占据一边,彼此之间维系着一个记录下一次分配起始点的标记指针。

      复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。所以复制算法常见于年轻代的回收,但是在老年代,大部分对象都是存活对象,复制的成本就比较高了。而且标记清除算法又会产生比较多的内存空间碎片,所以老年代比较适合用标记压缩算法,它和标记清除算法的区别就在于在清除阶段,它并不是简单的去清理未被标记的对象,而是将所有的存活对象复制到另一端去(这里借鉴了复制算法的),最后再清理空间。这种方式既避免了产生内存空间碎片,又不需要将内存一分为二,因此这种算法是比较理想的。

2.4 增量算法(Incremental Collecting)

      在垃圾回收过程中,应用程序处于STW的状态(stop the world),在这种状态下,应用程序的所有线程都会挂起,暂停工作,等待垃圾回收完成,因此,如果垃圾回收过程时间比较长,相应的,应用程序也会被挂起很久,严重影响用户体验、系统稳定性。所以最初的想法,是希望一个进程执行垃圾收集工作,另一个进程执行应用程序,避免STW的产生。但实际情况是,垃圾收集器在第一阶段中辛辛苦苦标记出来的结果可能被被另一个进程中的内存操作改得面目全非,导致第二阶段的工作无法展开。

      这个算法的基本思想是,如果一次性将所有的垃圾进行处理,需要造成较长的停顿,它让垃圾收集线程和应用程序线程交替执行,并且每次收集线程只收集一小片区域的内存空间,然后切换到应用程序线程。依次反复,直到垃圾收集完成。这种算法虽然能减少STW的时间,但是由于频繁地切换,总体回收的成本比较高,使系统吞吐量下降。

2.5 分代收集算法(Generational Collecting)

      分代收集算法将内存空间根据对象的特点分成几块,根据每块的特点,使用不同的回收算法来提高效率。

      比如常见的HotSpot虚拟机,它将需要分配的新对象放入一个称为年轻代的内存区域,这个区域的特点是,存活对象较少、生命周期短,因此,在年轻代使用效率较高的复制算法。当一个对象经过几次回收后依然存活,它就会进入称为老年代的内存区域,这个区域中的对象都是经过几次GC存活下来的,因此这里的对象生命周期较长、而且存活率较高,这里就不能再采用复制算法了(因为需要复制大量的对象),需要使用标记压缩算法。

      所以说,分代收集算法是在对对象生命周期进行分析后得出的垃圾回收算法,把堆内存区域根据对象的生命周期特点划分为年轻代、老年代、持久代(JAVA8移除),对不同生命周期的对象使用不同的算法。从J2SE1.2开始,JVM垃圾回收期都是采用此算法。

2.6 标记-清除算法、复制算法、标记压缩算法比较

  标记清除算法 复制算法 标记-压缩算法
速度 中等 最快 最慢
空间开销 少(但是会不断累积) 通常需要存活对象的2倍内存大小 少(不会堆积)
移动对象

 

参考文献:

[深入理解JVM&G1 GC].周明耀.2017.6

网站文章

  • linux ntp时间源服务器,linux搭建ntp时间服务器

    linux ntp时间源服务器,linux搭建ntp时间服务器

    1、时间服务器用来给其他主机提供时间同步服务,在搭建服务器集群的时候,需要保证各个节点的时间是一致的,时间服务器不失为一个好的选择。2、安装ntp服务器[root@centos7 ~]# yum in...

    2024-02-01 01:56:24
  • Docker Pull设置代理解决Get https://k8s.gcr.io/v2/: net/http: request canceled while waiting for connection

    今天搭建k8s集群时,发现一系列k8s.gcr.io的镜像无法pull:-<%>- docker pull k8s.gcr.io/kube-proxyUsing default tag: latest...

    2024-02-01 01:56:17
  • 【线性代数/机器学习】矩阵的奇异值与奇异值分解(SVD)

    上面介绍了奇异值,下面介绍如何利用奇异值对矩阵进行分解。设AAA是一个m×nm\times nm×n矩阵,σ1≥σ2≥⋯≥σn≥0σ1​≥σ2​≥⋯≥σn​≥0是它的奇异值。令rrr为AAA的秩,也就是AAA非零奇异值的个数。定义5AAAAUΣVTAUΣVT其中UUU是一个m×mm\times mm×m正交矩阵;VVV是一个n×nn\times nn×n正交矩阵;Σ。

    2024-02-01 01:56:10
  • C++项目中如何实现一个栈计算器?

    C++项目中如何实现一个栈计算器?

    写一个栈计算器写一个栈计算器,设计如下:支持 +、-、*、/运算支持后缀输入例如:23+输出:5堆操作可以总结如下:push:将一个元素添加到栈顶部pop:从栈顶部移除该元素top: 获取栈顶部元素的...

    2024-02-01 01:55:42
  • 真的太刺激了,蚂蚁金服难忘的四面经历:Linxu+数据库+数据结构+算法+计算机网络

    真的太刺激了,蚂蚁金服难忘的四面经历:Linxu+数据库+数据结构+算法+计算机网络

    前言 前段时间,蚂蚁金服的热度可不小,互联网圈人人都在讨论它上市的事情,实际上蚂蚁金服上市是迟早的事情。这一下,蚂蚁的员工含金量上升了不少,那我之前蚂蚁提前批这波面经,也是时候分享了。 这次面试,可以...

    2024-02-01 01:55:36
  • Python虚拟环境管理之 pipenv

    Python虚拟环境管理之 pipenv

    创建虚拟环境需要一些工具,本文将会介绍这些工具。 pipenv 是Kenneth Reitz大神的作品,提供Python的各个版本间的管理,各种包管理。

    2024-02-01 01:55:29
  • hua图软件 mac_新入手Mac系统需要做的两件事

    hua图软件 mac_新入手Mac系统需要做的两件事

    最近有小伙伴们提问,新买的Mac 电脑,想要下载点软件,要么下载了软件提示:“xxx”因为出现问题而无法打开。请与开发者联系,还有的小伙伴是这样的提示“xxx.app已损坏,打不开。您应该将它移到废纸...

    2024-02-01 01:55:03
  • Git实操大全(最全操作)

    Git实操大全(最全操作)

    目录1.git常用命令1.1用户签名在此文件中查看(全局的)1.2 git init初始化本地库1.3 git status查看本地库1.4 git add 添加到暂存区1.5 添加到本地库 git ...

    2024-02-01 01:54:56
  • Liunx安装MySQL解压版

    liunx安装解压版教程;

    2024-02-01 01:54:49
  • 【C++】类与类之间的 5 种关系

    【C++】类与类之间的 5 种关系

    该文章就是简要的总结一下面向对象的:类与类之间的关系,熟悉类与类之间的关系,能够帮助我们更好的设计出合理的类;: A继承B,说明A is B;

    2024-02-01 01:54:20