垃圾回收算法
什么是垃圾?当一个对象没有被引用时就会被视为垃圾而回收。
内存泄露:申请内存空间而没有被正确释放,造成该块内存空间不可达;
内存溢出:存储数据超出了内存的额定大小,如栈溢出 stackoverflow。
1,垃圾回收的基本策略:
引用计数法:
通过计算器机制:对象引用一次+1,引用实效一次-1;为0则回收。但是这样无法解决循环依赖问题。
可达性分析法:
GC Roots所引用的对象为有限对象,这里的引用包括:
- Java 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈中引用的对象
- 方法区中常量引用的对象
- 方法区中类静态属性引用的对象
由于不包括堆中的对象,所以此种方式不会涉及循环依赖问题。
引用类型:
- 强引用:所有new出来的对象,只要引用存在,都不会被回收,即便JVM内存溢出;默认引用类型
- 软引用:内存不足时会被回收;应用在高速缓存等
- 弱引用(WeakReference):只要发动垃圾回收,都会被回收
- 虚引用(Phantom Reference):随时被回收,设置目的是跟踪垃圾回收过程。
2,方法区内的垃圾回收:
我们知道,方法区内主要存放生命周期较长的类信息、常量、静态变量等。因此方法区主要回收的垃圾有两类:
- 废弃常量:没有被常量或对象引用的;
- 无用的类:
- 该类的所有对象已经被清除;
- 加载该类的 ClassLoader 已经被回收;
- 该类的 java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
3,堆内的垃圾回收:
堆内不同的分区根据对象的生命周期不同会采用不同的回收策略,即分代收集策略:
3.1,新生代-复制算法-Minor GC
原理很简单,为了解决碎片问题,将内存分为等额的两份,垃圾回收时将一份分区内存活的对象复制到另一份分区,然后将原先分区全部回收即可。
但这样的缺点就是内存使用率不足,仅为原来的一半。为了改进此问题,可以将内存分为按8:1:1的形式分为Eden、From Survivor、To Survivor三个区,每次将Eden和一个Survivor分区的存活对象复制到另一个Survivor内,从而使得内存浪费率仅为10%。当然即便新生代的存活对象较少,当10%的内存不够时需要借助老年代进行分配担保。
3.2,老年代-标记清除和标记整理算法-Major GC
复制算法的高效性是建立在存活对象少、垃圾对象多的前提下的。这种情况在新生代经常发生,但是在老年代,更常见的情况是大部分对象都是存活对象。如果依然使用复制算法,由于存活对象较多,复制的成本也将很高。因此,基于老年代垃圾回收的特性,需要使用其他的算法。
3.3,标记清除算法
标记:遍历所有的 GC Roots
,然后将所有 GC Roots
可达的对象标记为存活的对象。
清除:遍历堆中对象,没有标记的即为垃圾;同时去掉标记对象的标记(不保证下次不被回收)。
不足:效率不高;标记清除导致空间内存碎片较多,不利于下次大对象分配而会导致提前触发垃圾回收机制。
3.4,标记整理算法
标记方式同样是遍历所有的GC Roots
,然后将所有 GC Roots
可达的对象标记为存活的对象。随后整理策略是移动存活的对象,并按照内存地址排序,从而可以将末端地址的内存回收。注意在移动过程中需要暂停用户的应用程序(Stop The World,STW)
为了解决垃圾回收时系统应用线程挂起的对的情况,增量收集算法诞生。其基本思想是,垃圾回收线程和应用线程交替执行,以保持系统的稳定性。但是线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
参考:https://doocs.gitee.io/jvm/#/docs/03-gc-algorithms
4,其他
对象在被视为垃圾回收之前,还有没有在挽回的余地呢?答案是有。Java 提供了一个对象的finalization机制,使用finalize()方法可以自定义对象在回收之前的处理逻辑,且该方法只会被调用一次。
我们知道对象的引用关系是有很大的不确定性的,当前行存活,下一行就有可能被回收。为此,GC的时间点和作用点都要有一定的要求。实际上,程序在执行时并非在所有地方都能停顿下来GC,能够停顿下来GC的地方我们称之为安全点(比如方法调用、循环跳转和异常跳转等,确保此处不会发生对象的引用关系变化)相应的,还有安全区的概念,即当前一段的代码片都是安全的。
补充:垃圾回收器
主要分类:
-
并发式垃圾回收器与应用程序线程交替工作,以尽可能减少应用程序的停顿时间。
-
独占式垃圾回收器(Stop the world)一旦运行,就停止应用程序中的所有用户线程,直到垃圾回收过程完全结束。
垃圾收集器的评价指标:
-
吞吐量:运行用户代码的时间占总运行时间的比例(总运行时间 = 程序的运行时间 + 内存回收的时间)
-
垃圾收集开销:吞吐量的补数,垃圾收集所用时间与总运行时间的比例。
-
暂停时间:执行垃圾收集时,程序的工作线程被暂停的时间。
-
收集频率:相对于应用程序的执行,收集操作发生的频率。
-
内存占用:Java堆区所占的内存大小。
-
快速:一个对象从诞生到被回收所经历的时间。
分析:高吞吐量和低暂停时间是相互矛盾的。垃圾一定的情况下,高吞吐量意味着会降低内部的回收频率,这就导致GC需要更多的暂停时间执行内存回收。相应的,如果为了低延迟,会频繁回收内存,容易导致吞吐量下降。