JAVA——垃圾收集器与内存分配策略

Garbage Collection(GC)

首先,有三个问题:

1.哪些内存才能需要回收?

2.什么时候回收?

3.如何回收?

针对以上三个问题,我们对于GC展开讨论:

对象已死吗?

在堆中存放着Java世界几乎所有的对象实例,GC在对堆进行回收前,第一件事就是先确定哪些对象还活着,哪些对象已死去(即不可能再被任何途径使用的对象)。

1.引用计数算法(Reference Counting)

很多教科书判断对象是否存活的算法是这样的:给对象添加一个引用计数器,每当一个地方引用它时,计数器就增加1;当引用是失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。

客观的说,Reference Counting的实现很简单,判定效率很高,在大部分情况下,它都是个不错的算法,也有一些著名的案例,如微软公司的COM(Component Object Model)技术、使用ActionScript 3的FlashPlayer、Python语言都使用了引用计数算法进行内存管理。

package test;
/**
 * 
 * 在testGC执行完成后,objA和objB会不会被GC呢?
 */
public class ReferenceCountingGC {
	public Object instance = null;
	private static final int _1MB = 1024*1024;
	private byte[] bigSize = new byte[2*_1MB];
	
	public static void testGC(){
		ReferenceCountingGC objA = new ReferenceCountingGC();
		ReferenceCountingGC objB = new ReferenceCountingGC();
		objA.instance = objB;
		objB.instance = objA;
		
		objA = null;
		objB = null;
//		假设在这行发生GC,objA和objB能否被回收呢?
		System.gc();
	}
}
如果按照上面的思想,思路是:对象objA和objB都有字段instance,赋值令objA.instance = objB;objB.instance =objA;

除此以外就没有任何引用,实际上两个对象不可能再被访问,但是就是因为它们互相引用着对方,导致计数器不为0,于是引用计数算法无法通知GC回收它们。

但是运行结果可以看到,GC日志中包含"4603K->210K“,就意味着虚拟机并没有因为这两个对象互相引用着就不回收它们,这也侧面说明了虚拟机并不是通过引用计数算法来判断对象是否还存活着

2.可达性分析算法(Reachability Analysis)

Reachability Analysis来判断对象是否可回收。

这个算法的基本思路:通过一系列的称为"GC Roots"的对象作为作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链(就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。

20131007215931031 (856×476)

从上图可知,对象object F、object G、object H虽然互相关联着,但是它们到GC Roots是不可达的,所以,它们将作为回收的对象。

在JAVA中,可作为GC Roots的对象包括下面的几种:

虚拟机栈(栈帧中的本地变量表)中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中JNI(一般说的是Native方法)引用的对象

3.再谈引用

无论是通过引用计数算法还是可达性分析算法,判断对象是否存活都与"引用"有关。

引用分为强引用(Strong reference)、软引用(Soft reference)、弱引用(Weak reference)、虚引用(Phantom reference)

强引用(Strong reference):在程序代码中普遍存在,类似“Object obj = new Object()”这类的引用,只要强引用存在,那么GC就永远不会回收掉被引用的对象;

软引用(Soft reference):用来描述一些还有用但是并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK1.2之后,提供了SoftReference类来实现软引用

弱引用(Weak reference):用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次GC发生之前。当GC开始工作时,无论内存够不够,都会将它回收掉。在JDK1.2之后,提供了WeakReference类来实现弱引用

虚引用(Phantom Reference):也被称为幽灵引用或是幻影引用,它是最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存周期构成影响,也无法通过虚引用来获得一个对象的实例。为一个对象设置虚引用的唯一目的就是该对象在被GC回收时收到一个通知。在JDK1.2之后,提供了PhantomReference类来实现虚引用
垃圾收集算法

1.标记-清除(Mark-Sweep)算法

此算法如同它的名字,分为“标记”和“清除”两个阶段。首先先标出所有需要回收的对象,在标记后统一回收所有被标记的对象。标记的方法就是我们之前所讲的判断对象是否可回收的方法。

它主要有两个不足:一是效率问题,标记和清除两个过程的效率都不高,二是标记清楚后会有很多的碎片,空间碎片太多可能会导致以后再程序运行中需要分配较大的对象时,无法找到足够的连续内存而不得不提前出发一次GC活动

495229_201212191517290634.png (632×396)

2.复制算法

为了解决效率问题,一种“复制”(Copying)的收集算法出现了。它将可用的内存按容量划分为大小相等的两块,每次只使用其中的一块。当一块内存用完以后,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况。只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

495229_201212191517380775.png (629×374)

IBM公司的专门研究表明,新生代的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor空间上,最后清理Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被"浪费",我们没办法保证内次回收只有不多于10%的对象可存活,如果当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

所谓内存担保就是就好比去银行贷款,如果我们信誉很好,98%的情况下我们都能按时偿还,于是银行可能默认为下次也能按时按量的偿还,只需要有一个担保人能保证如果我不能还款时,可以从担保人的账户里扣钱。内存担保也一样,如果另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象会直接通过内存分配担保机制进入老年代。

3.标记-整理(Mark-Compact)算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率就会变低。过程和标记-清除法一样,但后续处理不是清除,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

495229_201212191517480603.png (618×365)

4.分代收集算法

根据对象存活的周期不同将内存划分为几块。一般把Java堆分为新生代和老年代,这样根据各个年代的特点采用最适当的收集算法。在是新生代中,每次GC时都发现有大量对象死去,只有少量存活,可以选择复制算法。而老年代因为对象存活率高,没有额外空间对它进行担保分配,就必须使用标记-清楚或是标记-整理的算法进行回收。

HotSpot的算法实现

上述的内容都是从理论上介绍对象存活判定的算法和垃圾收集的算法,而在HotSpot虚拟机上实现这些算法。



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值