1.如何判断垃圾对象
1)引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。
引用技术算法实现相对简单,判定效率也比较高(ActionScript3,Python语言等都使用此算法),但是java虚拟机没有使用这个算法,主要原因是引用技术算法很难解决循环引用的问题
2)可达性分析算法
在主流的商用程序语言(java、c#)的主流实现中,都是通过可达性分析来判定对象是否存活的。
对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。
通过一系列名为根(GC Roots)的引用作为起点,从这些根开始搜索,经过一系列的路径,如果可以到达java堆中的对象,那么这个对象就是“活”的,是不可回收的。可以作为根的对象有:
- 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
- 方法区中的类静态属性引用的对象。
- 方法区中的常量引用的对象。
- 本地方法栈中JNI的引用的对象。
2. 两次标记过程
- 如果对象在进行可达性分析后发现没有被任何GC Roots对象所引用,那么它将被第一次标记并且进行一次筛选,条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()或finalize()方法以及被虚拟机调用过(只会被调用一次),虚拟机将视为没有必要执行finalize
- 如果需要执行finalize(),那么这个对象将被放置在一个叫做F-queue队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它
- finalize()是对象逃脱被回收的最后一次机会,稍后GC将对F-queue中的对象进行第二次标记。如果对象要在finalize()中拯救自己(把this赋值给某个类变量或者对象的成员变量)那么第二次标记的时候它将被移出”即将回收“的集合。否则,对象会被回收。
3.回收方法区
永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。
废弃常量:假如一个字符串为”abc“,如果没有被任何String对象引用,如果此时发生内存回收且有必要回收的时候,”adb“常量会被系统清理出常量池
无用的类:
1.该类所有的实例都已经被回收,也就是行锁java堆中不存在该类的任何实例
2.加载该类的ClassLoader已经被回收
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法
HotSpot虚拟机提供了-Xnoclassgc参数来控制是否回收类
4.垃圾收集算法
垃圾收集器通常会假设大部分的对象的存活时间都非常短,只有少数对象的存活时间比较长。
垃圾收集算法在JVM中主要是复制算法(新生代GC)和标记/整理算法(老年代GC)。
标记-清除(Mark-Sweep)算法
算法过程:
1. 先判定对象是否可回收,对其标记。
2. 统一回收(简单地删除对垃圾对象的内存引用)。
优点:简单直观容易实现和理解。缺点:效率不高,内存空间碎片化。
复制(Copying)算法
将内存平均分成A、B两块,算法过程:
1. 新生对象被分配到A块中未使用的内存当中。当A块的内存用完了, 把A块的存活对象对象复制到B块。
2. 清理A块所有对象。
3. 新生对象被分配的B块中未使用的内存当中。当B块的内存用完了, 把B块的存活对象对象复制到A块。
4. 清理B块所有对象。
5. goto 1。
优点:简单高效。缺点:内存代价高,有效内存为占用内存的一半。
对复制算法进一步优化:使用Eden/S0/S1三个分区
平均分成A/B块太浪费内存,采用Eden/S0/S1三个区更合理,空间比例为Eden:S0:S1==8:1:1,有效内存(即可分配新生对象的内存)是总内存的9/10。
算法过程:
1. Eden+S0可分配新生对象;
2. 对Eden+S0进行垃圾收集,存活对象复制到S1。清理Eden+S0。一次新生代GC结束。
3. Eden+S1可分配新生对象;
4. 对Eden+S1进行垃圾收集,存活对象复制到S0。清理Eden+S1。二次新生代GC结束。
5. goto 1。
标记-紧凑(Mark-Compact)
算法过程:
1. 标记:标记可回收对象(垃圾对象)和存活对象。
2. 紧凑(也称“整理”):将所有存活对象向内存开始部位移动,称为内存紧凑(相当于碎片整理)。完毕后,清理剩余内存空间。
分代收集策略
由于不同的对象适合使用不同的垃圾收集算法,所以引入“代”这个概念。不同的代有不同的分区,一般分为新生代区和老年代区。
新生代:适合采用复制算法进行垃圾收集,对象分布在Eden/S0/S1三个区。
老年代:适合采用标记-紧凑算法进行垃圾收集。