欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-12 16:24:26
...

前面了解了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. 分代收集算法

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

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

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