垃圾收集(GarbageCollection,GC)
在考虑GC的时候,首先需要考虑三个问题:
哪些内存需要被回收?
什么时候回收?
如何回收?
1.哪些内存需要被回收?(java堆上)
内存溢出:
内存泄漏:
判断对象的存活与死亡
a.引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,引用计数器加1,当引用失效时就减1;任何时刻计数器为0的对象就是不可能再被使用的。
优点:实现简单、效率高
缺点:无法解决循环引用的问题
b.可达性分析
通过一系列称为“GC Roots”的对象作为起点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到任何GC root没有引用链相连时,则证明此对象是不可用的,即为被回收对象。
在JAVA语言中,可作为
GC root的对象包括:
虚拟机栈(栈中的本地变量表)中引用的对象;
方法区中静态属性引用的对象
方法区中常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
JDK1.2之前,引用的定义:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这块内存代表一个引用。
JDK1.2之后,java对引用的概念进行了扩充,将引用分为强引用、软引用、弱引用、虚引用。
强引用:Object obj=new Object();,只要强引用还在,垃圾回收器永远不会回收掉被引用的对象。
软引用:用来描述一些还有用但是并非必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列为回收范围之中的进行第二次回收,如果这次回收还没有足够的内存,就会抛出OOM异常
弱引用:用来描述非必须的对象,他的强度更弱一些,被弱引用关联的对象只能生存到下一次垃圾回收发送之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉被弱引用关联的对象
虚引用:为一个对象设施虚引用关联的唯一目的就是能在这个对象被收集器回收时受到一个系统通知。
对象的生存与死亡?
在可达性分析算法中不可达的对象,也不一定“非死不可”,要真正宣告一个对象死亡,要经过两次标记过程:如果对象在进行可达性分析的后发现没有与GC root相连接的引用链,那么它将第一次被标记并且进行一次筛选(筛选条件此对象是否有必要进行finalize()方法)。当对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过,则判定为没有必要执行。
如果对象被判定为有必要执行finalize()方法,则该对象被放入F-Queue的队列中,并在稍后由Finalizer线程(由虚拟机自动建立、低优先级的线程)去执行。这里的执行是指虚拟机会触发这个方法,但是并不承诺会等待它运行结束
原因:如果一个对象在finalize()中执行缓慢,或者发生了死循环,将很可能导致F-Queue队列中的其他对象永远处于等待状态,甚至导致整个内存回收系统崩溃。
finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次标记。如果对象没有逃脱,那么基本上就被回收了。
对象在finalize()中拯救自己,只需要与引用链中的任何一个对象建立起关系即可,eg,将自己(this关键字)赋值给某个类变量或者对象的成员变量。
注意:任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次的回收,它的finalize()方法不会被再次执行。
缺点:运行代价高昂,不确定性大,无法保证各个对象的调用顺序。finalize()能做的所有工作,使用try-finally或者其他方式都可以做的更好、更及时。因此不建议使用finalize()方法(深入理解java虚拟机p68)
回收方法区
方法区(永久代)的垃圾收集主要回收两部分内容:废弃常量和无用的类
回收废弃常量和回收java堆中的对象非常类似。
废弃常量;没有任何String对象引用常量池中的该常量,也没有其他地方引用了这个字面量
无用的类:1.该类的所有实例都已经被回收,也就是java堆中不存在任何该类的实例;
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方呗引用,无法在任何地方通过反射访问该类的方法
虚拟机可以对满足上述三个条件的无用类进行回收,并不是必然回收。
HotSpot虚拟机提供了-Xnoclassgc参数进行控制
java堆分为新生代和老年代,新生代划分为一块较大的Eden区和两块较小的Survivor区
垃圾收集算法
标记--清除算法:最基础的收集算法
算法分为“标记”和"清除"两个阶段:首先标记要回收的对象,标记完成后统一回收所有被标记的对象。
不足:1.效率不高
2.标记清除之后会产生大量的不连续的内存碎片。
“标记--清除”算法示意图
复制算法:将内存按照容量划分为大小相等的两块,每次只使用一块,当一块的内存使用完,将其中还存活的对象复制到另一个内存块上,然后将已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配也不需要考虑内存碎片的情况,只需要推动堆顶指针按序分配即可。
复制的算法常用来收集新生代,研究表明,新生代中的对象98%是“朝生夕死”,所以并不需要按照1:1的比例来划分内存空间,而是将内存划分为一块较大的Eden区和两块较小的Survivor区,每次使用Eden区和一个Survivor。当回收时,将Eden和Survivor中还存活的对象一次性地复制到另外一个Survivor空间上,最后清理掉Eden和刚才使用过的Survivor空间。HotSpot默认Eden和Survivor的大小比例为8:1,即每次新生代中可用的内存空间为整个新生代容量的90%。当Survivor空间不够用的时候需要依赖其他内存(老年代)进行分配担保。
复制收集算法在对象存活率较高时就需要进行较多的复制操作,效率会降低,而且如果不想浪费50%的空间,就需要额外的空间进行分配担保。
标记--整理算法:标记,存活对象像一边移动,然后直接清理掉端边界以外的内存
标记--整理算法用来回收老年代
分代收集算法:根据对象存活周期的不同将内存划为几块,根据不同区的特点采用不同的收集算法。