文章目录
五、垃圾收集(GC)算法
最近在学习 JVM 的知识,作为一个 Java 开发人员不懂 JVM 实在是不应该,毕竟底层基础决定上层建筑,而作为 JVM 里面的重点之一必然是垃圾收集(GC)了,那今天就带大家一起来了解下这是辣鸡,竟然如此的出名。(这篇也还是放在《算法撕裂者》系列吧,毕竟带"算法"两字,就水一波吧)
0、java对象内存申请过程
- JVM会试图为相关Java对象在Eden中初始化一块内存区域;当Eden空间足够时,内存申请结束。否则到下一步;
- JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;
- Survivor区被用来作为Eden及old的中间交换区域,当old区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;
- 当old区空间不够时,JVM会在old区进行major collection;
- 垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现"Out of memory错误";
1、stop the world
在新生代进行的GC叫做minor GC,在老年代进行的GC都叫major GC,Full GC同时作用于新生代和老年代。在垃圾回收过程中经常涉及到对对象的挪动(比如上文提到的对象在Survivor 0和Survivor 1之间的复制),进而导致需要对对象引用进行更新。为了保证引用更新的正确性,Java将暂停所有其他的线程,这种情况被称为“Stop-The-World”,导致系统全局停顿。Stop-The-World对系统性能存在影响,因此垃圾回收的一个原则是尽量减少“Stop-The-World”的时间。
不同垃圾收集器的Stop-The-World情况,Serial、Parallel和CMS收集器均存在不同程度的Stop-The-Word情况;而即便是最新的G1收集器也不例外。
- Java 中一种全局暂停的现象,JVM挂起状态
- 全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互
- 多半由于JVM的GC引用,如:
- 老年代空间不足
- 永生代(jkd7)或者元数据空间(jkd8)不足。
- System.gc()方法调用。
- CMS GC时出现promotion failed和concurrent mode failure
- YoungGC时晋升老年代的内存平均值大于老年代剩余空间
- 有连续的大对象需要分配
- 除了GC还有以下原因:
- 死锁检查
- Dump线程–人为因素
- 堆Dump–人为因素
关于stop the world 可查看: https://www.jianshu.com/p/d686e108d15f
2、如何判断对象是否“死去”?
在堆里面存放着Java中几乎所有的对象实例,垃圾收集器在对堆进行回收之前,第一件事情就是要确定哪些对象还存活着,哪些对象已经死去(即不可能在被任何途径使用的对象)。
- 引用计数算法
- 可达性分析算法
a、引用计数算法
引用计数法顾名思义,就是对一个对象被引用的次数进行计数,当增加一个引用计数就加1,减少一个引用计数就减1。
图片来自 https://www.cnblogs.com/leefreeman/p/7389919.html
上图表示3个Teacher的引用指向堆中的Teacher对象,那么Teacher对象的引用计数就是3,以此类推Student对象的引用计数就是2。
上图表示Teacher对象的引用减少为2,Student对象的引用减少为0(减少的原因是该引用指向了null,例如teacher3=null),按照引用计数算法,Student对象的内存空间将被回收掉。
引用计数算法原理非常简单,是最原始的回收算法,但是java中没有使用这种算法,原因有2:
-
频繁的计数影响性能,
-
它无法处理【循环引用】的问题。
例如Teacher对象中引用了Student对象,Student对象中又引用了Teacher对象,这种情况下,对象将永远无法被回收。
b、可达性分析算法
这个算法的基本思想就是,通过一系列称为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为【引用链】(Reference Chain),当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说,就是从 GC Roots 到这个对象不可达)时,则证明此对象时不可用的。
在 Java 语言中,可作为 GC Roots 的对象包括一下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI (即一般说是 Native 方法)引用的对象
c、再谈引用
在 JDK 1.2 之后,Java 对引用的概念进行了扩充,将引用分为
- 强引用(Strong Reference)
- 软引用(Soft Reference)
- 弱引用(Weak Reference)
- 虚引用(Phantom Reference)
-
强引用
代码中普遍存在的,类似 “ Object obj = new Object() ” 这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。
-
软引用
用来描述一些还有用但非必须的对象。软引用所关联的对象,在系统将要发生内存溢出异常之前,将会把这些对象列入回收范围,并进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。提供了
SoftReference
类实现软引用。 -
弱引用
描述非必须的对象。强度比软引用更弱一些,被弱引用关联的对象,只能**生存到下一次垃圾收集发生之前。**当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。提供了
WeakReference
类实现弱引用。 -
虚引用
一个对象是否有虚引用,完全不会对其生存时间构成影响,也无法通过一个虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的,就是能在这个对象被收集器回收时收到一个系统通知。通过了
PhantomReference
类实现虚引用。
3、标记-清除算法(Mark-Sweep)
什么是标记-清除算法
分为【标记】和【清除】两个阶段。首先标记处所有需要回收的对象,在标记完成后统一回收被标记的对象。
有什么缺点?
- 效率问题:标记和清除的效率都不高
- 空间问题:标记清除后会产生大量**【不连续的内存碎片】,空间碎片太多可能导致,程序分配较大对象**时无法找到足够的连续内存,不得不提前出发另一次垃圾收集动作。
4、复制算法(Copying)- 新生代
将可用内存按容量划分为【大小相等的两块】,每次只使用其中一块。当这一块的内存用完了,就将存活着的对象复制到另一块上面,然后再把已经使用过的内存空间一次清理掉。
优点?
复制算法使得每次都是针对其中的一块进行内存回收,内存分配时也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
缺点?
将内存缩小为原来的一半。在对象存活率较高时,需要执行较多的【复制操作】,效率会变低。
应用?
商业的虚拟机都采用复制算法来回收新生代。因为新生代中的对象容易死亡,所以并不需要按照1:1的比例划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间。每次使用 Eden 和其中的一块 Survivor。
当回收时,将 Eden 和 Survivor 中还存活的对象一次性拷贝到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。**Hotspot 虚拟机默认 Eden 和 Survivor 的大小比例是8:1,**也就是每次新生代中可用内存空间为整个新生代容量的90%(80% + 10%),只有10%的内存是会被“浪费”的。
5、标记-整理算法(Mark-Compact)- 老年代
标记过程仍然与“标记-清除”算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象向一端移动,然后直接清理掉边界以外的内存。
优点?
经过整理(压缩),即再次扫描,并往一端**【滑动】**存活对象,没有内存碎片
缺点?
需要移动对象的成本
6、分代收集算法
根据对象的存活周期,将内存分为几块。一般是把【Java堆】分为【新生代】和【老年代】, 这样就可以根据各个年代的特点,采用最适当的收集算法。
- 新生代:每次垃圾收集时会有大批对象死去,只有少量存活,所以选择【复制算法】,只需要少量存活对象的复制成本就可以完成收集。
- 老年代:对象存活率较高,没有额外空间对它进行分配担保,必须使用【标记-清除】或【标记-整理】算法进行收集。
参考资料:
《深入理解Java虚拟机(第2版)》 周志明