1、标记清除法(Mark And Sweep)
第一步:从根部出发,遍历全局,然后对所有可达的对象进行标记
第二步:对所有未标记的对象进行清除
优点:方法简单,速度较快。缺点:容易产生较多的内存碎片。
采用这种方式的语言:lua等
2、标记整理回收(mark-compact)
第一步和标记清除法一样,标记所有可达对象
第二步将未标记的对象清除,同时将现有对象的空间合并
优点:没有内存碎片。缺点:合并空间的时候,引用该对象的所有线程都会被挂起,合并完成后才会重新执行。
采用这种方式的语言:c#等
3、标记复制回收(mark-copy)
复制算法开辟了两个相等的空间,每次只使用其中的一块空间
第一步标记
第二步将所有标记过的对象,复制到另一块空间,当复制完成后,指向原有对象的指针指向新的对象。全部复制完成后,释放原有的空间。
优点:没有内存碎片,不会gc ,效率高 缺点:需要额外的内存空间
采用这种方式的语言:java的新生代
4、引用计数算法(reference counting)
对象每次被引用的时候对引用次数加1,每次被引用对象被删除时,则对引用次数减1,当引用计数为0时,则删除对象。
优点:迅速,每次当对象引用次数为0时,则马上就会被清除。无需系统支持,去确定程序的根。
缺点:
1、计数赋值器带来额外的开销。所以不适合通用的大容量的内存管理器。
2、多线程的程序中,可能释放过早。引用计数的存储指针操作是原子化的,并发线程却同时进行读取和修改,开发者要避免更新指针槽过程中出现的竞争问题。
3、对单个对象的简单操作也会引发内存请求(更新引用次数),会“污染”高速缓存
4、无法解决循环引用问题
5、有可能卡顿,当删除一个大的根节点的时候,需要去递归删除每一个子孙节点。
循环引用的解决方法:
1、定期用标记算法作为补充处理
2、设为强引用和弱引用,把可能产生环的引用设为弱引用,所有强引用可达且不成环,当强引用次数为0时,删除对象(这种方法为了一些安全性原因,性能开销大,只有少数语言使用)
c++的智能指针的弱引用和这种算法的弱引用不一样,c++的弱引用只能确定是否可达,主要是为了避免非法访问。
3、部分跟踪算法,循环引用指针出现有2个条件:
(1)环状指针内部,所有引用对象都有内部对象指针产生
(2)如果删除某一对象后,引用计数仍然为0,则说明产生了环状
扫描对象,如果一个对象的所有引用都是循环引用,则进行处理。临时移除对目标对象的引用次数,从而移除内部指针的引用次数,如果目标对象引用计数仍然大于0,则说明存在外部引用,否则一起处理掉。
计数回收的语言有:python等
5、分代算法
将内存分为几个区域,不同状态的对象放进不同的区域里,对每个区域采取不同的垃圾回收策略,可以兼顾优点,但是比较复杂。
采用分代回收的语言:java等
java将内存分成了新生代、年老代和永久代
新生代:新生代用标记复制回收,因为绝大部分创建的对象都是临时用的,很快会被回收掉,同时为了提高性能,和适合用复制回收,复制回收的两块区域大小是9:1。
年老代:当在新生代里复制一定次数还没有被回收以后,则放到年老代里,年老代采用复制标记回收。
永生代:当在年老代理一定时间没有被回收,则放入永生代,永生代采用复制整理回收。
分成的好处针对不同性质的对象,采用不同的处理方式。复制整理回收的回收效果好,但是整理过程中会造成gc,所以用了两层过渡,减少复制整理的发生。