垃圾回收
文章目录
垃圾回收概述
java语言和C++语言的区别,就在于垃圾收集技术和内存动态分配上,C++语言没有垃圾收集技术,需要开发人员手动的收集。垃圾收集并不是java的产物。
垃圾回收的三大经典问题:
那些内存需要回收?
什么时候回收?
如何进行回收?
什么是垃圾?
垃圾是指运行程序中没有任何引用指向的对象,需要被回收。
为什么要回收垃圾?
如果垃圾不及时回收清理可能会造成内存溢出。
在回收的时候进行碎片整理,将所占据的堆内存移到堆的一端,有利于新对象加载。
内存溢出和内存泄漏
内存溢出:经过垃圾回收之后,内存仍旧无法存储新创建的对象,内存不够溢出。
内存泄漏:又叫“存储泄漏”,对象不会在被程序使用了,但是GC又不能回收他们。例如:IO流不适用了但是没有被close、数据库连接JDBC没有被close。这些对象不会被回收就会占据内存,大量的此类对象存在,也是导致内存溢出的原因。
java垃圾回收机制
自动内存管理
优点:将开发人员从繁重的内存管理释放出来,专心业务开发。无需开发人员手动参与内存分配与回收,这样降低内存溢出和内存泄漏的风险。
缺点:无需开发人员手动参与内存的分配和回收,将严重弱化了开发人员解决内存溢出问题的能力。
应该关心哪些区域的回收
垃圾回收器可以对年轻代会后,也可以对老年代回收,甚至可以对全栈和方法区进行回收,其中java堆是垃圾回收的重点区域。
一般性的:
频繁回收Young区,
较少回收Old区,
基本不回收方法区。
垃圾回收相关算法
垃圾标记阶段算法
标记阶段的目的:判断对象是不是垃圾对象。
堆中几乎所有的对象实例在回收之前都需要区分有用对象和垃圾对象,GC将会释放掉被标记为垃圾对象所占据的空间,这个过程称为垃圾标记阶段。
如何标记一个垃圾对象(什么样的对象能被标记为垃圾对象)?
当一个对象已经没有任何引用指向时,就表示它是垃圾对象。
判断对象是否为垃圾对象一般有两种方式:引用计数算法和可达性分析算法。
引用计数算法
对每个对象保存一个引用计数器属性,用来记录被引用状况。
对于一个对象A来讲,只要有一个引用指向了A,那么A的引用计数器就加1;当引用失效的时候,引用计数器就减1。只有对象A的引用计数器的值为零时,就表示对象A不可能在被使用,为垃圾对象,可以被回收了。
优点:实现简单,垃圾对象便于识别,判定效率高,回收没有延迟。
缺点:
-
需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
-
每次赋值都需要更新计数器,伴随这计数器的操作,这样也就增加了时间开销。
-
引用计数器无法解决循环引用的情况。
循环引用就是指:class A{ public B b; }
class B{ public A b; }
class C{ public static void main(String[] args){ A a = new A(); B b = new B(); a.b = b; b.a = a; } }
函数结尾,a、b的计数器都是2。先撤销a,然后a的计数器为1,在等待b.a对a的撤销,也就是等待b的撤销,b也是同理。两个对象都在等待对方撤销,都不能释放。这样会造成内存泄漏。
可达性分析算法
可达性分析算法又称为根搜索算法、追踪性垃圾收集
相比于引用计数算法,可达性分析算法不仅同样实现简单和执行高效,而且解决了循环引用问题。
实现思路
可达性分析算法是以根(GCRoots)为起始点,按照从上至下的方式搜索被根对象所连接的目标对象。
使用可达性分析算法后,内存中存活的对象都会被跟对象直接或间接连接着,搜索所走过的路径为引用链
如果目标对象没有任何引用链相连接,则是不可达的对象,就可以标记为垃圾对象。
GC Roots可以是哪些元素?
- 虚拟机栈中引用的对象。比如:各个线程被调用的方法中使用到的参数、局部变量等。
- 方法区中静态属性引用的对象,比如:Java类中的引用类型静态变量。
- 被同步锁synchronized持有的对象。
- Java虚拟机内部的引用。
对象的finalization机制
finalize()方法机制
对象在真正销毁之前调用一次Object类下的finalize()方法(只能调用一次),看是否还有逻辑需要执行,其实finalize()方法中是可以复活某些垃圾对象的。复活的对象可以暂时不做回收但是下一次在进行回收时就不会调用finalize()方法了。
Object类中的finalize()方法的源码:
protected void finalize() throws Throwable { }
允许子类重写finalize()方法。
永远不要主动调用某个对象的finalize方法,因该交由垃圾回收机制调用。因为:
- finalize()可能会导致对象复活。
- finalize()在执行的时间没有保障,如果没有发生GC时,则finalize()方法将没有执行机会。
- 可能会印象GC的性能。
生存还是死亡?
因为finalize()方法的存在,虚拟机中的对象分为三种状态:
- 可触及的:从根节点开始可达对象。
- 可复活的:对象的所有引用都被释放,但是对象可以在finalize()方法中复活的。
- 不可触及的:对象的finalize()已经调用过的,并且没有复活的,就会进入不可触及状态。
只有进入不可触及状态才能被回收。
具体过程
如果对象A到GC Roots没有引用链,则进行一次标记。
进行筛选,看对象A有没有必要执行finalize()方法:
- 如果对象A没有重写finalize()方法,或者已经调用过了finalize()方法,则A就会被判定为不可触及的。
- 如果对象A重写了finalize()方法,并还没有执行,则A执行finalize()方法进行复活。
垃圾回收阶段算法
当成功区分内存中存活的对象和死亡对象后,就要执行垃圾回收,释放空间。目前JVM比较常见的三种垃圾收集算法是:
标记-复制算法
标记-清除算法
标记-压缩算法
标记-复制算法
将内存容量划分为两个大小相等的区域,每次只使用其中的一块,在垃圾回收时将内存中存活的对象复制到没有使用的内存块中,之后清除正在使用的内存块中的所有对象。
优点:没有标记和清除过程,实现简单,运行高效
复制到另一个内存块后保证空间连续性,不会出现碎片问题。
缺点:因为需要两个大小相等的内存区域,所以需要更多的空间。将存活的对象复制到另一个区域,需要更多的时间。
适用场景: 存活对象少 新生代适合使用标记复制算法。
标记-清除算法
将垃圾对象维护进一个空闲列表中,当有新的对象进来时,直接覆盖就行。
优点:实现简单。
缺点:效率不高,有碎片空间。
标记-压缩算法
复制算法的高效是在存活对象较少的情况下,使用与新生代。而老年代更多的是存活对象多,不适宜使用复制算法。
压缩算法:将所有存活对象压缩到内存的一端,之后清除边界外所有空间。
优点:消除了清除算法中内存区域分散的缺陷,只需要一个内存空间,消除了复制算法的空间浪费。
缺点:移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址移动过程中,需要全程暂停用户应用程序。
垃圾回收阶段算法小结
标记复制 | 标记清除 | 标记压缩 | |
---|---|---|---|
速率 | 最快 | 中 | 最慢 |
空间开销 | 两个大小相同的空间 | 少(会堆积碎片) | 少(不会碎片堆积) |
移动对象 | 是 | 否 | 是 |
Stop The World(STW)
STW:是指发生GC时,会有引用程序停顿的情况,这个停顿称为STW。
可达性分析算法中枚举根节点时会导致Java线程停顿。
为什么需要停顿?
- 分析工作需要在一个能确保一致性的快照中进行。
- 一致性是指分析阶段整个执行系统看起来像是被冻结了一样。
- 如果在分析过程中,线程在不断执行,将会有一些对象实例无法确定其状态,会导致漏标,错标的情况。
垃圾回收器
垃圾回收器是垃圾回收的实践者。
垃圾回收器分类
-
按照线程数量可以分为:**单线程(串行)垃圾回收器和多线程(并行)**垃圾回收器。
单线程垃圾回收器(serial):只有一个线程,用于简单场景,在进行垃圾回收时其他用户线程终止。多线程垃圾回收器(parallel):在多CPU时提高了效率,也是会暂停其他用户线程的。
-
按照工作模式可以分为:独占式垃圾回收器和并发式垃圾回收器。
独占式:垃圾回收时,其他用户线程暂停。
并发式:垃圾回收时,与其他用户线程并发执行。 -
按照内存区间可以分为:年轻代垃圾回收器和老年代垃圾回收器。
GC性能指标
吞吐量:运行用户代码占总运行时间的比例。
暂停时间:暂停时间越短越好。
Hotspot垃圾回收器
途中有虚线相连的两个回收器表示两个可以配合使用。
回收器所在区域就表示他们是新生代或者是老年代收集器。
CMS回收器
CMS(Concurrent Mark Sweep,并发标记清除)收集器为追求低停顿,在垃圾收集时使得GC线程和用户线程并发执行,不会是用户明显的感觉到卡顿。
垃圾回收过程
初始标记:独占执行,使用一条初始线程对所有与GC Roots相关联的对象进行标记。
并发标记:GC线程和用户线程并发执行。这里进行可达性分析,标记出垃圾对象。
重新标记:独占执行,使用多条标记线程并发执行,将刚才并发标记过程中新出现的垃圾对象标记出来。
并发清除:使用一条GC线程,GC线程和用户线程并发执行,清除刚刚标记的对象。
并发标记和并发清除耗时最长。
优点:可以作到并发收集
弊端: 使用标记清除算法,会产生内存碎片;并发执行影响到用户线程;无法处理浮动垃圾。
浮动垃圾:并发清理阶段用户线程还在运行,这段时间就可能产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。
三色标记算法
三色标记算法将对象的颜色分为了黑、灰、白三种。
黑色:确定存活的对象,例如GC Roots;
灰色:对象中还存在没有被扫描的对象,此对象还会在扫描一次。
白色:与黑色、灰色没有关联的对象,不可达对象,属于垃圾。
三色标记过程
- 确定GC Roots对象为黑色。
- 将于GC Roots直接关联的对象标记为灰色。
- 遍历灰色对象的引用,灰色对象标记为黑色,其引用标记为灰色。
- 重复3,直到没有灰色对象。
- 回收白色对象。
此过程稳定运行是建立在没有其他线程改变对象的条件下,但是并发标记时,用户线程依然运行,因此就会出现漏标,错标的情况。
漏标:黑色断开灰色
假设GC已经遍历到B对象了,但是此时用户线程执行了A.B=null,切断A到B的链接,本来执行了A.B=null 之后,B、D、E 都可以被回收了,但是由于 B 已经变为灰色,它仍会被当做存活对象,继续遍历下去,直到下一次回收,也算是浮动垃圾。
错标:黑色链接的对象,灰色断开了。
假设线程已经执行到了B,此时用户线程执行了:
B.D=null;//B 到 D 的引用被切断
A.xx=D;//A 到 D 的引用被建立
此时 GC 线程继续工作,由于 B 不再引用 D 了,尽管 A 又引用了 D,但是因为 A 已经标记为黑色,GC 不会再遍历 A 了,所以 D 会被标记为白色,最后被当做垃圾回收。
可以看到错标的结果比漏表严重的多,浮动垃圾可以下次 GC 清理,而把不该回收的对象回收掉,将会造成程序运行错误。
解决错标:
错标产生的条件:
- 灰色指向白色的引用全部断开
- 黑色指向白色的引用建立
原始快照:打破条件一。灰色指向白色的引用断开时,先将引用关系记录下来,扫描结束后再,以灰色为根再扫描一次。
增量更新:打破条件二。:当黑色指向白色的引用被建立时,就将这个新的引用关系记录下来,等扫描结束后,再以这些记录中的黑色对象为根,重新扫描一次。
G1(Garbage-First) 垃圾优先
将堆内存各个区又分成较小的多个区域, 对这些个区域进行监测,对某个区域中垃圾数量大的区域优先回收.
也是并发收集的.