垃圾回收的概述
什么是垃圾
- 垃圾是指在程序运行中,没有任何指针指向的对象,这个对象就需要被垃圾回收
- 如果不及时的回收这些垃圾,这些对象所占用的内存空间就会一直保留到程序结束,被保留的空间无法被其他对象使用,就会导致内存溢出
为什么需要GC
- 对于高级语言来说,一个基本的认知是,如果不进行垃圾回收,内存迟早会消耗完,因为会不断的分配新的空间。
- 除了释放没用的对象之外,垃圾回收也可以清除内存的记录碎片,碎片整理将堆内存的空间连续,方便存储比较大的对象
- 没有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 阶段的开销与存活对象的数据成正比。