程序计数器、虚拟机栈、本地方法栈都是线程私有的,会随着线程而生,随线程而灭; 栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈操作. 每个栈帧中的本地变量表都是在类被加载的时候就确定的,每一个栈帧中分配多少内存基本上是在类结构确定时就已知了,因此这几块区域内存分配和回收都具备确定性,就不需要过多考虑回收问题了.
然而,Java堆和方法区中的内存清理工作就没那么容易了. 堆和方法区所有线程共享,并且都在JVM启动时创建,一直得运行到JVM停止时.因此它们没法随着线程的创建而创建、线程的结束而释放.
Java堆中存放JVM运行期间的所有对象,虽然每个对象的内存大小在加载该对象所属类的时候就确定了,但究竟创建多少个对象只有在程序运行期间才能确定. 方法区中存放类信息、静态成员变量、常量.类的加载是在程序运行过程中,当需要创建这个类的对象时才会加载这个类.因此,JVM究竟要加载多少个类也需要在程序运行期间确定. 因此,堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一点心思.
在对堆进行对象回收之前,首先要判断哪些是无效对象.我们知道,一个对象不被任何对象或变量引用,那么就是无效对象,需要被回收.一般有两种判别方式:
GC Roots对象包括: -虚拟机栈(栈帧中的本地变量表)中引用的对象
GC Roots并不包括堆中对象所引用的对象!这样就不会出现循环引用.
当经可达性算法筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会,具体过程如下:
注意:强烈不建议使用finalize()函数进行任何操作!如果需要释放资源,请使用try-finally或者其他方式都能做得更好. 因为finalize()不确定性大,开销大,无法保证各个对象的调用顺序.
以下代码示例看到:一个对象的finalize被执行,但依然可以存活
/**
* 演示两点:
* 1.对象可以在被GC时自救
* 2.这种自救机会只有一次,因为一个对象的finalize()方法最多只能被系统自动调用一次,因此第二次自救失败
* @author sss
* @since 17-9-17 下午12:02
*
*/
public class FinalizeEscapeGC {
private static FinalizeEscapeGC SAVE_HOOK = null;
private void isAlive() {
System.out.println("yes,I am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize methodd executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次成功自救
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级很低,所以暂停0.5s以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,I am dead :(");
}
// 自救失败
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no,I am dead :(");
}
}
}
运行结果
finalize methodd executed!
yes,I am still alive :)
no,I am dead :(
如果使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代,新生代中的对象"朝生夕死",每次垃圾回收都会清除掉大量的对象;而老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除掉.
由于方法区中存放生命周期较长的类信息、常量、静态变量,因此方法区就像是堆的老年代,每次垃圾收集的只有少量的垃圾被清除掉.
方法区中主要清除两种垃圾:
回收废弃常量和回收对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除.
判定无用类的条件则较为苛刻:
现在我们知道了判定一个对象是无效对象、判定一个类是无用类、判定一个常量是废弃常量的方法,也就是知道了垃圾收集器会清除哪些数据,那么接下来介绍如何清除这些数据.
最基础的收集算法,因为后续的算法也都是基于此思路并对其不足进行改进而得.
首先利用刚才介绍的方法判断需要清除哪些数据,并给它们做上标记;然后清除被标记的数据.
不足: 这种算法标记和清除过程效率都不高,且标记清除之后存在大量不连续的内存碎片,导致日后因为存储大对象时无法找到足够连续内存而提前触发另一次GC,降低了空间利用率.
效率偏低。
在回收垃圾前,标记过程仍与"标记-清除"算法一样,但后续不是直接清理可回收对象,而是让所有存活的对象移到一端,然后直接清掉端边界之外的内存.
当前商业虚拟机都采用此算法.根据对象存活周期的不同将Java堆划分为老年代和新生代,根据各个年代的特点使用最佳的收集算法.
Java中根据生命周期的长短,将引用分为4类
A a = new A();
也就是通过关键字new创建的对象所关联的引用就是强引用.
只要强引用还存在,该对象永远不会被回收.