收集器

JVM通过GC来回收堆和方法区中的内存,GC基本原理:

  1. 找到程序中不再被使用的对象
  2. 回收这些对象所占用的内存

通常采用收集器方式实现GC,主要的收集器有:

  1. 引用计数收集器
  2. 跟踪收集器

1. 引用计数收集器

采用分散式的管理方式,通过计数器记录对象是否被引用。 当计数器为零时,代表该对象不再被使用

引用计数法描述的算法为:给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1;任何时刻计数器为0的对象就是不能再被使用的,即对象已“死”。

  • 优点

    • 算法实现简单
  • 缺点

    • 不能解决对象之间循环引用的问题。有垃圾对象不能被正确识别,这对垃圾回收来说是很致命的,所以GC并没有使用这种搜索算法

2. 跟踪收集器/根搜索法/可达性分析算法

以一些特定的对象作为基础原始对象,或者称作“根”,不断往下搜索,到达某一个对象的路径称为引用链。

如果一个对象和根对象之间有引用链,即根对象到这个对象是可到达的,则这个对象是活着的,不是垃圾,还不能回收。例如,假设有根对象O,O引用了A对象,同时A对象引用了B对象,B对象又引用了C对象,那么对象C和根对象O之间的路径的可达的,C对象就不能当做垃圾对象。引用链为O->A->B->C。

反之,如果一个对象和根对象之间没有引用链,根对象到这个对象的路径是不可达的,那么这个对象就是可回收的垃圾对象。

  • 优点

    • 可找到所以得垃圾对象,并且完美解决对象之间循环引用的问题
  • 缺点

    • 不可避免地要遍历全局所有对象,导致搜索效率不高

根搜索算法是现在GC使用的搜索算法。

可以当做GC roots的对象有以下几种:

  • 虚拟机栈中的引用的对象。(java栈的栈帧本地变量表)
  • 方法区中的类静态属性引用的对象。
  • 方法区中的常量引用的对象。(声明为final的常量对象)
  • 本地方法栈中JNI的引用的对象。(本地方法栈的栈帧本地变量表)

下面是从网上找来的图,将就看看:



GC ROOTS就是跟对象节点,蓝色的是可达的引用链,引用链上的对象是活着的,不能被当做垃圾对象回收。相反暗灰色的路径表示不可达的路径,这些对象将会被回收。

每个圈圈里面的数字,表示其被引用的次数,没错,就是上面说到的引用计数法的计数值

那么什么是引用呢?

jdk1.2之前,定义为:如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就成为这块内存代表着一个引用。

那么我们好像对于那种如果希望内存足够的时候保留,不够的时候回收的对象一个十分明确的解释。

jdk1.2之后,扩展为:

  • 强引用:只要存在,垃圾收集器就不会回收对象。
1
Object obj = new Object();
  • 软引用:用来描述一些还有用但是不必须的对象,系统将要发生内存溢出异常之前,将会把这些对象列入回收范围之中进行第二次回收,如果还是不够那就只能抛出内存溢出的异常了。
1
SoftReference<String>s = new SoftReference<>(“我还有用但不是必须的!”);
  • 弱引用:用来描述非必须对象,但是强度比弱引用更弱,被弱引用关联的对象只能生存到下一次垃圾收集发生之前,垃圾收集工作的时候,无论是否必要都会回收掉只被弱引用关联的对象。
1
WeakReference<String>s = new WeakReference<String>(“我只能活到下一次垃圾收集之前”);
  • 虚引用(幽灵引用或幻影引用):一个对象是否有虚引用,与其生命周期毫无关系,也无法通过虚引用取得一个对象实例,只被虚引用的对象,随时都会被回收掉
1
PhantomReference<String>ref = new PhantomReference<String>(“我只能接受死亡通知”) , targetReferenceQueue<String>);

不可达对象非死不可吗

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SaveSelf {
public static SaveSelf instance = null;

@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize method executed!");
instance = this;
}
public void isAlive(){
System.out.println("I'm alive!");
}

