JVM知识总结之垃圾收集算法

一、什么是垃圾

本文要讲的是垃圾收集算法,那么首先要确定的问题就是什么是垃圾,也就是哪些对象是要被回收的,对此有两种判断方式:

1.1 引用计数算法

什么样的对象是要被回收的,很明显,没有被引用的对象才要被回收。因此在对象中加一个引用计数器,当有一个对象引用该对象的时候,计数器就加一,当引用结束后,计数器就减一,当计数器为0的时候,对象就可以被回收了。

1.1.1 优点

  • 原理简单
  • 判断效率高

1.1.2 缺点

  • 需要花费额外的内存空间(引用计数器)
  • 无法回收相互循环引用的对象:比如有对象A和对象B,A引用B,B引用A,两对象的引用计数器都为1,理论上来说,没有其他对象能够引用到A和B了,因此这两个对象应该被回收,然后按照引用计数算法的判断,这两个对象无法被回收。(要克服这个缺点,需要在代码中做很多特殊处理)

1.2 可达性分析算法

因为引用计数算法的缺陷,各大主流的商用程序语言都采用可达性分析算法来判断对象是否需要被回收。可达性顾名思义,是指对象跟对象之间有引用关系,此处有两种引用关系:

  • 对象A引用对象B,则称对象A到对象B可达
  • 对象A引用对象B,对象B引用对象C,对象A可以通过若干个对象(此处为对象B)引用到对象C,则称对象A到对象C可达。

要判断一个对象是否可达,首先要有一个根对象,在Java中有一系列被称为“GC Roots”的根对象作为起始节点集,任何从“GC Roots”不可达的对象都是需要被垃圾收集器回收的垃圾。

1.2.1 优点

可以有效解决引用计数算法的相互循环引用问题

二、什么是引用

在讨论什么是垃圾的时候,多次提到引用一词,那么什么是引用呢?

2.1 JDK1.2以前

按照书中的说法,在JDK1.2以前,引用的意思是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称该reference数据是代表某块内存、某个对象的引用。
在这种定义下可以发现,对于一个对象来说,就只有未被引用和被引用两种状态了,但其实可以发现,在实际应用中,并不是一定要把对象回收掉的,书中有个词就很贴切,“食之无味,弃之可惜”,我们想要的是当内存空间足够的时候,把这部分本该回收的对象留着不回收,当内存不够的时候,就将其回收。

2.2 JDK1.2之后

因此在JDK1.2之后,引用的概念就扩张到了以下四种:

  • 强引用:指传统意义上的引用,有强引用的对象是肯定不被回收的;
  • 软引用:用于描述一些还有用,但非必须的对象,当要发生内存溢出的时候,就会回收软引用对象;
  • 弱引用:用于描述非必须对象,强度比软引用弱一点,当垃圾收集器开始工作,无论内存够不够都会回收弱引用对象;
  • 虚引用:虚引用意思就是这个引用跟没有一样,对对象完全没有印象,其存在的唯一作用就是在对象被垃圾收集器回收时能收到一个系统通知。

三、垃圾判断全流程

按照书中所述,我画了个流程图,如下:

一个对象在被回收前,需要进行两次标记,第一次进行可达性分析后,对象被垃圾收集器认为是垃圾,则对对象进行第一次标记,然后垃圾收集器会给予对象一次自救的机会,不然就没必要两次标记了,一次标记直接回收就好了。
我们都知道对象有个finalize()方法,自救的机会就在这个方法中,当第一次标记后,垃圾收集器会对对象做一次筛选,筛选条件是要不要执行对象的finalize()方法,如果开发者未对finalize()方法进行覆写或者虚拟机已经执行过该对象的finalize()方法了,那么自然就不用再执行了,反之则需要执行。
将筛选出来的需要执行finalize()方法的对象放入一个特定的队列中,由虚拟机统一执行,如果finalize()方法中使得对象被别的对象引用了,导致可达性分析认为对象是可用的,那么自救就成功了。
根据筛选的条件可以知道,对象的自救机会在整个程序中只有一次,因为finalize()方法只会被执行一次。
需要注意的是,官方明确申明不推荐使用finalize()方法,因为使用它的不确定性太大。对于资源清理等操作,try…catch语法可以做的更好。

