JVM垃圾收集机制

GC需要完成三件事:

  • 哪些对象需要回收?
  • 何时进行回收?
  • 怎么样回收?

哪些对象需要回收

死掉的对象需要回收。

如何判断对象已死?

可达性分析算法+finalize().

可达性分析

把一系列称为”GC Roots”的对象作为起点,向下进行搜索,当GC Roots到某个对象不可达时,这个对象就是可回收的。

GC Roots对象包括:

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中引用的对象

为什么不使用引用计数法呢?

引用计数法就是每当加了一个引用,引用计数器加一,一个引用失效,引用计数器减一,引用计数器为零时该对象死亡。

但是引用计数无法解决的是循环引用的问题。

循环引用:

!title

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Test {

public static void main(String[] args) {
// TODO Auto-generated method stub
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();

object1.object = object2;
object2.object = object1;

object1 = null;
object2 = null;
System.gc();
}

finalize()

即使某个对象是不可达的,也并不一定非死不可。宣告一个对象死亡,要经过两次标记过程:第一个是GC Roots不可达,第二步是此对象是否有必要执行finalize()方法

如果该对象重写了finalize()方法且finalize()方法还没有被虚拟机所调用,则其对象需要执行该方法。

那么,该对象会放入一个队列之中,并由一个Finalizer线程去执行finalize()方法。finalize方法是对象拯救自己的最后一次方法,只需要与任何一个GC Roots建立关联即可。这样他就还是存活的。

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
public class FinalizeEscape {
public static FinalizeEscape SAVE=null;
@Override
protected void finalize() throws Throwable {
// TODO Auto-generated method stub
super.finalize();
System.out.println("finalize excute....");
FinalizeEscape.SAVE=this;
}

public void isAlive() {
System.out.println(" i am still alive .....");
}
public static void main(String[] args) throws Exception{
// TODO Auto-generated method stub
SAVE=new FinalizeEscape();
SAVE=null;
System.gc();
Thread.sleep(500);
if(SAVE==null) {
System.out.println(" i am dead .....");
}else {
SAVE.isAlive();
}

SAVE=null;
System.gc();
Thread.sleep(500);
if(SAVE==null) {
System.out.println(" i am dead .....");
}else {
SAVE.isAlive();
}
}
/*
输出:
finalize excute....
i am still alive .....
i am dead .....
*/

由此可见,SAVE对象的finalize()方法确实执行了,并在收集前成功逃脱了。

代码中有两段完全一样的方法,第一次成功逃脱,第二次因为已经执行过了finalize()方法,所以也就不在执行了,因此第二段代码逃脱失败。

然而,并不鼓励使用finalize()方法。

不推荐使用finalize()

四种引用类型

title

  • 强引用:

    强引用是使用最普遍的引用。Object obj =new Object(); 如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 软引用:

    如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。使用SoftReferrnce类实现软引用。

    缓存使用。

  • 弱引用:

    被弱引用引用的对象只能生存到下一次垃圾回收之前。当GC开始工作时,无论内存是否充足,都会回收弱引用引用的对象。使用WeakReference来实现弱引用类。

    ThreadLocal。

  • 虚引用:

    顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。

    当垃圾回收器回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。由于Object.finalize()方法的不安全性、低效性,常常使用虚引用完成对象回收前的资源释放工作。

方法区GC

方法区主要回收废弃常量以及无用的类。

废弃常量,没有地方引用他。

废弃类需要满足以下条件:

  • 该类所有实例已经被回收。
  • 加载该类classLoader已经被回收。
  • 该类的class对象没有在任何地方被引用。

对于jdk8之后方法区变为了元空间,如果Metaspace的空间占用达到了设定的最大值,也会触发GC来收集死亡对象和类的加载器。

MetaSpace GC

GC算法

标记-清除

title

标记出所有需要回收的对象,然后将做了标记的都给清除。缺点是导致内存碎片化。

复制

title

将内存一般分为A区域,一半分为B区域。图中我们将前两行分为A,后两行分为B。刚开始的时候,我们只使用A区域的内存,而不使用B区域的内存。

第一次GC,经过一次可达性分析后,我们将A中存活对象直接复制到B区域,然后直接将整块A区域清除。A区域变成未使用的。第二次GC,同理,将B的存活对象复制到A,将B清除,B变为空。

这样A和B区域交互使用。

这个算法可以解决内存碎片化的问题,但是会导致内存浪费,一次只能使用一半的内存。

新生代主要使用的是复制算法。一般来说,Eden:Survior1:Survior2=8:1:1,因为每次GC新生代垃圾都会有75%-90%,这样,直接将Eden幸存的对象复制到Survior1区域中,然后将Eden区域清除,第二次清除时,将Eden区域和S1区域幸村对象复制到S2区域,将Eden和S1区域清除,就这样,S1,S2两个区域交替使用,新生代内存利用空间可以达到90%,而且解决了内存碎片化的问题。注意,当Survior内存区域不够时(多于10%对象存活),可以向老年代进行分配担保。

适用于存活率比较低的对象,要是存活率过高的话,会造成大量复制,效率变低

标记-整理

复制算法在对象存活率较高时就会产生一个问题,因为要进行过多的复制操作,效率会降低,而且浪费空间会比较多。对于老年代,存活对象率比较高,而且对象比较大,占用内存大,所以不宜使用复制算法,采用标记整理算法。

title

将存活的对象移到回收对象留下的空间里,以形成连续的内存。

适用于存活率较高的。

总结

新生代中,每次GC都有大量对象死去,少量存活,选用复制算法。

老年代中,对象存活率高,没有额外空间进行内存担保,使用标记-整理。

HotSpot算法实现

两个问题

  • 寻找GC Roots效率问题,如果逐个检查引用,太慢。

    使用OopMap来解决,这个数据结构存储了引用以及他的作用范围(从哪个指令开始到哪个指令结束)。

    在类加载完成的时候,就生成了一个OopMap。

  • 一致性问题。寻找GC Roots这个阶段需要保证引用情况不再发生变化,因此需要发生GC停顿。

OopMap与Rememebered Set

OopMap

编译时就有了。

用于枚举GC Roots。

当垃圾回收时,收集线程会对栈上的内存进行扫描,看看那些位置上存储了Reference类型。如果发现了某个位置上存储的是Reference类型,就意味着这个引用所指向的对象在这一次垃圾回收过程中不能够回收。

但是要是逐个检查引用,这一样效率太低了。

于是采用空间换时间的方法,把栈中是引用类型的变量的位置记录下来,这样他指向的对象肯定是GC Roots。这样,再做GC的时候,就可以直接读取,不用全部扫描了。

一个线程意味着一个栈,一个栈由多个栈帧组成,一个栈帧对应着一个方法,一个方法里面可能有多个安全点。 gc 发生时,程序首先运行到最近的一个安全点停下来,然后更新自己的 OopMap ,记下栈上哪些位置代表着引用。枚举根节点时,递归遍历每个栈帧的 OopMap ,通过栈中记录的被引用对象的内存地址,即可找到这些对象( GC Roots )。

因为一个方法有多个安全点,每个安全点就有一个OopMap,所以,一个方法里有多个OopMap。

可以把oopMap简单理解成是调试信息。在源代码里面每个变量都是有类型的,但是编译之后的代码就只有变量在栈上的位置了。oopMap就是一个附加的信息,告诉你栈上哪个位置本来是个什么东西。 这个信息是在JIT编译时跟机器码一起产生的。因为只有编译器知道源代码跟产生的代码的对应关系。 每个方法可能会有好几个oopMap,就是根据safepoint把一个方法的代码分成几段,每一段代码一个oopMap,作用域自然也仅限于这一段代码。 循环中引用多个对象,肯定会有多个变量,编译后占据栈上的多个位置。那这段代码的oopMap就会包含多条记录。

Rememebered Set

RememberedSet 用于处理这类问题:比如说,新生代 gc (它发生得非常频繁)。一般来说, gc 过程是这样的:首先枚举根节点。根节点有可能在新生代中,也有可能在老年代中。这里由于我们只想收集新生代(换句话说,不想收集老年代),所以没有必要对位于老年代的 GC Roots 做全面的可达性分析。但问题是,确实可能存在位于老年代的某个 GC Root,它引用了新生代的某个对象,这个对象你是不能清除的。那怎么办呢?

维护一个表,记录别的代对新生代的引用关系,这个表叫Remembered Set。

在G1收集器中,堆被分成一个个region,难免会存在别的region中的对象会引用某个region的对象,那么,就对每一个region维护一个Remembered Set,记录其他所有region对象对他其中对象的引用。

安全点

在OopMap的帮助下,可以很容易的寻找GC Roots,但是,每一个指令都可能导致OopMap的变化,如果为每一条指令都生成一个对应的OopMap,那么,将会需要大量的空间。于是,HotSpot只是在特定的点记录了这些信息,这些点叫做安全点,程序旨在安全点才停下来执行GC。

如何让让所有的线程跑到安全点中断呢?

抢先式中断和主动式中断。

抢先式中断是把所有的线程都中断,然后把不在安全点上的线程恢复,直到他到达安全点上。

主动式中断:设置一个中断标志,各个线程主动区轮询这个标志,发现中断标志为真时,自己主动挂起。

垃圾收集器

所有的收集器都避免不了stop the word,只可能尽可能的缩短。

title点击并拖拽以移动

以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。

  • 单线程与多线程:单线程指的是垃圾收集器只使用一个线程,而多线程使用多个线程;
  • 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。

Serial

  • 适用于新生代和老年代。

  • 单线程收集器,在他进行垃圾收集时,必须暂停所有其他工作的线程。

  • 新生代采用复制算法,老年代采用标记-整理算法。

ParNew

  • Serial的多线程版本。除了Serial,只有ParNew可以与CMS一起使用。

  • 适用于新生代和老年代。

  • 新生代采用复制算法,老年代采用标记-整理算法。

Parallel Scavenge+Parallel old

Parallel Scavenge一个新生代收集器,特点是吞吐量优先。经常与Parallel Old一起使用 。

注重吞吐量的情况下,使用Parallel Scavenge+Parallel old(科学计算,天文计算等)

Parallel Scavenge新生代采用复制算法,Parallel old老年代采用标记-整理。

CMS

基于标记-清除算法。并不是标记整理。

注重于获取最短停顿时间。并发收集,分区处理。停顿时间短,在垃圾收集的时候,JVM还可以运行。

img

分为以下四个流程:

  • 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
  • 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
  • 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
  • 并发清除:不需要停顿。

在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。

CMS具有以下缺点:

  • 在并发标记以及并发清除阶段,GC会占用一部分的CPU资源,会造成吞吐量下降CMS 默认启动的回收线程数=(CPU 数目+3)4 当 CPU 数>4 时, GC线程一般占用不超过 25%的 CPU 资源, 但是当 CPU 数<=4 时, GC线程 可能就会过多的占用用户 CPU 资源, 从而导致应用程序变慢, 总吞吐量降低.。
  • 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
  • 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。

G1

用来替代CMS的。

特点

  • 采用的是标记-整理算法+复制算法,避免产生内存空间碎片。标记整理出需要回收的region,region间使用复制算法。因此,从整体上看,G1是基于标记-整理的,从局部上来看(两个region之间),是复制算法。
  • 一般的垃圾收集器将内存分为Eden,Survior以及Old三类,且各个代都是连续的。而G1将整个Java堆分成一个个相等的独立区域,虽然还有分代的概念,但各个代不再是连续的,新生代和老年代不再物理隔离。内存的粒度变得更小了。
  • 可预测的停顿。G1每次回收不是收集整代内存,而是根据优先列表收集几块内存(region),到底要回收多少需要看用户的垃圾收集时间配置,配置的时间长,收集的就多。
  • 如果应用的内存非常吃紧,对内存进行部分回收根本不够,始终要进行整个Heap的回收,那么G1要做的工作量就一点也不会比其它垃圾回收器少,而且因为本身算法复杂了一点,可能比其它回收器还要差。因此G1比较适合内存稍大一点的应用(一般来说至少4G以上)。

title

通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。

每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。

适用场景

  • 服务端多核CPU、JVM内存占用较大的应用。
  • 运行过程中会产生大量内存碎片。
  • 想要可控的,可预期的GC停顿时间。

收集过程

  • 初始标记,标记GC Roots直接关联对象,需要暂停,时间短
  • 并发标记,对GC Roots进行可达性分析,并发执行,时间比较长。
  • 最终标记,修正并发标记阶段而产生的变动,这一段是暂停的。
  • 筛选回收,将各个region根据回收价值和回收成本进行排序,然后进行收集。这个阶段需要暂停用户线程。

Minor GC and Full GC

Minor GC

回收新生代,因为新生代对象存活时间很短,因此Minor GC会频繁进行,执行速度也比较快。
当Eden区域满了的话,会触发Minor GC。

Full GC

回收新生代和老年代,老年代因为存活时间比较长,因此Full GC很少执行,速度也比较慢。

触发Full GC:

  • 老年代空间不足。

  • 空间分配担保失败。

    新生代采用复制收集算法,需要将存活的对象复制到survivor中,然后直接清理Eden区,但是会有一种情况,就是存活的对象大于survivor内存空间,这样,就需要老年代分配担保,将survivor中无法分配的对象放入老年代。但是,万一老年代也不够用呢?

    加入老年代剩余最大连续可用空间大于Eden区,那么肯定可以直接放。

    否则的话,看老年代是否允许担保失败,可以的话,检查老年代剩余最大连续可用空间是否大于历次晋升到老年代对象的平均大小,如果大于,尝试进行Minor GC,小于的话,直接Full GC。

  • CMS垃圾收集器浮动垃圾的问题。因为在CMS并发清理阶段用户线程也在运行,所以需要留出一定的空间做缓冲。这样,老年代没有满的时候就需要触发Full GC,默认是92%。但要是预留的空间无法满足程序需要,就会报 Concurrent Mode Failure 错误,并触发 Full GC。

对象分配策略

  • 对象优先在Eden区分配。当Eden区没有足够的空间进行分配时,会触发Minor GC。如果启动了TLAB,那么优先在TLAB上分配,G1默认就是启动TLAB的。

  • 大对象直接进入老年代。

  • 长期存活对象将进入老年代,对象每熬过一次Minor GC,年龄增加一岁,当年龄达到阈值(默认是15),那么这个对象晋升到老年代。

  • 动态对象年龄判定。虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。

参考

OopMap详解

OopMap与Remembered Set

G1垃圾收集器