文章目录
垃圾标记阶段
引用计数算法
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况。对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效的时候,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
- 缺点:
(1)需要单独的字段存储计数器,这样的做法增加了存储空间的开销。
(2)每次赋值都要更新计数器,伴随着加法和减法操作,增加了时间开销。
(3)引用计数器无法处理循环引用的情况,这是一个致命的缺陷,导致在Java的垃圾回收器中没有使用此类算法。
可达性分析算法(根搜索算法、追踪性垃圾收集)
该算法可以有效的解决在引用计数算法中循环引用的问题,防止内存泄露的发生。
基本思路:
- 该算法是以根对象集合
GC Roots
为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达**。 - 使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径称为引用链。
- 如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象已经死亡,可以标记为垃圾对象。
- 在可达性分析算法中,只有能够被根对象集合直接或间接连接的对象才是存活的对象。
GC Roots
根集合就是一组必须活跃的引用。
在Java语言中,GC Roots
包括以下几类元素:
- 虚拟机栈中引用的对象
比如:各个线程被调用的方法中使用到的参数、局部变量等。 - 本地方法栈内JNI(通常说的本地方法)引用的对象
- 方法区中静态属性引用的对象
Java类的引用类型静态变量 - 方法区中常量引用的对象
字符串常量池里的引用 - 所有被同步锁
synchronized
持有的对象 - Java虚拟机内部的引用
基本数据类型对应的Class
对象,一些常驻的异常对象(如NullPointerException、OutOfMemoryError
),系统类加载器。 - 反应java虚拟机内部情况的
JMXBean、JVMTI
中注册的回调、本地代码缓存等。
除了这些固定的GC Roots
集合以外,根据用户所选用的垃圾收集器以及当前回收内存区域的不同,还可以有其他对象“临时性”的加入,共同构成完整的GC Roots
集合。比如:分代收集和局部回收。 小技巧:由于Root
采用栈方式存放变量和指针,所以如果一个指针,他保存了堆内存里面的对象,但是自己又不存放在堆内存里面,那么他就是一个Root
。
垃圾清除阶段
标记-清除算法(Mark-Sweep)
执行过程:
当堆中的有效空间内存被耗尽的时候,就会停止整个程序(Stop the world
),然后进行两项工作,标记和清除。
- 标记:
Collector
从根节点开始遍历,标记所有被引用的对象,一般是在对象的Handler
中记录为可达对象。 - 清除:
Collector
对堆内存从头到尾进行线性的遍历,如果发现某个对象在其Handler
中没有标记为可达对象,则将其回收。
缺点:
- 效率不算高
- 在进行
GC
的时候,需要停止整个应用程序,导致用户体验差。 - 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表。这里的清除并不是真的置空,而是把需要清除的对象地址保存在空闲列表中,下次有新对象需要加载的时候,判断垃圾的位置空间是否够用,如果有,则存放,进行覆盖。
复制算法
核心思想:
将活着的内存空间分成两块,每次只使用其中的一块,在垃圾回收的时候将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存中的角色,最后完成垃圾回收。
优点:
- 没有标记和清除的过程,实现简单,运行高效。
- 复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
- 需要两倍的内存空间。
- 对于G1这种分拆成为大量
region
的GC,复制而不是移动,意味着GC需要维护region
之间对象引用关系,不管是内存占用或者时间开销也不小。
标记-压缩算法(Mark-Compact)
执行过程:
第一阶段和标记-清除算法一样,从根节点开始标记所有被引用对象。第二阶段将所有的存活对象压缩到内存的一端,按顺序排放。之后,清理边界外所有的空间。
标记-压缩算法的最终效果等同于标记-清除算法执行完成之后,再进行一次内存碎片的整理,因此,也可以称为标记-清除-压缩算法。二者的本质差别是标记-清除算法是一种非移动式的回收算法,标记-压缩算法是一种移动式的回收算法。
标记的存活对象将会被整理,按照内存地址以此排列,而未被标记的内存将会被清理掉,所以,当我们需要给新对象分配内存时,JVM需要持有一个内存的起始地址即可,这比维护一个空闲列表少了很多的开销。
优点:
- 消除了标记-清除算法当中,内存区域分散的特点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
- 消除了复制算法中,内存减半的高额代价。
缺点:
- 从效率上说,标记整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
- 移动过程中,需要全程暂停用户应用程序。
方法对比
标记-清除 | 标记-压缩 | 复制 | |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(但会堆积碎片) | 少(不堆积碎片) | 通常需要活对象的2倍大小(不堆积碎片) |
移动对象 | 否 | 是 | 是 |
分代收集算法
不同对象的生命周期是不一样的,因此不同生命周期的对象可以采用不同的收集方式,以便提高回收效率。目前几乎所有的GC都是采用分代收集算法执行垃圾回收的。GC所使用的内存回收算法必须结合年轻代和老年代各自的特点:
- 年轻代:
区域相对老年代较小,对象生命周期短,存活率低,回收频繁。
这种情况复制算法的回收整理速度是最快的。复制算法的效率只与当前存活对象大小有关,因此很适用于年轻代的回收,而复制算法内存利用率不高的问题,通过hotspot
中的两个survivor
的设计得到了缓解。 - 老年代:
区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。
这种情况下存在大量存活率高的对象,复制算法明显不合适,一般是由标记-清除或者标记-清除与标记-整理的混合实现。
标记阶段的开销与存活对象的数量成正比。
清除阶段的开销与所管理区域的大小成正比。
压缩阶段的开销与存活对象的数量成正比。
增量收集算法
在垃圾回收的过程中,应用软件将处于一种Stop the World
的状态,在此状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成,如果垃圾回收时间过长,应用程序就会挂起很久,将严重影响用户体验或系统的稳定性。
基本思想:
如果一次性将所有的垃圾记性处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程,依次反复,直到垃圾收集完成。
该算法的基础是标记-清除和复制算法,该算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。
缺点:
由于在垃圾回收的过程中,间断性的还执行了应用程序代码,所以能减少系统的停顿时间,但是,因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
分区算法
一般来说,在相同的条件下,堆空间越大,一次GC时所需要的时间就越长,有关GC停顿的也越长,为了更好的控制GC产生的时间停顿时间,将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间,而不是整个堆空间,从而减少一次GC所产生的停顿。每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个小区间。