垃圾就是在运行程序中没有任何指针指向的对象,这个对象就是需要被回收的垃圾。如果不及时对内存中的垃圾进行清理,这些垃圾对象所占的占用的空间一直保留到程序运行结束,被占用的内存无法被其他对象占用,甚至导致内存溢出。下面是一些JVM垃圾回收算法 。

1.垃圾标记

堆里放着几乎所有Java实例对象,在GC执行垃圾回收之前,首先需要区分出内存要区分出内存哪些是存活的对象,哪些是已经死亡的对象。只有被标记为已经死亡的的对象,GC才会在执行垃圾回收时,释放掉其所占用的内存空间,因此这个过程我们可以称之为垃圾标记阶段。判断对象存活的一般有两种方式:

  1. 引用计数法
  2. 可达性分析算法

2.引用计数器

引用计数算法比较简单,为每一个对象保存一个整型的引用计数器,用于记录对象被引用的情况。

原理

对一个对象A,只要有任何一个对象引用了A,则A的计数器加1,当引用失效时计数器减1。当A的引用计数器为0的时候就表示A不再被引用,可以回收。

优点

  1. 实现简单,垃圾对象容易辨别;
  2. 判定效率高,回收没有延迟性。

缺点

  1. 需要额外空间来存贮引用计数器
  2. 每次更新计数器伴随着加减法操作,增加时间开销
  3. 无法处理循环引用的情况。

注意:Java没有使用引用计数算法!

3.可达性分析算法

所谓"GC Roots" 根集合就是一组必须活跃的引用。

基本思路

  1. 可达性分析算法是以根对象集合(GC Roots) 为起始点,按照从上至下 的方式搜索被根对象集合所连接的目标对象是否可达
  2. 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间 接连接着,搜索所走过的路径称为引用链(Reference Chain)
  3. 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己 经死亡,可以标记为垃圾对象。
  4. 在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象 才是存活对象。

GC Roots包括下面的元素:

  1. 虚拟机栈中引用的对象
  2. 本地方法栈内引用的对象
  3. 方法区中类静态属性引用的对象
  4. 方法区中常量引用的对象

由于Root采用栈方式存放变量和指针,所以如果-一个指针,它保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。

注意

如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在 一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。这点也是导致GC进行时必须Stop The World" 的一个重要原因。

4.finalization机制

  1. Java语言提供了对象终止(finalization)机制来允许开发人员提供对 象被销毁之前的自定义处理逻辑。
  2. 当垃圾回收器发现没有引用指向一个对象,即:垃圾回收此对象之前,总会先调用这个对象的finalize()方法。
  3. finalize()方法允许在子类中被重写,用于在对象被回收时进行资源释放 通常在这个方法中进行一些资源释放和清理的工作,比如关闭文件、套接字和数据库连接等。

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用。理由包括下面三点:

  1. 在finalize() 时可能会导致对象复活。
  2. finalize()方法的执行时间是没有保障的,它完全由GC线程决定,极端情况下若不发生GC,则finalize() 方法将没有执行机会。
  3. 一个糟糕的finalize()会严重影响GC的性能。

由于finalization机制,虚拟机中的对象可能存在下面三种状态:

  1. 可触及的:从根节点开始,可以到达到这个对象
  2. 可复活的:对象所有的引用都被释放,但是对象有可能在finalize()中复活
  3. 不可触及的:对象finalize()被调用并且没有复活,那么就进入不可初级状态。处于不可触及的对象无法复活,因为每个对象的finalize()只能被调用一次。

5.标记清除算法

背景

标记清除算法(Mark-Sweep)是一种非常基础和常见的垃圾收集算法。该算法被J . McCarthy等人在1960年提出并并应用于Li sp语言。

执行过程

当堆中的有效内存空间(available memory) 被耗尽的时候,就会停止整个程序(也被称为stop the world) ,然后进行两项工作,第-一项则是标记,第二项则是除。

  1. 标记: Collector从 引用根节点开始遍历,标记所有被引用的对象。一般是在对象的Header中记录为可达对象。
  2. 清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。

图解过程

JVM GC 标记清除算法
JVM GC 标记清除算法

