程序计数器、虚拟机栈、本地方法栈这三个区域随线程而灭,栈中栈帧的内存大小也是在确定的。这几个区域的内存分配和回收都具有确定性,因此不需要过多考虑如何回收。
Java堆和方法区这两个区域有着很显著的不确定性
- 一个接口的实现类需要的内存可能不一样
- 一个方法所执行的不同条件分支所需要的内存可能不一样
只有处于运行期间,才能知道程序需要多少内存,这部分内存的分配和回收是动态的。垃圾收集器关注的正是这部分内存。
判断对象是否有用
- 引用计数算法
每个对象中添加一个引用计数器,每当有一个地方引用它,计数器值就加一;当失去一个引用时,计数器值就减一。任何时刻计数器为0的对象就是不可能再被使用的。
但是这个算法不能解决循环引用的问题。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024*1024;
private byte[] bigSize = new byte[2*_1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
System.gc();
}
}
但是实际上这两个对象可以被回收,所以实际上HotSpot使用的并不是这种内存回收方式。
- 可达性算法分析
通过这个算法用来判断对象是否存活。这个算法的基本思路就是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,如果某个对象不在链上,则说明这个对象可被回收。
可作为GC Roots的对象包括以下几种:
- 虚拟机栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象,譬如字符串常量池里的引用
- 本地方法栈中Native方法引用的对象
- Java虚拟机内部的引用
- 所有被同步锁持有的对象
- 反映Java虚拟机内部情况的JMXBean
除了这些还有其他对象可以临时性地加入GC Roots集合。
引用扩展
JDK 1.2之前,如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称这个reference数据是引用。
JDK 1.2之后,引用扩展为了强引用、软引用、弱引用、和虚引用4种,这4中引用强度依次减弱。
- 强引用指的是类似
Object obj = new Object()
的引用关系。只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。通过
SoftReference
类来实现软引用。 - 弱引用也是用来描述非必须对象的,但是只被弱引用关联着的对象,无论系统内存是否充足,在进行垃圾回收时都会被回收掉。通过
WeakReference
类来实现弱引用。 - 一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被回收时收到一个系统通知。通过
PhantomReference
类来实现虚引用。
回收过程
即使是在可达性分析算法中被判定为不可打的对象,也不一定会被回收。一个对象真正被回收需要经过两次标记过程:当这个对象不可达时进行第一次标记,随后判断这个对象是否有必要执行finalize()
方法;如果这个对象没有覆盖finalize()
方法或者finalize()
方法已经被虚拟机执行过了,那么就被视为“没有必要执行”。
如果这个对象被判定确有必要执行finalize()
方法,那这个对象会被放置在一个F-Queue
的队列中,并稍后由一条由虚拟机自动建立的、低调度优先级的Finalizer
线程去执行它们的finalize()
方法。如果这个对象在finalize()
方法中把自己和引用链上的任何一个对象关联了起来,那么这个对象就逃过了第二次标记。没有逃过标记的对象会被回收。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("yes, i am still alive :)");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
SAVE_HOOK = null;
System.gc();
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 :(");
}
}
}
运行结果
回收方法区
方法区的垃圾收集主要回收两部分内容:废弃的常量和不再使用的类。
判断一个常量是否废弃相对简单(查看引用个数),而要判定一个类是否不再使用,需要同时满足下面三个条件
- 该类的所有实例都已经被回收,Java堆中不存在该类及其派生子类的实例
- 加载该类的类加载器已经被回收(只出现在自己设计类加载器的场景)
- 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法