如何进行垃圾回收
挑选出垃圾
要进行垃圾回收,当然要知道什么是垃圾,比如说我…手上这支笔,用完了就成为了垃圾,jvm同样也需要将不会再使用的对象进行回收。
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
缺陷:当对象间存在相互引用时,jvm不会清理它们,而实际上它们已经无法再被引用了。
可达性分析
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
确定垃圾(java堆)
选择了垃圾但不一定是真是垃圾,说不定还可以抢救一波,要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链,那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。
对象在被第一次标记后会进入F-Queue队列中等待finalize()方法执行(等死),但是这个时候并不意味着真的只能等待执行了,GC在第二次标记中会小规模的再次标记F-Queue中的对象,确定真的需要被清理,对象还可以在这期间进行自救——重新与引用链上的任何对象相关联,譬如把自己(this关键字)赋值给某个类变量或者对象的成员变量。第二次标记时就会被移除出F-Queue队列,如果没有自救,就真的会被回收了。
值得注意的是一个对象只会执行一次finalize()方法,所以说它只有一次自救的机会。
确定垃圾(方法区)
方法区中的垃圾收集效率远远低于java堆,而方法区收集的垃圾主要包含两部分内容:废弃常量和无用的类。
废弃常量的收集
收集废弃常量与java堆十分相似,以常量池中字面量的回收为例,假如一个字符串“abc”已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc”的,换句话说,就是没有任何String对象引用常量池中的“abc”常量,也没有其他地方引用了这个字面量,如果这时发生内存回收,而且必要的话,这个“abc”常量就会被系统清理出常量池。常量池中的其他类(接口)、方法、字段的符号引用也与此类似。
类的收集
1.该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。
2.加载该类的ClassLoader已经被回收。
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
虚拟机对类的回收并不一定必须回收,只是满足条件之后可以回收,但实际是可以自己控制的。
垃圾回收算法
标记-清除
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
主要不足有两个:
a.效率问题,标记和清除两个过程的效率都不高
b.空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
复制
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
主要不足:
a.将内存缩小为了原来的一半,代价未免太高了一点。
标记-整理
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
主要不足:
a.效率同样不高
分代收集
就是字面意思,不同代采用不同的回收算法,最合理
标记-清除 和标记-整理 适用于对象存活率高,回收少
复制 适用于对象存活率低,频繁发生对象的产生及销毁
对象分配
java中对象分配的原则:
- 对象优先放在Eden区
- 大对象直接进入年老代
- 在Eden区内长期存活的对象放入年老代(存活年龄超过平均年龄)
参考文档:深入理解java虚拟机,jvm高级特性与最佳实践