缺点

  1. 效率不高
  2. 在进行GC的时候需要停止整个程序,用户体验不好。
  3. 这种方式清理出来的内存是不连续的,产生内存碎片,需要维护一个空闲列表。

6.复制算法

背景

为了解决标记清除算法效率方面的缺陷,M. L. Minsky于 1963年发表了著名的论文,“ 使用双存储区的Lisp语言垃圾收集器CA LISP Garbage Collector Algorithm Using Serial Secondary storage )”。M. L.Minsky在该论文中描述的算法被人们称为复制(Copying)算法,它也被M. L.Minsky本人成功地引入到了Lisp语言的一个实现版本中。

核心思想

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。

图解过程

优点

  1. 没有标记和清除过程,实现简单,运行高效。
  2. 复制过去之后保证空间连续性,不会存在“碎片问题”。

缺点

  1. 需要两倍的内存空间
  2. 对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。

7.标记整理算法

背景

复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。

标记-清除算法的确可以应用在老年代中,但是该算法不仅执行效率低下,而且在执行完内存回收后还会产生内存碎片,所以JVM的设计者需要在此基础之上进行改进。标记压缩(Mark Compact)算法由此诞生。

执行过程

  1. 第一阶段和标记清除算法一样,从根节点标记所有被引用的对象。
  2. 将所有存活对象整理到内存的一端,按顺序排放。之后,清理边界外所有的空间。

图解过程

优点

  1. 解决了标记清除算法中内存碎片化的问题,给新的对象分配内存时,JVM只需要一个起始内存即可。
  2. 解决了复制算法中,内存减半的高额代价。

缺点

  1. 效率低于标记清除算法和复制算法。
  2. 移动对象的同时,如果对象被其他对象引用,还需要修改引用地址。
  3. 移动过程中需要全称暂停用户应用程序,即STW。

8.三种算法对比

标记清除标记整理复制算法
速度中等最慢最快
空间开销少(内存碎片)少,且无内存碎片需要活对象两倍空间
移动对象不移动移动移动
  1. 效率上来说,复制算法是最快的,但是内存开销也是最大的。
  2. 兼顾上面三个指标,标记整理算法是相对于比较平滑的,但是效率上却又不尽人意,因为它比复制算法多了一个标记的过程,比标记整理算法多了一个内存整理的过程。

9.分代收集算法

由于上面三种算法各自有各自的优缺点,都有自己适合的场景,后来代收集算法应运而生。不同生命周期的对象采用的不同的收集方式,提高回收效率。

在HotSpot虚拟机,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代的特点。

年轻代

  1. 年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。
  2. 这种情况复制算法的效率是最高的,复制算法之和当前存活的对象大小有关,适合年轻代的回收。而HotSpot中通过两个的Surviver区缓解了复制算法的内内利用率的问题。

老年代

  1. 老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
  2. 老年区存活对象多,对象比较大,所以就不适合复制算法;老年区一般是标记整理或者标记整理和标记清除算法混合实现。

10.增量收集算法

上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the world的状态。在Stop the World状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental Collecting) 算法的诞生。

基本思想

如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。

缺点

使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码, 所以能减少系统的停顿时间。但是,因为线程切换和上下文转换的消耗,会 使得垃圾回收的总体成本上升,造成系统吞吐量的下降

11.分区算法

一般来说,在相同条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC产生 的停顿也越长。为了更好地控制Gc产生的停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理地回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。 分代算法将按照对象的生命周期长短划分成两个部分,分区算法将整个堆空间划分成连续的不同小区间region。 每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。

标签云

ajax AOP Bootstrap cdn Chevereto CSS Docker Editormd GC Github Hexo IDEA JavaScript jsDeliver JS樱花特效 JVM Linux Live2D markdown Maven MyBatis MyBatis-plus MySQL Navicat Oracle Pictures QQ Sakura SEO Spring Boot Spring Cloud Spring Cloud Alibaba SpringMVC Thymeleaf Vue Web WebSocket Wechat Social WordPress Yoast SEO 代理 分页 图床 小幸运 通信原理

JVM垃圾回收算法
JVM垃圾回收算法