四、垃圾收集算法

大多数虚拟机的垃圾收集都采用了分代收集的形式,这是因为三条经验法则:

  • 弱分代假说:绝大多数对象都是朝生夕死的;
  • 强分代假说:熬过越多次垃圾收集过程的对象就越难以消亡
  • 跨代引用假说:跨代引用相对于同代引用来说仅占极少数

因为对象的生存周期是不一样的,所以我们不能对所有对象采用同一种垃圾收集算法,采用分代收集,将有共性的对象放在一个集合里,会大大地提高垃圾收集效率。
按照上述经验法则,可以将堆内存分为两代:

  • 新生代:对应弱分代假说
  • 老年代:对应强分代假说

下面分别介绍三种垃圾收集算法:

4.1 标记-清除算法

标记-清除算法是最基础的垃圾收集算法,顾名思义,标记就是判断对象是否是垃圾,也就是前面第四节讲到的内容,清除就是统一回收垃圾,该算法有两种执行过程:

  • 标记所有需要被回收的对象,统一回收所有标记的对象;
  • 标记所有存活对象,统一回收所有未被标记的对象。

标记-清除算法示意图:

标记-清除算法有两大缺点:

  • 执行效率不稳定:标记和清除两个过程的执行效率随对象数量的增加而降低
  • 内存空间碎片化:执行完标记和清除后,会产生大量的不连续的内存碎片,当分配大对象的时候,如果找不到足够的连续内存,那么会提前触发下一次垃圾收集。

4.2 标记-复制算法

基于标记-清除算法的缺点,标记-复制算法将内存空间一分为二,两块内存空间等大,每次只使用其中一块内存空间,当这一块内存空间用完了,就把存活的对象复制到另一块内存空间中,然后一次性清理所有已使用的内存空间。

标记-复制算法示意图:

标记-复制算法解决了标记-清除算法面对大量可回收对象场景下的不足之处,面对这种情况,标记-复制算法只需要将内存空间中的存活对象复制到另一半内存空间中,可以有效解决内存碎片的问题,在给对象分配内存的时候,只需要移动堆顶指针按顺序分配即可,不过这个算法也有缺点:

  • 面对大量不可回收对象的时候,会产生大量内存间对象复制的开销;
  • 原先的内存空间缩小了一半,会造成严重的空间浪费

4.3 标记-整理算法

标记-复制算法不足以应对有大量存活对象的场景,因此就有了标记-整理算法,该算法的执行流程如下:

  • 与其他算法一样,首先对对象进行标记;
  • 将所有存活对象往内存的一个方向移动;
  • 直接清理掉边界以外的内存

标记-整理算法示意图:

标记-整理算法同样可以解决内存碎片化问题,并且不会造成空间浪费,不过它也有缺点:

在大量对象存活的情况下,移动对象并更新引用也会花费大量时间

4.4 应用

不同的场景适用不同的垃圾收集算法,像标记-复制算法就适用于存活对象少的情况下,也就是新生代区域,像标记-整理算法就适用于存活对象多的情况下,也就是老年代。
这里有点需要注意的是,标记-整理算法对于老年代来说也不是完美的,在5.3节我们说过,在大量对象存活的情况下,移动对象和更新引用也是要花费大量时间的,不过算法这个东西吧,它比的是谁更适合,对于标记-复制算法来说,我把区域一分为二,如果大量对象存活,我要把对象全部复制到另一块内存区域,这个开销不见得比标记-整理算法少,并且它还有个缺点就是可用内存一下子少了一半,这个问题在标记-整理算法中是没有的。也有的虚拟机采用标记-清除算法标记-整理算法协作的垃圾收集方案,没有最适合,只有更适合

4.5 优化

前面讲标记-复制算法的时候说到要把内存区域等半分,这是在没有规定场景的情况下,在新生代中采用该垃圾收集算法可以做更好的优化。

