JVM的垃圾回收算法工作原理详解
怎么判断对象是否可以被回收?
共有2种方法,引用计数法和可达性分析
1.引用计数法
所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一。当一个对象的引用计数器为零时,说明此对象没有被引用,也就是“死对象”,将会被垃圾回收.
引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象a引用对象b,对象b又引用者对象a,那么此时a,b对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法。
public classreferencefindtest{ publicstaticvoidmain(string[] args){ myobject object1 = new myobject(); myobject object2 = new myobject(); object1.object = object2; object2.object = object1; object1 = null; object2 = null; } }
2.可达性算法(引用链法)
该算法的思想是:从一个被称为gc roots的对象开始向下搜索,如果一个对象到gc roots没有任何引用链相连时,则说明此对象不可用。
在java中可以作为gc roots的对象有以下几种:
- 虚拟机栈中引用的对象
- 方法区类静态属性引用的对象
- 方法区常量池引用的对象
- 本地方法栈jni引用的对象
虽然这些算法可以判定一个对象是否能被回收,但是当满足上述条件时,一个对象比不一定会被回收。当一个对象不可达gc root时,这个对象并不会立马被回收,而是出于一个死缓的阶段,若要被真正的回收需要经历两次标记。
如果对象在可达性分析中没有与gc root的引用链,那么此时就会被第一次标记并且进行一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法或者已被虚拟机调用过,那么就认为是没必要的。
如果该对象有必要执行finalize()方法,那么这个对象将会放在一个称为f-queue的对队列中,虚拟机会触发一个finalize()线程去执行,此线程是低优先级的,并且虚拟机不会承诺一直等待它运行完,这是因为如果finalize()执行缓慢或者发生了死锁,那么就会造成f-queue队列一直等待,造成了内存回收系统的崩溃。gc对处于f-queue中的对象进行第二次被标记,这时,该对象将被移除”即将回收”集合,等待回收。
堆内存分代策略以及意义
策略
java虚拟机将堆内存划分为新生代、老年战和永久代,永久代是hotspaot 虚拟机特有的概念,它采用永久代的方式来实现方法区,其他的虚拟机实现没有这一概念,而且hotspot也有取消永久代的趋势,在jdk 1.7中hotspot已经开始了“去永久化”,把原本放在永久代的字符串常量池移出。永久代主要存放常量、类信息、静态变量等数据(移植到方法区),与垃圾回收关系不大,新生代和老年代是垃圾回收的主要区域。
新生代(young)
新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低,在新生代中,常规应用进行一次垃圾收集-般可以回收70% ~ 95%的空间,回收效率很高。
老年代(oldgenerationn)
在新生代中经历了多次(具体看虚拟机配置的阀值)gc后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行gc的频率相对而言较低,而且回收的速度也比较慢。
永久代(permanentgenerationn)
永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。
- jdk1.6及之前: 有永久代, 常量池1.6在方法区。
- jdk1.7: 有永久代,但已经逐步“去永久代”,常量池1.7在堆。
- jdk1.8及之后: 无永久代,常量池1.8在元空间。而元空间是直接存在内存中,不在java虚拟机中的,因此元空间依赖于内存大小。当然你也可以自定义元空间大小。
意义
有了内存分代,新创建的对象会在新生代中分配内存,经过多次回收仍然存活下来的对象存放在老年代中,静态属性、类信息等存放在永久代中,新生代中的对象存活时间短,只需要在新生代区域中频繁进行gc,老年代中对象生命周期长,内存回收的频率相对较低,不需要频繁进行回收,永久代中回收效果太差, 一般不进行垃圾回收,还可以根据不同年代的特点,采用不同的垃圾收集算法。分代垃圾收集大大提升了垃圾收集效率,这些都是jvm分代的好处。
垃圾回收算法
1.复制算法
复制算法将可用内存按容量划分为相等的两部分,然后每次只使用其中的一块,当一块内存用完时,就将还存活的对象复制到第二块内存上,然后一次性清楚完第一块内存,再将第二块上的对象复制到第一块。但是这种方式,内存的代价太高,每次基本上都要浪费一半的内存。
2.标记清除算法
是jvm垃圾回收算法中最古老的一个,该算法共分成两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,清除未被标记的对象。该算法的缺点是需要暂停整个应用,并且在回收以后未使用的空间是不连续,即内存碎片,会影响到存储。
3.标记整理算法
此算法结合了标记-清楚算法和复制算法的优点,也分为两个阶段,第一阶段从引用根节点开始标记所有被引用的对象,第二阶段遍历整个堆,在回收不存活的对象占用的空间后,会将所有的存活对象往左端空闲空间移动,并更新对应的指针。标记-整理算法是在标记-清除算法的基础上,又进行了对象的移动,因此成本更高,但是却解决了内存碎片的问题,按顺序排放,同时解决了复制算法所需内存空间过大的问题。
4.分代收集
分代收集算法是目前大部分jvm的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(tenured generation)和新生代(young generation),在堆区之外还有一个代就是永久代(permanet generation)。老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的收集算法。
a.年轻代回收算法(核心其实就是复制算法)
hotspot将新生代划分为三块,-块较大的eden空间和两块较小的survivor空间,默认比例为8: 1: 1。划分的目的是因为hotspot采用复制算法来回收新生代,设置这个比例是为了充分利用内存空间,减少浪费。新生成的对象在eden区分配(大对象除外,大对象直接进入老年代) ,当eden区没有足够的空间进行分配时,虚拟机将发起一次minor gc。gc开始时,对象只会存在于eden区和from survivor区,to survivor区是空的(作为保留区域)。
gc进行时,eden区中所有存活的对象都会被复制到to survivor区,而在fromsurvivor区中,仍存活的对象会根据它们的年龄值决定去向,年龄值达到阀值(默认为15 ,新生代中的对象每熬过一轮垃圾回收年龄值就加1 ,gc分代年龄存储在对象的header中)的对象会被移到老年代中,没有达到阀值的对象会被复制到to survivor区。
接着清空eden区和from survivor区,新生代中存活的对象都在to survivor区。接着, from survivor区和to survivor区会交换它们的角色,也就是新的to survivor区就是上次gc清空的fromsurvivor区,新的from survivor区就是.上次gc的to survivor区,总之,不管怎样都会保证to survivor区在一轮gc后是空的(其实这就是分代收集算法中的年轻代回收算法,稍后我们会看到)。
gc时当to survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。
b.老年代回收算法(回收主要以标记-整理为主)
1)在年轻代中经历了n次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
2)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发major gc即full gc,full gc发生频率比较低,老年代对象存活时间比较长,存活率标记高。
c. 持久代(permanent generation)的回收算法
用于存放静态文件,如java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。在该区内很少发生垃圾回收,但是并不代表不发生gc,在这里进行的gc主要是对持久代里的常量池和对类型的卸载。
条件:
1)该类所有的实例都已经被回收,即java堆中不存在该类的任何实例;
2)加载该类的classloader已经被回收;
3)该类对应的java.lang.class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机可以对满足上述3个条件的无用类进行回收,此处仅仅是“可以”,而并不是和对象一样,不使用了就必然回收!
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。
上一篇: 陈少游为何能屡屡升迁呢?只因他有一项绝活