Java垃圾收集算法
一、对象标记算法垃圾回收器在对堆内存进行回收前,第一件事情就是要确定哪些对象还”存活”中,哪些对象已经”死去”。一般有下面两种方法来对其进行标记。1、引用计数法原理:给对象中添加一个引用计数器,每当有一个地方引用到它,计算器的值就加1,当引用失效的时候,计数器就减1,任何时刻计数器为0的对象就是没有被使用的对象,表示可以回收。说明:这种方法在主流的虚拟机里面没有被采用,原因是它很难解决对象之间循环
一、对象标记算法
垃圾回收器在对堆内存进行回收前,第一件事情就是要确定哪些对象还”存活”中,哪些对象已经”死去”。一般有下面两种方法来对其进行标记。
1、引用计数法
原理:给对象中添加一个引用计数器,每当有一个地方引用到它,计算器的值就加1,当引用失效的时候,计数器就减1,任何时刻计数器为0的对象就是没有被使用的对象,表示可以回收。
说明:这种方法在主流的虚拟机里面没有被采用,原因是它很难解决对象之间循环引用的问题。
2、可达性分析算法
原理:通过一系列称为”GC Roots”的对象作为起始起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象没有被使用,可以被回收。
Java中可作为GC Roots的对象包括以下几种:
(1)虚拟机栈中引用的对象
(2)方法区中类静态属性引用的对象
(3)方法区中常量引用的对象
(4)本地方法栈中JNI引用的对象
说明:
真正宣告一个对象的死亡,至少要经历两次标记过程:
(1)如果对象在进行可达性分析后没有与GC Roots相连接的引用链,会被第一次标记,并且会进行异常筛选,筛选的条件是此对象是否执行了finalize()方法,当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机执行过了(finalize()方法只能被执行一次),虚拟机都会视为该方法没有必要执行了,否则就判断该对象有必要执行,如果被判定为有必要执行finalize()方法,他就会放到一个F-Queue队列中,并且稍后由虚拟机创建一个低优先级的Finalizer线程去执行,但是需要注意的是,这个执行只是虚拟机会触发这个方法,但并不会承诺等待它运行结束,因为finalize()方法可能会有耗时操作,导致其他对象永久等待甚至导致回收系统崩溃。
(2)finalize()方法是对象逃脱回收的最后一次机会,在执行finalize()方法后会再次进行一次小规模的标记,因为可能某个对象在第一次被标记过,但是在finalize()方法被再次引用,那么它就会重新回到引用链上去,否则进行第二次标记,被第二次标记的对象就是即将回收的对象。
补充:含有Finalize方法的object是在new的时候由虚拟机生成了一个Finalize reference在来引用到该Object的,而在Finalize方法执行的时候,该object所对应的Finalize Reference会被释放掉,即使在这个时候把该object复活(即用强引用引用住该object),再第二次被gc的时候由于没有了Finalize reference与之对应,所以Finalize方法不会再执行
二、方法区的回收
方法区也是永久代,永久代的回收主要是两个部分:废弃常量和无用的类。
1、回收废弃常量与回收Java堆中的对象是类似的,以常量池中的字面量为例,例如一个字符串”abc”已经进入了常量池中,但是当前系统没有任何一个String对象叫做”abc”,那么”abc”常量可能会被系统清理出常量池。
2、判断一个类是否为无用的类的条件相对比较苛刻
(1)该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
(2)加载该类的ClassLoader已经被回收
(3)该类对应的java.lang.Class对象没有被任何地方引用。
满足上面三个条件才可以被回收,需要注意的是它只是可以被回收并不是不使用就必然会被回收,这个跟对象的回收不同。
三、垃圾回收算法
1、标记-清除算法
(1)标记:上面已经说到过,使用的就是可达性分析算法将无用的对象标记出来
(2)将第一步标记的对象进行回收清除
不足:
(1)效率问题:标记和清楚的效率都不高
(2)空间问题:清除后会产生大量不连续的内存碎片。
2、复制算法
思想:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。
这种算法虽然实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。
3、标记-整理算法
思想:在完成标记之后,它不是直接清理可回收对象,而是将存活对象都向一端移动,然后清理掉端边界以外的内存。
4、分代收集算法
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。
内存被分为下面三个区域:
(1)新生代:Enden、form survicor space、to survivor space。
(2)老年代
(3)永久代:方法区
新生代
包含有Enden、form survicor space、to survivor space三个区,绝大多数最新被创建的对象会被分配到这里,大部分对象在创建之后会变得很快不可达,对象从这个对象消失的过程称为”minor GC”
特征:
(1)GC的发生相对比较频繁
(2)GC的发生比较迅速高效,因为新生代的空间通常比较小,并且可能包含了许多短周期对象。
(3)经历过几次新生代的垃圾回收之后,存活下来的对象会被拷贝到老年代的堆空间中。
老年代
从新生代存活下来的对象会被拷贝到这里,它的空间比新生代要大,所以在老年代上发生的GC要比新生代少得多。对象从老年代消失的过程称为”major GC”或者”full GC”。
特征:
(1)比新生代的堆空间要大
(2)内存占用的增长比较慢
(3)GC操作不是很频繁,但是耗时比新生代中的GC要长。
持久代
也被称为方法区,用来存放类常量和字符串常量,这个区域不是用来存储那些从老年代存活下来的对象。它也会发生GC操作。
内存分配与回收的几条策略
1、对象优先在Enden上分配
2、大对象可以直接进入老年代
设置标签-XX:PretenureSizeThreshold = n
3、长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Enden出生并且经过第一次Minor GC仍然存活,并且能够被Survivor空间容纳,进入移动到Survivor空间,并且设置对象年龄为1,对象在Survivor区每熬过一个Minor GC,年龄就增加1岁,当它的年龄到达一定的程度(默认为15岁),就会被移动到老年代,这个年龄阀值可以通过-XX:MaxTenuringThreshold
设置。
4、动态对象年龄判断
虚拟机并不是永远要求对象的年龄达到MaxTenuringThreshold才移动到老年代,如果Survivor区中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象也被移动到老年代。
5、空间分配担保
在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。
新生代GC的过程
(1)新对象会被分配到Enden空间中。
(2)当Enden空间满时,就会触发minor GC过程。
(3)没有被引用的对象就会被垃圾收集
(4)被引用的对象会被复制到S0 survicor区,并且age加1。
(5)当对象被移动到S0 survicor区,Enden空间会被清空。
当minor GC再次发生
首先Enden区进行GC,然后S0 survicor区进行GC
(6)最后进行GC操作之后被引用的对象变成from survicor空间。
(7)GC后被引用的对象会被复制到to survicor空间,另外age加1。
(8)复制完成之后,将Enden区和from survicor空间清空。
当minor GC再次发生
首先Enden区进行GC,然后to survicor空间进行GC。
(9)最后进行GC操作之后被引用的对象变成from survicor空间。
(10)GC后被引用的对象会被复制到to survicor空间,另外age加1。
(11)复制完成后将Enden区和from survicor空间清空。
当下一次GC发生
首先Enden区进行GC,然后from survicor空间进行GC。
重复(9)(10)(11)操作
当存活的对象达到了一个age的阀值,最终就会复制到老年代。
总结:
(1)每次minor GC都会进行重复处理
(2)当存活的对象达到了一个age的阀值时,他们会被复制到老年代。
补充:
在新生代中,每次垃圾收集时发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集,而老年代中因为对象存活率高,没有额外的空间对它进行分配担保,就必须使用”标记-清理”或者”标记-整理”算法进行回收。
更多推荐
所有评论(0)