public static void main(String[] args) {
instance = new SaveSelf();
instance = null;
System.gc();
try {
Thread.sleep(500);//用于等待Finalize线程执行finalize方法
if (instance != null){
instance.isAlive();
}else{
System.out.println("I will dead!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}

instance = null;
System.gc();
try {
Thread.sleep(500);
if (instance != null){
instance.isAlive();
}else{
System.out.println("I will dead!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
finalize method executed!

I’m alive!

I will dead!

Process finished with exit code 0

任何一个兑现过的finalize方法都会只被系统自动调用一次。当然finalize方法一般用来回收一些外部资源。

回收方法区

方法区也是有垃圾收集的,那么为什么会有呢?因为如果常量池中存在一个”abc”,而没有任何的String对象引用常量“abc”,那么我们需要对这个常量进行回收的。另外永久代的垃圾收集主要包括废弃常量和无用的类。

判定一个废弃常量很简单,那么如何判定一个无用的类(类对象比如Integer)呢?

  • 该类的所有实例都已经被回收,也就是java堆中不存在任何该类的实例
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过访问该类的方法。

当然这里的是可以回收,但不一定必然回收。

HotSpot中的实现

枚举根节点

可达性分析对于时间的敏感性:可达性分析的过程必须在一个确保一致性的快照中进行

这里“一致性”的意思是指在整个分析的过程中执行系统看起来像是被冻结在某个时间点,不可以出现分析过程中对象的引用关系还在不断变化的情况,这样子分析结果的准确性就无法得到保证。这点是导致GC进行时必须停顿所有执行线程(Stop the world)的一个重要原因,即使是在号称(几乎)不会发生停顿的CMS收集器中,枚举根结点也是必须要停顿的。

在HotSpot虚拟机中,使用了一组成为OopMap的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就可以将对象内什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下战和寄存器中哪些位置是引用。

安全点

HotSpot没有为每条指令都生成OopMap,因为这需要大量的内存空间,所以只是在“特定的位置”记录了这些信息,这些位置称为安全点(SafePoint),即程序执行时并非在所有地方都能停顿下来开始GC,只有在到达安全点的时候才能进行暂停。

安全点的选定基本上是以程序“是否具有让程序长时间执行的特征”为标准进行选定的–因为每条指令执行的时间都非常短暂,程序不太可能因为指令流长度太长这个原因而长时间运行,而“长时间执行”最明显的特征就是指令序列服用,例如方法调用、循环跳转、一场跳转等,所以具有这些功能的指令才会产生Safepoint。

如何中断
  1. 抢先式中断:不需要线程的执行代码主动的配合,在GC执行的时候,首先把所有线程全部中断,如果发现有线程中断的地方不再安全点上,就恢复线程,让它“跑”到安全点上。
  2. 主动式中断:当GC需要进行中断线程的时候,不直接对线程操作,仅仅简单的设置一个标志,所个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起,轮询标志的地方和安全点是重合的,另外再加上创建对象的时候需要分配内存的地方。

几乎没有虚拟机采用抢先式中断!

安全区域

使用安全点的方法保证了程序执行时,在不太长的时间内就会遇到可以进入GC的安全点,但是如果程序不执行的时候,比如所谓的程序不执行就是没有分配到CPU时间,即处于Sleep状态或者Blocked状态,这时候线程无法响应JVM的中断请求,“走“到安全的地方去中断挂起,JVM显然不太可能等待线程重新被分配CPU时间。

安全区域是指在一段代码片段中,引用关系不会发生改变,这个区域中的任何位置开始进行GC都是安全的,我们可以把安全区域看作是拓展了的安全点。

当线程执行到安全区域的时候,首先标识自己进入了安全区域,那样,当这段时间里JVM要发起GC的时候,就不需要管标识自己为安全区域的线程,在线程要离开安全区域时,它要检查系统是否完成了根节点枚举(或者是整个GC过程),如果要完成了,线程就继续执行,否则就必须等待直到回收过程完成并可以安全离开安全区域的信号为止。