每天每天
越来越爱

jvm垃圾收集算法 jvm垃圾收集器算法

垃圾回收的概述

什么是垃圾

  1. 垃圾是指在程序运行中,没有任何指针指向的对象,这个对象就需要被垃圾回收
  2. 如果不及时的回收这些垃圾,这些对象所占用的内存空间就会一直保留到程序结束,被保留的空间无法被其他对象使用,就会导致内存溢出

为什么需要GC

  1. 对于高级语言来说,一个基本的认知是,如果不进行垃圾回收,内存迟早会消耗完,因为会不断的分配新的空间。
  2. 除了释放没用的对象之外,垃圾回收也可以清除内存的记录碎片,碎片整理将堆内存的空间连续,方便存储比较大的对象
  3. 没有GC不能保证程序的正常运行,而经常GC又会造成STW(Stop The Workd) ,所以才会不断的对GC进行优化

早期的垃圾回收

  • 早期的垃圾回收基本是手工进行的,开发人员可以使用关键字NEW 来申请内存并使用DELETE释放内存
  • 这种方式可以灵活的控制内存释放的时间,但是开发人员的负担比较大,一旦有内存没有被及时的释放,就会产生内存泄漏的问题,垃圾对象永远无法被清除

Java垃圾回收机制

自动内存管理不需要开发人员手动参与内存的分配与回收,降低内存溢出的风险,同时也将开发人员从内存管理中释放出来,可以更专注于业务开发。

GC主要关注的区域

GC主要关注与方法区和堆中的垃圾回收, 其中,堆是垃圾收集器工作的重点区域

从执行效率上讲,应该多次进行Young区垃圾回收,较少收集Old区,基本不动方法区

垃圾回收算法

标记阶段:引用计数法

堆中存放着java所有的对象实例,在GC垃圾回收之前首先需要区分出那些事存活的那些是死亡的,只有标记为死亡的对象,才会被回收,释放占用的资源这个阶段称为垃圾标记阶段。jvm中判断一个对象是否存活就是判断有没有存活的对象引用它,如果没有,就可以判断为死亡,判断的方式有两种,一种是可达性分析一种是引用计数算法

引用计数法:引用计数法比较简单,就是对没一个引用保存一个整形的计数器,用于记录对象被引用的情况,有一个引用指向该对象就+1,引用实效时就-1,只要引用计数器的值为0 则表示该对象可以被回收

优点:实现简单,垃圾对象便于标识,回收没有延时性

缺点:需要单独的存储计数器,比较浪费空间,在对象引用或者取消引用的时候会进行算术运算,浪费时间,最主要的就是没有办法解决循环引用的问题。

可达性分析算法(根搜索算法)

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

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

对象的finalization机制

  • finalization机制是可以允许开发人员在对象销毁之前做一些自定义的逻辑处理
  • 当垃圾收集器收集垃圾对象之前会调用这个对象的 finalize() 方法,该方法是Object的一个方法,可以在子类重写,通常是用来关闭资源和清理的工作。
  • 不要主动的去调用对象的finalize()方法,原因有三点:
    • 1.执行finalize()方法时,对象可能会复活。
    • 2.finalize()方法的执行时间没有保障,完全是有GC线程决定,若不发生GC 则finalize()方法将永远不会执行
    • 3.如果finalize()方法逻辑不严谨,严重的影响GC的性能
  • 由于finalize()方法的存在,对象存在三种状态,可触及 可复活 不可触及
    • 1.可触及:从根节点可以达到这个对象
    • 2.可复活:对象的所有引用都被垃圾收集器回收,但是对象可能在finalize()方法复活
    • 3.不可触及:对象的finalize()方法只能被调用一次,如果finalize()方法被调用了,但是对象没有复活,才能被回收
  • 只有对象是不可触及状态的才可以被回收

标记清除算法

执行过程

当堆中的空间不足的时候,就会停止整个程序(stop the world)然后进行两个工作,一个是吧标记,一个是清除, 标记Collector从引用根节点开始遍历,标记所有存在被引用的对象, 清除Collector对堆内存从头到尾进行线性遍历,如果发现对象没有被标记,就将该对象回收。

缺点
  • 标记清除算法的效率不高,要进行两次遍历
  • 在进行GC的时候,需要停止整个程序,体验差
  • 清理之后的内存是不连续的,产生了内存碎片,需要单独维护一个空闲列表

复制算法

核心思想

将内存空间分为两块,每次只使用其中的一块,在垃圾回收时,将正在使用区域中的所有活对象,复制到未使用的内存中,之后清除正在使用内存区域的对象,然后将两块区域角色互换。(新生代分为两个幸存者区)

优点
  • 没有标记清除的过程,实现起来简单,运行高效
  • 复制的对象能够保证内存的连续,不会出现内存碎片问题
缺点
  • 需要两个同样的内存空间,但是只有一块区域存数据,空间浪费
  • 如果存活的对象数量过大效率低

标记压缩算法

执行过程

第一阶段和标记清除算法一样,都是从根节点开始遍历,标记存在引用的对象,第二阶段就将所有存活的对象压缩到内存的一短,按顺序排放,最后清除掉最后一个排放后面的所有空间

标记压缩和标记清除算法的区别
  • 标记压缩可以理解为在标记清除之后,在进行一次压缩。
  • 标记清除不用更改对象的内存地址,标记压缩需要更改内存地址
  • 标记压缩内存比较规整,不用单独维护一个空闲列表
优点
  • 解决了标记清除算法中的内存碎片问题
  • 解决了复制算法中的内存浪费问题
缺点
  • 从效率上来说,比标记清除和复制算法都要低
  • 如果移动的对象被其他对象引用,还需要调整引用的地址
  • 移动过程中需要停止用户线程(Stop The World)

总结

标记清除 标记压缩 复制算法
速度 中等 最慢 最快
空间 少(存在内存碎片问题) 少(不存在内存碎片问题) 多(需要两个同样大小的空间,但是只能存放一半内存的对象)
移动对象

分代收集算法

  • 前面所有这些算法中,并没有一种算法可以完全替代其他算法,它们都具有自己独特的优势和特点。分代收集算法应运而生。
  • 分代收集算法,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。一般是把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点使用不同的回收算法,以提高垃圾回收的效率。
  • 在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。

目前几乎所有的 GC 都采用分代收集算法执行垃圾回收的。

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

  • 年轻代(Young Gen)

年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。

这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过 HotSpot 中的两个 Survivor 的设计得到缓解。

  • 老年代(Tenured Gen)

老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。

这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记-整理的混合实现。

  • Mark 阶段的开销与存活对象的数量成正比。
  • Sweep 阶段的开销与所管理区域的大小成正相关。
  • Compact 阶段的开销与存活对象的数据成正比。
赞(0) 打赏

评论 抢沙发