本篇将带你入门学习JVM中的垃圾回收机制
目录
如何判断对象可以回收
引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。
可能存在的问题:对象之间相互循环引用
可达性分析
以GC Roots的对象为起点,从这些节点开始向下搜索,节点所走过的路径为引用链。不在引用链中的对象需要被回收。
哪些对象可以作为 GC Roots ?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 本地方法栈(Native 方法)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 所有被同步锁持有的对象
四种引用
不论是引用计数法还是可达性分析,都与引用有关。
强引用
我们使用的大部分引用都是强引用。垃圾回收器宁愿抛出OutOfMemoryError也不会回收具有强引用的对象
软引用
如果说具有强引用的对象像生活中的必需品,那么只具有软引用的对象就像生活中可有可无的物品。垃圾回收器一般不会回收这一类对象。如果内存空间不足,就会回收这一些对象。软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
弱引用
垃圾回收器线程在扫描内存区域过程中一旦发现只具有弱引用的对象,不管内存空间是否充足,都会回收它的内存。由于垃圾回收器是个优先级很低的线程,所以只具有弱引用的对象也不至于很快被发现。
虚引用
虚引用又称为幽灵引用,它形同虚设,如果一个对象只具有虚引用,那么它在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
软引用和弱引用可以和引用队列联合使用,而虚引用必须和引用队列联合使用。这会使得当它们所引用的被垃圾回收时,JVM会把这些引用加入到与之关联的引用队列中。当调用引用队列的poll方法,就会依次输出存在于引用队列中的引用。
GC分类
- Young GC(Minor GC):回收年轻代
- Old GC(Major GC):回收老年代
- Full GC:对整个堆空间进行垃圾回收
垃圾收集算法
- 标记-清除算法
将不打算垃圾回收的区域进行标记,然后设置为未标记的位置已被回收。这个方法会导致存在内存碎片。
- 标记-复制算法
将不打算垃圾回收的区域复制到一个新的区域中,这样就不会有内存碎片,但是需要额外空间开销。
- 标记-整理算法
将不打算垃圾回收的区域标记,然后将没标记的区域清理后,对剩余的区域进行整理,不会有内存碎片产生,但是需要额外时间开销。
- 分代收集算法
依据不同年代的特点选择合适的垃圾收集算法,垃圾收集算法一般从以上三个方法中挑选
HotSpot为什么要将堆分为新生代和老年代?
答:将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。(摘自JavaGuide)
附上我自己的理解:如果不分代,我们每次都进行Full GC,性能会比较差。
垃圾收集器
不同新生代的垃圾回收器可以和不同老年代的垃圾回收器自由组合
Serial 收集器 和 Serial Old 收集器
特点:单线程,且过程需要进行STW(Stop The World),简单高效,可以用在客户端模式下的虚拟机。
新生代使用标记-复制算法,老年代使用标记-整理算法。
ParNew 收集器 和 Parallel Old 收集器
Serial收集器的多线程版本,除了使用多线程意以外与Serial收集器一致。ParNew收集器虽然使用多线程来并发进行垃圾收集,但是并没有与用户线程同时运行。过程需要进行STW(Stop The World)。
新生代使用标记-复制算法,老年代使用标记-整理算法。
Parallel Scavenge 收集器
Parallel Scavenge收集器的关注点是吞吐量,追求高吞吐量即追求高效率地利用CPU。让单位时间内,STW 的时间最短,0.2 + 0.2 = 0.4,垃圾回收时间占比最低,这样就称吞吐量高。这是JDK8的默认收集器。过程需要进行STW(Stop The World)。
新生代使用标记-复制算法,老年代使用标记-整理算法。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。尽可能让单次 STW 的时间最短 0.1 + 0.1 + 0.1 + 0.1 + 0.1 = 0.5。CMS收集器是第一款真正意义上的并发收集器,它第一次让垃圾收集线程与用户线程同时工作。
CMS工作过程分为四个阶段:
- 初始标记: 记录下直接与 root 相连的对象,需要STW(Stop The World),速度很快。
- 并发标记: 同时开启 GC 和用户线程,记录可达对象。这个阶段结束时不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。不需要STW。
- 重新标记: 修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,需要STW,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。不需要STW。
总的来说,CMS中需要STW的阶段需要的时间都很短,所以用户体验较好。
CMS在并发清除阶段使用的是标记-清除算法。因为CMS是进行并发清除,在清除过程中对标记的对象进行清理,而没标记的对象保持不动,这样才能与用户线程同时并发进行而不影响用户线程。
这也导致CMS有三个明显的缺点:
1、无法处理浮动垃圾(在并发标记阶段产生了新垃圾)
2、垃圾收集结束后会有空间碎片(因为使用标记-清除算法)
3、对CPU 资源敏感(并发时与用户线程抢占CPU)
G1收集器
G1 - Garbage-First 名字的由来:
G1收集器会在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region 。 这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
G1的特点:整体上是标记-整理算法,两个区域之间是标记-复制算法
G1的内存结构:棋盘状,分为多个Region,Region的大小为1M~32M
E为新生代Eden,S为幸存区Survive,O为老年代Old
当声明的对象的大小为特殊情况时:
1、0.5倍Region < 对象大小 < 1倍Region,直接存储到O区且标记为H区
2、对象大小 > 1倍Region,使用多个连续Region区来存储该对象且标记为H区
G1中的一些概念:
-
Rset(Remembered Set),存储在Region当中,记录引用了当前对象的其它Region对象。新生代和老年代都具有Rset,记录的都是哪些老年代对象引用了自己。Rset中记录了有老年代对象引用了自己的Region区在垃圾收集时不能被回收。
Rset本质是一种哈希表,key是本身Region的地址,value是引用了它的对象的卡页集合。
-
卡表(Card Table),每个Region又被分成了若干个大小为512字节的Card,这些Card都会记录在全局卡表中。Card中的每个元素对应着其标识的内存区域中一块特定大小的内存块,这个内存块被称为卡页。一个卡页的内存中通常不止一个对象,只有卡页中有一个及以上对象的字段存在着跨Region引用,这个对应的元素的值就标识为1。
比如G1默认的Region有2048个,默认每个Region为2M,那每个Region对应的Card的每个元素对应的卡页的大小为2M / 512=4K,即这4K内存中只要有一个或一个以上的对象存在着跨Region对年轻代的引用,这个卡页对应的Card的元素值为1。
这样在Minor GC时,只需要将变脏的Region中的那个卡页加入GC Roots一并扫描即可。比起扫描老年代的所有对象,大大减少了扫描的数据量,提升了效率。(这一段引用自硬核子牙)
Rset记录的是谁引用了我,卡表记录的是我引用了谁
- Cset(Collection Set),回收集合,保存了本次GC需要清理的Region集合。
G1垃圾回收阶段:
这三个阶段会循环进行
- Young GC,年轻代的回收,将Eden和S(from)区中存活下来的对象复制到S(to)区,这时候会STW。当幸存区中的对象到达一定年龄时会晋升到老年代区中。在这个阶段中也会进行GCRoot的初始标记。
- Concurrent Mark,并发标记,当堆中老年代占用堆空间达到一定比例(默认为45%)时会发生并发标记,这个阶段不会发生STW。
- Mix GC,混合回收,会对E、S、O三个区域进行全面的垃圾回收。在这个阶段中对老年代对象的回收,会有选择地选出回收价值高(即能释放出更多空间)的对象进行回收。在这个阶段中有重新标记和并发清除,与CMS一致,且这两步骤会STW。
但是G1收集器在垃圾回收时的标记方面也可以分为以下阶段
- 初次标记 => 标记GCRoot对象所在的Region,称为RootRegion,会STW
- 使用上一步得到的RootRegion,遍历判断老年代区Region中的Rset中是否含有RootRegion中的对象(即有没有RootRegion中的对象引用了我),有的话会标识出这些老年代Region
- 并发标记 => 同CMS,但是遍历范围缩小,只需要遍历上面标识出来的老年代Region(因为没有被标记的老年代Region没有被GCRoot对象引用,将会被清除)
- 重新标记 => 同CMS,会STW
- 并发清除 => 采用标记-复制的算法,会STW
参考: