程序计数器、虚拟机栈、本地方法栈都是线程私有的,会随着线程而生,随线程而灭
堆和方法区的内存回收具有不确定性,因此垃圾收集器在回收堆和方法区内存的时候花了一点心思.
栈中的栈帧随着方法的进入和退出有条不紊的执行着出栈和入栈操作.
静态内存分配和回收
- 静态内存分配是指在程序开始运行时由编译器分配的内存,在被编译时就已经能够确定需要的空间,当程序被加载时系统把内存一次性分配给它,这些内存不会在程序执行时发生变化,直到程序执行结束时才回收内存.。
- 包括原生数据类型及对象的引用。
- 这些静态内存空间在栈上分配的,方法运行结束,对应的栈帧撤销,内存空间被回收.。
- 每个栈帧中的本地变量表都是在类被加载的时候就确定的,每一个栈帧中分配多少内存基本上是在类结构确定时就已知了,因此这几块区域内存分配和回收都具备确定性,就不需要过多考虑回收问题了.。
动态内存分配和回收
- 在程序执行时才知道要分配的存储空间大小,对象何时被回收也是不确定的,只有等到该对象不再使用才会被回收。
1 Java堆内存的回收
1.1 判定回收的对象
在对堆进行对象回收之前,首先要判断哪些是无效对象即一个对象不被任何对象或变量引用,需要被回收。
一般有两种判别方式:
1、引用计数法 (Reference Counting)
每个对象都有一个整型的计数器,当这个对象被一个变量或对象引用时,该计数器加一;当该引用失效时,计数器值减一.当计数器为0时,就认为该对象是无效对象.
2、可达性分析法 (Reachability Analysis)
所有和GC Roots直接或间接关联的对象都是有效对象,和GC Roots没有关联的对象就是无效对象.
-
GC Roots对象包括:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈JNI(即所谓的Native方法)引用的对象
GC Roots并不包括堆中对象所引用的对象!这样就不会出现循环引用.
- 两者对比
引用计数法虽然简单,但存在无法解决对象之间相互循环引用的严重问题,且伴随加减法操作的性能影响。
因此,目前主流语言均使用可达性分析方法来判断对象是否有效.
2 回收无效对象的过程
当经可达性算法筛选出失效的对象之后,并不是立即清除,而是再给对象一次重生的机会,具体过程如下:
1、判断是否覆盖finalize()。
- 未覆盖该或已调用过该方法,直接释放对象内存
- 已覆盖该方法且还未被执行,则将finalize()扔到F-Queue队列
3、对象重生或死亡。
- 如果在执行finalize()方法时,将this赋给了某一个引用,则该对象重生
- 如果没有,那么就会被垃圾收集器清除
注意:强烈不建议使用finalize()进行任何操作!
如果需要释放资源,请用try-finally或者其他方式都能做得更好.
因为finalize()不确定性大,开销大,无法保证各个对象的调用顺序.
以下代码示例看到:一个对象的finalize被执行,但依然可以存活
/**
* 演示两点:
* 1.对象可以在被GC时自救
* 2.这种自救机会只有一次,因为一个对象的finalize()最多只能被系统自动调用一次,因此第二次自救失败
* @author s
* @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 :(
3 方法区的内存回收
如果使用复制算法实现堆的内存回收,堆就会被分为新生代和老年代。
- 新生代中的对象"朝生夕死",每次垃圾回收都会清除掉大量对象
- 老年代中的对象生命较长,每次垃圾回收只有少量的对象被清除
由于方法区中存放生命周期较长的类信息、常量、静态变量。
因此方法区就像堆的老年代,每次GC只有少量垃圾被清除。
方法区中主要清除两种垃圾:
- 废弃常量
- 无用类
3.1 回收废弃常量
回收废弃常量和回收对象类似,只要常量池中的常量不被任何变量或对象引用,那么这些常量就会被清除.
3.2 回收无用类
判定无用类的条件则较为苛刻:
- 该类所有实例都已被回收,即Java堆不存在该类的任何实例
- 加载该类的ClassLoader已被回收
- 该类的java.lang.Class对象没有被任何对象或变量引用,无法通过反射访问该类的方法。只要一个类被虚拟机加载进方法区,那么在堆中就会有一个代表该类的对象:java.lang.Class.这个对象在类被加载进方法区的时候创建,在方法区中该类被删除时清除.