垃圾收集器与内存分配策略_垃圾收集算法
前面了解了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. 分代收集算法
当前商业虚拟机的垃圾收集都是采用"分代收集"算法,将对象存活周期的不同将内存划分为几块,一般是划分为新生代和老年代,然后根据不同年代的特定采用适当的收集算法。
新生代:大量对象消亡,少量存活,采用复制算法
老年代:存活率高,没有额外的内存分配,采用标记-清除算法,标记-整理算法
上一篇: 贪心算法(背包算法、普里姆算法)
下一篇: PHP创建单例后台进程的方法示例