垃圾回收策略与算法
垃圾回收策略与算法
需要关注垃圾回收的内存位置
程序计数器、java虚拟机栈、本地方法栈、栈帧的生命周期跟随线程或方法,内存在生命周期结束时回收,不需考虑回收问题。
关注的内存区域:堆跟方法区[程序运行期间才知道创建哪些对象,内存分配和回收都是动态的,垃圾收集器所关注的是这部分内存]。
判断对象是否存活
若一个对象不被任何对象或变量引用,那么它就是无效对象,需要被回收。
- 引用计数法
对象头,维护计数器count,被引用则计数器+1,引用失效则计数器-1。当计数器为0时,就认为该对象无效。
循环引用及多线程下计数器变更需要昂贵同步的原因,主流虚拟机都不使用该方法管理内存。
- 可达性分析法
所有和GC Root关联的对象就是有效对象,和GC Root没有关联的对象就是无效对象。
- GC Root
Java虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈中引用的对象
方法区中常量引用的对象
方法区中类静态属性引用的对象
GC Root 不包含堆中对象所引用的对象,这样就不会有循环引用的问题。
引用的种类
- 强引用
"Object obj = new Object()",这类引用为强引用。
强引用存在,垃圾收集器永远不会回收被引用的对象。
- 软引用
只有JVM认为内存不足时,才会试图回收软引用指向的对象。
JVM会确保在抛出OutOfMemoryError之前,清理软引用指向的对象。
软引用通畅用来事项内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
- 弱引用
当JVM进行垃圾回收时,无论内存是否充足,都会回收只被弱引用关联的对象。
- 虚引用
也称,幽灵引用或者幻影引用。
提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做 Post-Mortem 清理机制。
回收堆中无效对象
- finalize() 方法
该方法称为终结器,在实例被垃圾回收器回收时触发的操作。
如果对象被判定为有必要执行 finalize() 方法,那么对象会被放入一个 F-Queue 队列中,虚拟机会以较低的优先级执行这些 finalize() 方法,但不会确保所有的 finalize() 方法都会执行结束。如果 finalize() 方法出现耗时操作,虚拟机就直接停止指向该方法,将对象清除。
- 对象重生或死亡
如果在执行 finalize() 方法时,将this赋给了某一个引用,那么该对象就重生了。
如果没有,那么就会被垃圾收集器清除。
回收方法区内存
方法区中主要清除两种垃圾
- 废弃常量
判定废弃常量:只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除掉。
- 无用的类
判定无用的类:
- 该类的所有对象都已经被清除
- 加载该类的ClassLoader已经被回收
- 该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。[一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class。这个对象在类被加载进方法区时创建,在方法区该类被删除时清除]
垃圾收集算法
判定无效对象、无用类、废弃常量,之后就可以回收这些垃圾。
常见的垃圾收集算法
标记-清除算法
- 标记过程(Mark)
遍历所有的GC Roots,然后将所有GC Roots可达的对象标记为存活的对象。
- 清除的过程(Swep)
遍历堆中所有的对象,将没有标记的对象全部清除掉。
与此同时,清除那些被标记过的对象的标记,以便下次的垃圾回收。
- 标记-清除算法的不足之处
- 效率问题:
标记和清除两个过程的效率都不高。
- 空间问题:
标记清除之后,会产生大量不连续的内存碎片,碎片太多可能导致后续需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制算法(新生代)
复制算法,将内存按容量划分为大小相等的两块,每次只使用其中的一块。
当其中一块内存用完,需要进行垃圾收集时,就将存活着的对象复制到另一块上面,然后将原有那块内存全部清除。
- 这种方式的优缺点:
优点:不会有内存碎片的问题。
缺点:内存缩小为原来的一半,浪费空间。
为了解决空间利用率问题,可以将内存分为三块:Eden、From Survivor、 To Survivor,比例是8:1:1,每次使用Eden和其中一块Survivor。回收时,将Eden和Survivor中还存活着的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才使用的Survivor空间。这样只有10%的内存被浪费。
但是无法保证每次回收只有不多于10%的对象存活,当Survivor空间不够,需要依赖其他内存(老年代)进行分配担保。
- 分配担保
为对象分配内存空间时,如果Eden+Survivor中空闲空间无法装下该对象,会触发MinorGC进行垃圾回收。如果MinorGC过后,依然有超过10%的存活对象,这样存活的对象直接通过分配担保机制进入老年代,然后再将新对象存入Eden区。
标记-整理算法(老年代)
- 标记
第一阶段,与标记-清除算法一样,遍历GC Roots,然后将存活的对象标记。
- 整理
第二阶段,移动所有存活的对象,并且按照内存地址次序依次排列,然后将末端内存地址以后得内存全部回收。
这是一种老年代的垃圾收集算法。老年代的对象一般寿命比较长,每次垃圾回收会有大量对象存活,如果采用复制算法,每次需要复制大量存活的对象,效率很低。
分代回收算法
根据对象存活周期不同,将java堆划分为新生代跟老年代。针对各个年代的特点采用适当的回收算法。
新生代:使用复制算法
老年代:使用标记-清除算法、标记-整理算法
总结
重点分析了垃圾回收所关注的内存区域,如何判断无效类、无效对象及废弃的常量,基于前序无效或废弃判断,使用不同的垃圾回收算法进行内存的回收管理。
垃圾回收关注的内存区域是堆内存。
如何判断对象是否存活?一般使用可达性分析法。
强引用、软引用、弱引用、虚引用等几种引用类型的对象回收策略。
回收堆中无效对象与回收方法区内存。
常见的垃圾回收算法,标记-清除算法,标记-复制算法(新生代),标记-整理算法(老年代)以及根据对象分代,混合前述算法的的分代回收算法。
浙公网安备 33010602011771号