判断对象是否存活最经典的算法是引用计数法。 每当有引用一个对象时,计数器加一,引用失效时减一。
虽然这个算法比较高效,并且也有很多地方在使用,但是Java中却不使用这个方法。
原因是它很难解决对象之间循环引用的问题。当两个对象相互引用,其引用计数都不为零,但如果这两个对象除此之外再无引用,那么这两个对象实际已经无效,但却无法被标记为垃圾。
Java中使用的是更搜索算法,从一系列GC Roots对象为起始点,所有走过的路径为引用链,若一个对象到GC Roots无引用链相连,则此对象为不可用对象。
GC Roots包含以下几种:
在JDK1.2后,引用分为四种:
Object obj = new Object()
的引用,强引用永远不会被回收。SoftReference
被用来实现软引用。WeakReference
用来实现弱引用。PhantomReference
用来实现虚引用。一个对象的死亡需要至少经历两次标记。
当不可达时,会被标记一次并筛选,筛选条件是是否执行过finalize()
方法,若没有覆盖该方法或已经被虚拟机调用过,则被视为无必要执行。若有必要执行,则会被放置入F-Queue
中,并在稍后有一条虚拟机自动建立的、低优先级的线程去执行。该执行不会承诺等待其运行结束,这是为了防止finalize()
阻塞。
若其在finalize()
中与引用链上对象建立关联,则其在第二次标记时被移除即将回收集合,若未能完成,则之后会被回收。
finalize()
虽然可以做到上述操作,但并不建议这么做,finalize()
是由于历史原因而存在的函数,其原意类似C++中的析构函数,但现在已经毫无必要。
方法区主要回收废弃常量和无用的类。
废弃常量的回收与Java堆中对象相似,例如无人使用的字符常量"Useless"
在无引用时若发生垃圾回收,在必要的情况下,会被清除。常量池中其他的类、方法、字段的符号引用也类似。
无用的类需要同时满足以下三个条件:
java.lang.Class
对象没有被引用,无法通过反射访问该类。若满足则可以进行回收,但不一定会必然回收。
此算法首先标记所有需要回收对象,然后标记完成后回收。
该方法有两个问题,一个是效率问题,标记和清除过程效率都不高;另一个是空间问题,标记清楚之后会产生大量的内存碎片。
当代商业虚拟机使用分代算法,将堆分为新生代和老年代,不同年代使用不同算法。
有以下三种术语:
值得注意的是,所有的对象刚开始都是新生代,每个对象拥有一个年龄计数器,每发生一次Minor GC则年龄加一,当其达到一定年龄或者先相同年龄的对象大于Survivor空间大小一半时,年龄大于等于该年龄的对象进入老年代。
这种算法将内存分为两部分,每次只在一部分分配。当空间用完或垃圾收集时,简单的复制存活对象到另一块,然后清除原有空间。但代价是会浪费一半空间。
这种算法在对象存活率较高时需要执行较多复制操作,而且为了应对极端情况,需要保证50%空余空间。
现代商业虚拟机使用这种算法回收新生代,IBM的研究表明,98%的新生代是朝生夕死的,所以不需要1::分配空间,而将空间分配为Eden和Survivor空间。回收时将Survivor和Eden中存活的对象拷贝到另一块Survivor,然后清理刚才用过的Eden和Survivor空间。
根据老年代存活时间长的特点,采用了标记整理算法。
其与标记清除算法一样,但是在标记之后让存活的对象向一端移动直至排列整齐,然后清理掉边界之外的内存。
此处略去
对象的分配优先在Eden
区分配,当剩余空间不足时触发Minor GC,当对象无法全部放入Survivor空间时,通过分配担保机制提前转移到老年代。
大对象指需要大量连续内存空间的Java对象,大对象对虚拟机的内存分配来说很苦难,因为有可能会触发GC来获取足够连续空间来安置他们。更糟的是生命周期短的大对象,在Java程序中应尽量避免。
如上文所述,每个对象有年龄计数器,当其大于阈值(默认15),则成为老年代。同样,也有动态判定,当其相同年龄对象大小总和大于Survivor空间一般,年龄大于等于该年龄直接进入老年代。
发生Minor GC时,会检测之前每次晋升到老年代的平均大小是否大于老年代的剩余空间大小,若大于则进行Full GC,小于则查看HandlePromotionFailure
是否允许担保失败,允许则Minor GC,不允许则Full GC。