众所周知,新生代中的对象都是朝生夕死的,因此当标记完成后的存活对象肯定是少量的,根据这个现象,可以将内存区域非等半分,比如说9:1的分法,这里我们将90%的内存区域称为Eden空间,将10%的内存区域称为Survivor空间,一开始使用Eden空间的内存,当垃圾收集时,将Eden空间的存活对象复制到Survivor空间中。
这里肯定有人要问了,那下一次使用Survivor空间不是就只有10%的内存了吗?

对的,所以这里有两种解决方案:

  • 将存活对象从Eden空间复制到Survivor空间后,再从Survivor空间复制回Eden空间
  • Eden空间再分离出一个Survivor空间,每次可使用的内存为一个Eden空间一个Survivor空间,当垃圾收集时,将使用的内存区域中的存活对象复制到另一个Survivor空间中,下一次的可用内存则为Eden空间和这个Survivor空间,如此循环往复。

第二种方法就是大名鼎鼎的半区复制分代策略,现在叫Appel式回收,因为提出这个策略的人叫Apple,目前很多虚拟机在新生代的垃圾收集算法中采用这个策略。

4.5.1 缺点

半区复制分代策略也是有缺点的,从上面的叙述中我们可以知道,Eden空间Survivor空间的内存占比为8:1:1,如果当垃圾收集后的存活对象所需要的内存空间大于一个Survivor空间时,那就难办了。

4.5.2 补丁

既然Survivor空间的内存不够放存活对象了,那就去借内存区域,这个借当然不能跟Eden空间Survivor空间借,不然会影响到整个算法,增加算法的复杂度。新生代不能借,那就跟老年代借,这里就有一个所谓的内存分配担保,放不下的存活对象将直接通过分配担保机制进入到老年代中。有了这个“逃生门”一样的设计,这个策略才算是没有漏洞。

五、写在后面

几个垃圾收集算法的图是我直接截了书里面的图,因为我觉得它讲的很详细了,第四节垃圾判断过程在书中实际上是一长串的代码,看懂不难,不过我想画个流程图可能更清楚点,这个流程图是用plantUML画出来的,这个工具可以用代码画出各种图,功能强大,有兴趣的可以百度搜搜,下面是这个流程图的代码:

@startuml
start
:对对象进行可达性分析;
if (对象是否为垃圾?) then (是)
    :进行第一次标记;
    if (对象没有覆盖finalize()方法 或 finalize()方法已经被虚拟机调用) then(是)
        :没必要执行对象的finalize()方法;
    else (否)
        :将对象放入队列F-Queue中;
        :等待虚拟机的Finalizer线程执行对象的finalize()方法;
        :执行对象的finalize()方法;
    endif
    if (对象是否为垃圾?) then (是)
        :进行第二次标记;
        :垃圾回收;
    else (否)
        stop
    endif
else (否)
    stop
endif
stop
@enduml

