垃圾收集器与内存分配策略_垃圾收集算法

前面了解了java运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭

;栈中的栈帧随着方法的进入和退出执行者出栈和入栈操作。每个栈帧中分配多少内存基本是在类结构确定下来时就已知的。

因而这几个区域的内存回收都具有确定性。

也就是在对于程序计数器、虚拟机栈、本地方法栈这几个内存区域不需要考虑回收问题,方法结束或线程销毁,内存自然随着回收了

java堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法的多个分支需要的内存也可能不一样,只有在程序处于运行期间才能知道创建了那些对象,这部分内存的创建和回收都是动态的,垃圾收集器关注的是这部分内存

判断对象是否存活

1. 引用计数算法

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

但是主流的java虚拟机没有采用计数器算法,主要的原因是它很难解决对象键互相循环引用的问题。

public class ReferenceGc {
	class A{
		B b;
	}
	class B{
		A a;
	}
	public static void main(String[] args) {
		ReferenceGc rf = new ReferenceGc();
		A a = rf.new A();
		B b = rf.new B();
		a.b = b;
		b.a = a;
		a = null;
		b =null;
		System.gc();
	}
}

类A有个字段是B类类型,类B有个字段是A类的类型,创建对象a和对象b,此时引用a指向A对象,引用b执行B对象,后面在将两个类的字段相互引用,就算a,b引用失效,A类对象仍然被B类字段引用着,A类和B类独享都访问不到了,但是引用计数却不为0,无法通知到垃圾回收器回收.

2. 可达性分析算法

主流的善用程序语言(Java)主流实现中,都是通过可达性判断对象是否存活。

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


可以作为GC Roots对象:

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

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

3> 方法区中常量引用的对象

4> 本地方法栈中JNI,即一般说的Native方法引用的对象。

就拿上面相互引用的例子来说:

A 类的字段是B类型的,b对象通过A引用找A对象,A引用为null,认为A是不可达的,垃圾回收器回收A对象。

B 类的字段是A类型的,a对象通过B引用找B对象,B引用为null,认为B是不可达的,垃圾回收期回收B对象。

再谈引用

我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存中,如果内存空间在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。

Java对引用的进行扩充,将引用分为强引用,软引用,弱引用,虚引用,这4中引用强度依次减弱

1>  强引用就是指在程序代码之中普遍存在的,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。

2> 软引用是用来描述一些还有用但并非必须的对象。对于软引用关联的对象,在系统将要发生内存溢出异常前,将会把这些对象进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。

3> 弱引用也是描述非必须对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生前。

4> 最弱的一种引用关系,完全不会对对象的生存时间影响。

对象的生存还是死亡

即使在可达性分析算法中不可达的对象,也并非是“非死不可”,一个对象真正销毁,要经历两个阶段,如果对象在进行可达性分析后没有和GC Roots相连的引用链,会被第一次标记并进行一次筛选,筛选的条件是对象是否有必要执行finalize方法。当对象没有覆盖finalize()方法或者finalize()方法已经执行过,虚拟机将这两种情况视为 "没有必要执行",直接销毁

可以在finalize中逃脱对象销毁,但是不建议这么做。

回收方法区

很多人认为方法区(或者HotSpot虚拟机中的永久代)是没有垃圾回收的,java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的性价比一般比较低;在堆中(按分代算法分为新生代、老升代) ,新生代中,常规进行一次垃圾收集一般可以回收70%~95%的空间,永久代的垃圾收集效率远低于此。

永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象非常相似。

1. 判断一个常量是否是废弃常量比较简单,只要判断没有任何引用引用这个常量。

比如常量池中字符串“abc”,如果没有任何引用引用到这个常量,就会被系统清理出常量池,常量池中的其他类、方法、字段的符号引用也类似

2. 判断一个类是否是无用的类,需要同时满足下面三个条件

1> 该类的所有实例都已经被回收,也就是Java对中存在该类的任何实例

2> 加载该类的ClassLoader已经被回收

3> 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法

垃圾收集算法

1. 标记-清除算法

通过可达性算法标记处所有需要回收的对象,在标记完成后统计回收已死亡的对象

它的不足之处有两个:一个是效率问题,标记和清除的效率都不高,另一个方面,标记清除后产生大量不连续的内存碎片,可能造成以后分配较大对象后,由于空间不足,会再次触发一次垃圾回收


2. 复制算法

将可用内存按容量划分为大小相等的两块(一半,一半),每次只使用其中的一块。当这一块的内存用完,将还存活的对象复制到另外一块上,将已使用的内存空间进行清理。


优点:这样每次都是对半个内存区域进行回收,内存分配时在也不用考虑内存碎片等复杂情况,只要移动堆定指针,按顺序分配内存,简单,高效

缺点:将内存缩小为原来的一半,代价太高

现在商业虚拟机都是采用这种收集算法来回收新生代,IBM研究表明新生代中98%,是朝生夕死的,所以不需要按照1:1来划分内存空间。一般是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor空间进行清理,将存活对象移动到另一块Survivor空间(如果不够用,会通过分配担保机制移动到老年代中)

3. 标记-整理算法

复制算法在对象存活率较高时要进行较多的复制操作,效率会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象100%存活的极端情况,所以老年代不选用复制算法

根据这个特点,提出来标记整理算法,不过的是在可达性区分开存活对象和可回收对象后,将存活对象,移动到内存区域的边界,然后清除边界外的内存。


4. 分代收集算法

当前商业虚拟机的垃圾收集都是采用"分代收集"算法,将对象存活周期的不同将内存划分为几块,一般是划分为新生代和老年代,然后根据不同年代的特定采用适当的收集算法。

新生代:大量对象消亡,少量存活,采用复制算法

老年代:存活率高,没有额外的内存分配,采用标记-清除算法,标记-整理算法



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值