文章目录
标记阶段:判断哪些是垃圾?
清除阶段:怎么回收垃圾?
1、标记阶段:引用计数算法(Hotspot虚拟机不用这个算法标记)
- 判断对象存活
- 引用计数算法
- 引用计数算法无法解决循环引用
4. 小结
(1)引用计数算法,是很多语言的资源回收选择,例如因人工智能而更加火热的Python,它更是同时支持引用计数和垃圾收集机制。
(2) 具体哪种最优是要看场景的,业界有大规模实践中仅保留引用计数机制,以提高吞吐量的尝试。
(3)Java并没有选择引用计数,是因为其存在一个基本的难题,也就是很难处理循环引用关系。
(4)Python如何解决循环引用?
手动解除:很好理解,就是在合适的时机,解除引用关系。
使用弱引用weakref(你给一个对象标记为弱引用,那么这个对象就不会增加它的引用计数), weakref是Python提供的标准库,旨在解决循环引用。
2、标记阶段:可达性分析算法(或根搜索算法、追踪性垃圾搜索)
(1)可达性分析算法
- 相对于引用计数算法而言,可达性分析算法不仅同样具备实现简单和执行高效等特点,更重要的是该算法可以有效地解决在引用计数算法中循环引用的问题,防止内存泄漏的发生。
- 相较于引用计数算法,这里的可达性分析就是Java、C#选择的。这种类型的垃圾收集通常也叫作追踪性垃圾收集(Tracing Garbage Collection)。
(2)GC Roots
- 所谓"GC Roots "根集合就是一组必须活跃的引用。
●基本思路:
➢可达性分析算法是以根对象集合(GC Roots)为起始点,按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
➢使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间中接连接着,搜索所走过的路径称为引用链(Reference Chain)
➢如果目标对象没有任何引用链相连,则是不可达的,就意味着该对象己经死亡,可以标记为垃圾对象。
➢在可达性分析算法中,只有能够被根对象集合直接或者间接连接的对象才是存活对象。
3.临时加入到GC Roots的对象(高级点)。
图解上述情况:
3. 注意
(1) 如果要使用可达性分析算法来判断内存是否可回收,那么分析工作必须在一个能保障一致性的快照中进行。这点不满足的话分析结果的准确性就无法保证。
●这点也是导致GC进行时必须“Stop The World(STW)"的一个重要原因(在GC时,需要将用户线程停止,因为GC期间进行可达性分析时,不能让对象“一瞬间被引用,一瞬间又没有被引用了。刚刚已经被分析了的对象,又新的指针指向它了”。)。
➢即使是号称(几乎)不会发生停顿的CMS收集器中,枚举根节点时也是必须要停顿的。
3、对象的finalization机制(课程P142、面试题)
-
具体过程
使用jvisualvm工具查看Finalizer线程:
-
代码演示
public class Final {
private static Final obj=null;
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用了当前类重写的finalize方法");
obj=this;//将待回收的对象在finalize()方法与引用链上的一个对象obj建立了联系;
}
public static void main(String[] args) throws InterruptedException {
obj=new Final();
obj=null;
System.gc();//第一次gc
System.out.println("第一次GC");
//因为finalizer线程优先级低,让主线程停留两秒。
Thread.sleep(2000);
if(obj==null){
System.out.println("obj is dead");
}else {
System.out.println("obj is still alive");
}
obj=null;
System.gc();//第二次gc
System.out.println("第二次GC");
Thread.sleep(2000);
if(obj==null){
System.out.println("obj is dead");
}else {
System.out.println("obj is still alive");
}
}
}
结果:可以看出对象在第一次gc后,又复活了,第二次gc才消灭它。
4、MAT与Jprofiler的GC朔源
(1)MAT的GC root溯源
测试代码如下:
package com.yiheng.gc;
import java.util.ArrayList;
import java.util.Scanner;
public class GcTest {
public static void main(String[] args) {
ArrayList<Object> list = new ArrayList<>();
Object obj = new Object();
for (int i = 0; i < 100; i++) {
list.add(String.valueOf(i));
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("添加完毕请指示");
new Scanner(System.in).next();
list=null;
obj=null;
System.out.println("清空完毕,请指示!");
new Scanner(System.in).next();
System.out.println("程序结束!");
}
}
第一步:导处dump文件(此处利用jvisualvm导出)
第二步:打开mat(MemoryAnalyzer)工具
- 使用mat打开dump文件
- 如下点击
- 查看前快照有哪些GC Root。
(2)使用Jprofiler工具查看GC Root。
第一步:点击Idea右上角的Jprofiler运行当前程序
第二步:点击Live memory
第三步:点击Mark Current Value,作用定格在当前时间
第四步:
第五步:
第六步:
第七步:
第八步:查看字符串“完毕请指示”的GC Root
5、清除阶段:标记-清除(Mark一Sweep)算法
(1)垃圾清除阶段
- 当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间,以便有足够的可用内存空间为新对象分配内存。
- 目前在JVM中比较常见的三种垃圾收集算法是标记一清除算法(Mark-Sweep )、复制算法( copying )、标记–压缩算法( Mark-Compact ) 。
(2)标记—清除(Mark一Sweep)算法
-
当堆中的有效内存空间(available memory)被耗尽的时候,就会停止整个程序(也被称为stop the world),然后进行两项工作,第一项则是标记,第二项则是清除。
-
·标记:Collector从引用根节点开始遍历,标记所有被引用的对象(标记的不是垃圾)。一般是在对象的Header中记录为可达对象。
-
·清除:collector对堆内存从头到尾进行线性(一个一个)的遍历,如果发现某个对象在其Header(对象头)中没有标记为可达对象,则将其回收。
效率不高
在进行GC时,需要停止应用程序,用户体验性极差
这种方式清理出来的空闲区间是不连续的,产生了内存碎片,需要维护一个空闲列表。
这里的清除不是把该内存区域置空,而是把需要清除的对象地址保存在空闲的地址列表里面,下次有对象需要加载时,判断垃圾空间是否足够,如果够直接放,(覆盖掉了原来的垃圾对象)。(电脑的磁盘也是一样的道理,当我们把一个盘格式化以后,我们盘里面的数据其实还没有丢失,只要我们不再格式化之后的盘里面放新的文件,就可以用工具把盘复原)
回顾:用标记清除算法的话可以用”空闲列表”的方式给对象分配空间。
给对象分配空间的两大的方法
1、维护一个空闲列表
2、使用指针碰撞
6、清除阶段:复制算法(Copying)
- 背景
- 核心思想:
将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
没有标记(注意:在可达性分析时,把可达对象直接复制一份到另一半中,不需要所谓的标记)和清除过程,实现简单,运行高效
复制过去以后保证空间的连续性,不会出现“碎片”问题。
缺点:
- 此算法的缺点也是很明显的,就是需要两倍的内存空间。
- 对于G1这种分拆成为大量region的GC(jdk默认使用的), 复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销也不小。
特别地:
如果再一次GC中的存活的对象很多,这个算法的使用就不太理想,(因为在复制了大量的对象后,会改变对象的地址,导致许多引用会发生改变,开销很大。);所以复制算法需要复制的存活对象数量并不会太大,或者说非常低才行。
- 新生代就使用复制算法
回顾:用复制算法回收垃圾的区域因为空间是连续的,所以这个区域可以用指针碰撞给对象分配空间。
7、清除阶段:标记-压缩算法(标记一整理算法、Mark一Compact)
-
背景
- 标记压缩算法优缺点:
优点:
消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有一个内存的起始地址即可。
消除了复制算法当中,内存减半的高额代价。
缺点:
从效率上来说,标记-整理算法要低于复制算法。
移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址。
移动过程中,需要全程暂停用户应用程序。即: STW
回顾:老年代可以用这种算法清理垃圾,不需要复制算法的两倍空间,也不会像标记-清除算法造成空间碎片化,再给老年区分配对象时,使用指针碰撞的方式,且有利于存放大对象。
8、小结
Mark-Sweep | Mark-Compact | Copying | |
---|---|---|---|
速度 | 中 | 慢 | 快 |
空间开销 | 少(会产生空间碎片) | 少(不会产生空间碎片) | 需要2倍大小空间(不会产生空间碎片) |
会移动对象吗 | 不会 | 会 | 会 |
从效率上讲,复制算法是当之无愧的老大!
标记压缩算法:虽然兼顾了空间开销小、不会产生碎片化空间,但是效率上不尽人意!
9、清除阶段:分代收集算法
在以HotSpot中的CMS(回收老年代的)回收器为例,CMS 是基于Mark- Sweep实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致的Concurrent ModeoFailure时),将采用Serial Old执行Full GC以达到对老年代内存的整理。分代的思想被现有的虚拟机广泛使用。几乎所有的垃圾回收器都区分新生代和老年代
10、清除阶段:增量收集算法、分区算法
(1)增量收集算法:
- 背景:
上述现有的算法,在垃圾回收过程中,应用软件将处于一种stop the world的状态。在stop the world 状态下,应用程序所有的线程都会挂起,暂停一切正常的工作,等待垃圾回收的完成。如果垃圾回收时间过长,应用程序会被挂起很久,将严重影响用户体验或者系统的稳定性。为了解决这个问题,即对实时垃圾收集算法的研究直接导致了增量收集(Incremental collecting)算法的诞生。 - 基本思想:
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。总的来说,增量收集算法的基础仍是传统的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作。 - 缺点:
使用这种方式,由于在垃圾回收过程中,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是因为线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。
(2)分区算法:
- 背景:
堆空间越大,一次GC时间就越长,有关GC产生的停顿就越长,影响用户使用。 - 思想:我们讲一块大的内存区域分成许多小的区间,根据目标的停顿时间,每次合理的回收若干小区间,而不死这个堆空间,从而减少一次GC的停顿时间,给用户更好的体验。但是会导致频繁切换用户线程和GC线程,导致系统吞吐量会下降。
- 好处:每一个小区间都独立使用,独立回收,这种算法的好处是可以控制一次回收多少个小的区间。
名称 | 区别 |
---|---|
分代收集算法 | 分区收集算法 |
按照对象的生命周期长短划分为年轻代与老年代 | 将整个堆空间分成连续的若干个的小空间 |