到此这篇关于JVM知识总结之垃圾收集算法的文章就介绍到这了,更多相关JVM垃圾收集算法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java虚拟机JVM性能优化(三):垃圾收集详解

    Java平台的垃圾收集机制显著提高了开发者的效率,但是一个实现糟糕的垃圾收集器可能过多地消耗应用程序的资源.在Java虚拟机性能优化系列的第三部分,Eva Andreasson向Java初学者介绍了Java平台的内存模型和垃圾收集机制.她解释了为什么碎片化(而不是垃圾收集)是Java应用程序性能的主要问题所在,以及为什么分代垃圾收集和压缩是目前处理Java应用程序碎片化的主要办法(但不是最有新意的). 垃圾收集(GC)的目的是释放那些不再被任何活动对象引用的Java对象所占用的内存,它是Java

  • JVM垃圾收集器详解

    说起垃圾收集(Garbage Collection,GC),大部分人都把这项技术当做Java语言的伴生产物.事实上,GC的历史远比Java久远,1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集技术的语言.当List还在胚胎时期时,人们就在思考GC需要完成的3件事情: 哪些内存需要回收? 什么时候回收? 如何回收? 一.哪些内存需要回收? 从JVM区域结构看,可将这些区域划分为"静态内存"和"动态内存"两类.程序计数器.虚拟机栈.本地方法3个区域

  • 浅谈JVM垃圾回收之哪些对象可以被回收

    1.背景 Java语言相比于C和C++,一个最大的特点就是不需要程序员自己手动去申请和释放内存,这一切交由JVM来完成.在Java中,运行时的数据区域分为程序计数器.Java虚拟机栈.本地方法栈.方法区和堆.其中,程序计数器.虚拟机栈和本地方法栈是线程私有的,线程销毁后自动释放.垃圾回收的行为发生在堆和方法区,主要是堆,而堆中存储的主要是对象.那么自然而然地就会有这么几个问题,哪些对象可以被回收?通过什么方式回收?本文主要探讨第一个问题,以及JVM对Java中几种引用的回收策略. 2.如何判断一

  • 快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互:这些现象多半是由于gc引起. GC时的Stop the World(STW)是大家最大的敌人.但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象. JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation

  • JVM的7种垃圾回收器(小结)

    垃圾回收算法和垃圾回收器 对于JVM的垃圾回收算法有复制算法.标记清除.标记整理. 用阳哥的话就是:这些算法只是天上飞的理念,是一种方法论,但是真正的垃圾回收还需要有落地实现,所以垃圾回收器应运而生. JVM回收的区域包括方法区和堆,jvm对于不同区域不同的特点采用分代收集算法,比如因为所有的对象都是在Eden区进行分配,并且大部分对象的存活时间都不长,都是"朝生夕死"的,每次新生代存活的对象都不多,所以新采取复制算法:而jvm默认是新生代的对象熬过15次GC才能进入老年代,所以老年代

  • 从JVM的内存管理角度分析Java的GC垃圾回收机制

    一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能.本篇文章首先简单介绍GC的工作原理之后,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能.     GC的基本原理     Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放.     对于程序员来说,分配对象使用

  • 基于JVM 中常见垃圾收集算法介绍

    JVM 中常见的垃圾收集算法有四种: 标记-清除算法(Mark-Sweep): 复制算法(Copying): 标记-整理(Mark-Compact): 分代收集: 下面我们来一一介绍: 一.标记-清除算法(Mark-Sweep) 这是最基础的垃圾收集算法,算法分为"标记"和"清除"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象.它的主要缺点有两个:一个是效率问题,标记和清除效率都不高:另一个是空间问题,标记清除后会产生大量不连续的内存

  • 图解JVM垃圾内存回收算法

    前言 首先,我们要讲的是JVM的垃圾回收机制,我默认准备阅读本篇的人都知道以下两点: JVM是做什么的 Java堆是什么 因为我们即将要讲的就是发生在JVM的Java堆上的垃圾回收,为了突出核心,其他的一些与本篇不太相关的东西我就一笔略过了 众所周知,Java堆上保存着对象的实例,而Java堆的大小是有限的,所以我们只能把一些已经用完的,无法再使用的垃圾对象从内存中释放掉,就像JVM帮助我们手动在代码中添加一条类似于C++的free语句的行为 然而这些垃圾对象是怎么回收的,现在不知道没关系,我们

  • JVM垃圾回收原理解析

    概述 Java运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随着线程的而生,随线程而死,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收问题.而Java堆和方法区则不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一眼,我们只有在运行期,才能知道会创建的对象,这部分的内存分配和回收,是垃圾回收器所关注的.垃圾回收器需要完成三个问题:那些内存需要回收:什么时候回收以及如何回收. 那些垃圾需要回收 垃圾回收的基本思想是考察一个对象的可达性,即从根节点

  • JVM知识总结之垃圾收集算法

    一.什么是垃圾 本文要讲的是垃圾收集算法,那么首先要确定的问题就是什么是垃圾,也就是哪些对象是要被回收的,对此有两种判断方式: 1.1 引用计数算法 什么样的对象是要被回收的,很明显,没有被引用的对象才要被回收.因此在对象中加一个引用计数器,当有一个对象引用该对象的时候,计数器就加一,当引用结束后,计数器就减一,当计数器为0的时候,对象就可以被回收了. 1.1.1 优点 原理简单 判断效率高 1.1.2 缺点 需要花费额外的内存空间(引用计数器) 无法回收相互循环引用的对象:比如有对象A和对象B

  • JVM中四种GC算法案例详解

    目录 介绍 引用计数算法(Reference counting) 算法思想: 核心思想: 优点: 缺点: 例子如图: 标记–清除算法(Mark-Sweep) 算法思想: 优点 缺点 例子如图 标记–整理算法 算法思想 优点 缺点 例子 复制算法 算法思想 优点 缺点 总结 介绍 程序在运行过程中,会产生大量的内存垃圾(一些没有引用指向的内存对象都属于内存垃圾,因为这些对象已经无法访问,程序用不了它们了,对程序而言它们已经死亡),为了确保程序运行时的性能,java虚拟机在程序运行的过程中不断地进行

  • Java虚拟机JVM性能优化(一):JVM知识总结

    Java应用程序是运行在JVM上的,但是你对JVM技术了解吗?这篇文章(这个系列的第一部分)讲述了经典Java虚拟机是怎么样工作的,例如:Java一次编写的利弊,跨平台引擎,垃圾回收基础知识,经典的GC算法和编译优化.之后的文章会讲JVM性能优化,包括最新的JVM设计--支持当今高并发Java应用的性能和扩展. 如果你是一个开发人员,你肯定遇到过这样的特殊感觉,你突然灵光一现,所有的思路连接起来了,你能以一个新的视角来回想起你以前的想法.我个人很喜欢学习新知识带来的这种感觉.我已经有过很多次这样

  • JVM的垃圾回收算法工作原理详解

    怎么判断对象是否可以被回收? 共有2种方法,引用计数法和可达性分析 1.引用计数法 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收. 引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法

  • 深入理解JVM垃圾回收算法

    目录 一.垃圾标记阶段 1.1.引用计数法 (java没有采用) 1.2.可达性分析算法 二.对象的finalization机制 2.1.对象是否"死亡" 三.使用(MAT与JProfiler)工具分析GCRoots 3.1.获取dump文件 3.2.GC Roots分析 四.垃圾清除阶段 4.1.标记-清除算法 4.2.复制算法 4.3.标记-压缩(整理,Mark-Compact)算法 4.4.以上三种垃圾回收算法对比 4.5.分代收集算法 4.6.增量收集算法 4.7.分区算法G1

  • 浅谈JVM垃圾回收有哪些常用算法

    一.前言: 垃圾回收: 在未来的JDK中可能G1会为ZGC所取代 先问自己几个问题: 什么是垃圾? 垃圾就是堆内存中(范指)没有任何指针指向的对象实体.不具有可达性. 为什么要回收垃圾? 因为我们的内存是有限的,内存长时间不清理就会导致内存溢出,OOM: 只要是程序正在跑,那么就不断生成新的对象,我们需要GC开辟新的空间分配给新的对象. 我们怎么回收垃圾? 依靠Java的自动内存回收机制,机制的优劣由算法决定: 或者说是机制的适配度由算法和应用场景共同决定. 什么时候回收垃圾? 当堆中的实体对象

  • 最新JVM垃圾回收算法详解

    目录 1.垃圾回收需要做什么 2.如何判断对象可被回收 2.1 引用计数算法 2.1.2 优点 2.1.2 缺点 2.2 可达性分析算法 2.2.1 算法思路 2.2.2 GC Roots对象(两栈两方法) 2.2.3 优点 2.2.4 缺点 3.判断对象生存还是死亡 3.1 两次标记过程 3.2 finalize()方法 4.HotSpot虚拟机中对象可达性分析的实现 4.1 枚举根节点 4.2 安全点 4.2.1 安全点是什么,为什么需要安全点 4.2.2 安全点的选定 4.2.3 如何在安

随机推荐