算法 自动采集列表( Python如何解决引用计数算法的循环引用属性?(一) )
优采云 发布时间: 2021-11-28 07:00算法 自动采集列表(
Python如何解决引用计数算法的循环引用属性?(一)
)
15.1 标记阶段:引用计数算法
引用计数算法(Reference Counting)比较简单,为每个对象保存一个整数引用计数器属性,用于记录被引用对象的情况
对于一个对象A,只要有任何对象引用A,A的引用计数器就加1;当引用失效时,引用计数器减少1. 只要对象A的引用计数器的值为0,就表示对象A不能再使用,可以回收
优点:实现简单,易于识别;判断效率高,无延迟
缺点:需要单独的字段来存储计数器,增加了存储空间的开销
每次赋值都需要更新计数器,增加了时间开销
无法处理循环引用,这是一个致命的缺陷,Java的垃圾采集器中没有使用这种算法
测试Java中是否使用了引用计数算法
public class RefCountGC {
//证明java使用的不是引用计数算法
// 这个成员属性的唯一作用就是占用一点内存
private byte[] bigSize = new byte[5*1024*1024];
// 引用
Object reference = null;
public static void main(String[] args) {
RefCountGC obj1 = new RefCountGC();
RefCountGC obj2 = new RefCountGC();
obj1.reference = obj2;
obj2.reference = obj1;
obj1 = null;
obj2 = null;
// 显示的执行垃圾收集行为,判断obj1 和 obj2是否被回收?
System.gc();
}
}
概括
Python 支持引用计数和垃圾采集机制
Python 是如何解决循环引用的?
15.2 标记阶段:可达性分析算法
可达性分析算法(寻根算法、Tracing Garbage 采集)不仅具有实现简单、执行高效的特点,而且有效解决了引用计数算法中的循环引用问题,防止了内存泄漏的发生。
基本思想
GC Roots 可以是什么类型的元素?
总结
由于Root使用栈来存储变量和指针,如果一个指针将对象保存在堆内存中,但不存储在堆内存中,那么它就是一个Root。
如果要使用可达性分析算法来判断内存是否可回收,分析工作必须在能够保证一致性的快照中进行。如果不满足,则无法保证分析结果。这也会导致 GC“停止”。“世界”的一个重要原因,即使在声称没有暂停的cms采集器中,枚举根节点也必须暂停
15.3 对象的终结机制
对象的终结机制
生存还是死亡?
三种状态:
具体流程
判断一个对象objA是否可回收,至少需要两个标记过程:
如果对象objA到GC Roots之间没有引用链,则进行第一个标记过滤,判断是否需要为此独占执行finalize()方法
1. 如果对象objA没有refinallize()方法,或者finalize()已经被调用,虚拟机被认为“不需要执行”,objA被判断为不可达
2. 如果对象objA重新finalize()方法,并且没有被执行,那么objA会被插入到F-Queue队列中,自动创建一个虚拟机,一个低优先级的Finalizer线程触发它的finalize () 方法实现
3.finalize() 方法是对象逃脱死亡的最后机会。稍后,GC 会将 F-Queue 中的对象堆叠起来进行第二个标记。如果objA在finalize()方法中与引用链中的任何一个对象建立了连接,那么objA在第二次被标记时就会从“即将被回收”的集合中移除。之后,该对象将在没有引用的情况下再次出现。在这种情况下,finalize 方法将不会被再次调用,对象将直接变得不可达。
public class CanReliveObj {
//测试Object类中finalize()方法
// 类变量,属于GC Roots的一部分
public static CanReliveObj canReliveObj;
//该方法只能调用一次
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("调用当前类重写的finalize()方法");
canReliveObj = this;//当前待回收的对象与obj建立了联系
}
public static void main(String[] args) throws InterruptedException {
canReliveObj = new CanReliveObj();
canReliveObj = null;
System.gc();//调用垃圾回收器
System.out.println("-----------------第一次gc操作------------");
// 因为Finalizer线程的优先级比较低,暂停2秒,以等待它
Thread.sleep(2000);
if (canReliveObj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
System.out.println("-----------------第二次gc操作------------");
canReliveObj = null;
System.gc();
// 下面代码和上面代码是一样的,但是 canReliveObj却自救失败了
Thread.sleep(2000);
if (canReliveObj == null) {
System.out.println("obj is dead");
} else {
System.out.println("obj is still alive");
}
/*
*第一次gc alive
*第二次gc dead
*/
}
}
15.4 MAT 和 JProfiler GC Roots 的可追溯性
Memory Analyzer Tool at MAT 的缩写,是一个强大的 Java 堆内存分析器,用于发现内存泄漏和查看内存消耗。基于Eclipse开发,下载网站
获取转储文件
方法一:从命令行使用jmap
方法二:使用 JVisualVM 导出
使用 MAT 打开转储
在实际开发中,一般不会找到所有的GC Roots,而可能只是找到一个对象的整个链接,称为GC Roots traceability,可以使用JProfiler
确定导致 OOM 的原因
public class HeapOOM {
/*
*内存溢出排查
* -Xms8m -Xmx8m -XX:HeapDumpOnOutOfMemoryError
*/
// 创建1M的文件
byte [] buffer = new byte[1 * 1024 * 1024];
public static void main(String[] args) {
ArrayList list = new ArrayList();
int count = 0;
try {
while (true) {
list.add(new HeapOOM());
count++;
}
} catch (Exception e) {
e.getStackTrace();
System.out.println("count:" + count);
}
}
}
通过线程,可以定位到OOM出现的地方
15.5 清算阶段:Mark-Sweep算法(Mark-Sweep)
实施过程
当堆中的可用内存耗尽时,整个程序(stop the world)就会停止,然后执行两个任务。第一个是标记,第二个是清除。
什么是去除
这里的清空并不是真正的清空,而是将需要清空的对象的地址保存在空闲地址列表中。下次有新对象要加载时,判断垃圾位置空间是否足够,如果足够则存储
缺点
15.6 清洗阶段:复制算法(Copying)
实施过程
为了解决mark-sweep算法的缺点,将存活内存空间分成两块,每块只使用一个块,垃圾回收时将正在使用的内存中的存活对象复制到未使用的内存块中. 清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
将可到达的对象直接复制到另一个区域。复制完成后,A区没有任何作用,直接移除里面的对象。其实里面的新生代就使用了复制算法。
优势
缺点
应用场景
在新一代中,常规应用的垃圾回收通常可以一次回收70-99%的内存空间,回收非常划算。因此,目前的商用虚拟机使用这种采集算法来回收新一代。
15.7 清洗阶段:Mark-Compact(Mark-Compact)
实施过程
第一阶段和mark-sweep算法一样,从根节点标记所有被引用的对象
第二阶段将所有幸存的对象压缩成一段内存并按顺序排列
之后,清理边界外的所有空间
标记压缩算法的最终效果相当于执行标记扫描算法后,再次对内存进行碎片整理,因此也可以称为标记扫描压缩(Mark-Sweep-Compact)算法
两者的本质区别在于mark-sweep算法是非移动回收算法,mark-compression是移动的。回收后的残存物是否移动是一个有利有弊的冒险决定
标记的存活对象会根据内存地址进行排序排列,未标记的内存会被清理干净。JVM在为新对象分配内存时,只需要持有内存的起始地址,这显然比维护一个空闲列表要便宜。
优势
缺点
15.8 总结 Mark-SweepMark-CompactCopying
速度
中等的
最慢
最快的
空间开销
少,但碎片会堆积
少,无杂物
通常需要两倍大小的活体,无杂物堆积
移动物体
不
是的
是的
效率方面,复制算法当之无愧,但浪费内存太多
mark-sweep算法比较流畅,但是效率不理想。它比复制算法多一个标记阶段,比标记清除算法多一个清理内存的阶段。
15.9 分代采集算法
在之前的所有算法中,没有一种算法可以完全替代其他算法。它有自己的优点和特点。分代采集算法应运而生。
不同对象的生命周期是不同的。不同生命周期的对象可以采用不同的方式进行采集,提高采集效率。Java堆一般分为新生代和老年代,根据不同的特性可以使用不同的回收算法。提高回收效率
目前几乎所有的GC都使用分代采集算法来进行垃圾采集。
在HotSpot中,基于代的概念,GC使用的内存回收算法必须结合年轻代和老年代的特点。
年轻一代
以HotSpot中的cms采集器为例,cms是基于Mark-Sweep实现的,对象的回收效率非常高。对于碎片问题,cms使用基于Mark-Compact算法的Serial Old采集器作为补偿措施:当没有加入内存回收时(当碎片导致Concurrent Mode Failure时),会使用Serial Old来执行Full GC实现正确组织老年记忆
15.10 增量采集算法,分区算法
上述现有算法中,应用软件在垃圾采集过程中会处于STW状态。如果垃圾回收时间过长,会严重影响系统稳定性。为了解决这个问题,实时垃圾采集算法的研究直接导致了增量采集(Incremental Collecting)算法的诞生。
基本理念
垃圾采集线程和应用程序线程可以交替执行。每次垃圾采集线程只采集一小块内存空间,然后切换到应用线程。直到垃圾采集完成
增量采集算法的基础仍然是传统的标记-清除和复制算法。增量采集算法妥善处理线程之间的冲突,让垃圾采集线程分阶段完成标记、清理或复制工作
缺点
应用程序代码是间接执行的,因此可以减少系统暂停时间。但是由于线程切换和上下文切换的消耗,会增加垃圾回收的整体成本,导致系统吞吐量下降
生成算法根据对象的生命周期分为两部分。分区算法将整个堆空间划分为连续的不同小区间区域。每个电池独立使用和独立回收。它可以控制一次回收多少个细胞。