讲讲JVM的垃圾回收
带着问题学习才是最有效率的!
一、引言
本文将尽可能详细的说明JVM的垃圾回收算法与垃圾回收器,算是对上篇JVM总结的补充,以下是链接,需要JVM基础的请看 JVM知识梳理
首先放JVM最重要的图(内存模型),要是这张图记不住,先复习基础吧!!!
看了内存模型图,这时有一个疑问,GC(垃圾回收)发生在哪些地方?
答案:堆、方法区
这里我们主要讲堆的回收机制。单看堆的结构图如下:
图中所示堆分为新生代和老年代:
-
新生代(Young Generation):发生Minor GC
- Eden区
- Survivor0(From)区
- Survivor1(To)区
-
老年代 (Old Generation):发生Major GC
- Old Memory
可以通过设置JVM参数改变默认内存大小和查看回收情况:
#设置新生代堆大小
-Xmn
#设置最小堆内存分配大小 一般1/64
-Xms1m
#设置最大分配堆内存 默认1/4
-Xmx8m
#对象进入老年代的阈值
XX:MaxTenuringThreshold
//打印GC垃圾回收
-XX:+PrintGCDetails
//OOM的Dump文件
-XX:+HeapDumpOnOutOfMemoryError
1. 回收过程:
a. 大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1, 并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1) 当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold
来设置。(“Hotspot遍历所有对象时,按照年龄从小到大a对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。)
b. 经过这次GC后,Eden区和"From"区已经被清空。这个时候,“From"和"To"会交换他们的角色,也就是新的"To"就是上次GC前的“From”,新的"From"就是上次GC前的"To”。不管怎样,都会保证名为To的Survivor区域是空的。
c. Minor GC会一直重复这样的过程,直到“To”区被填满,"To"区被填满之后,会将所有对象移动到老年代中。
2. 什么情况下,对象会进入老年代?
- 如同上面步骤所讲,当一个对象的年龄达到15的时候,会被晋升到老年代(这个年龄并不是所有都默认15,这个是要分收集器的, parallel:15,CMS:6)
- 当创建对象时候,eden区已经满了(也就是说没有足够的空间给新对象),虚拟机将发起一次MinorGC,这时发现占满eden区的前一个大对象无法存入Survivor区,所以 只好通过 分配担保机制 把新生代的对象提前转移到老年代中去,老年代上的空间足够存放,所以不会出现 Full GC, 后面分配的对象如果能够存在 eden 区的话,还是会在 eden 区分配内存。
- 大对象直接进入老年代(比如:字符串、数组)
3. GC的准确区域
刚开篇我们说垃圾回收有两个部分:堆和方法区。而本篇又是讲堆的GC,那么由上面的堆内存划分图更细致地来又是如何分区的呢?
针对于HotSpot VM,它的GC准确来说又两大块:
-
Partial GC:并不收集整个GC堆
- Young GC:只收集 Young gen的GC,当eden满时候触发,存活对象会进入old gen,所以young GC后old gen占用会变高
- Old GC:只收集 old gen 的GC。只有 CMS 的concurrent collection是这个模式!
- Mixed GC:收集整个 young gen 以及部分 old gen 的GC。只有 G1 有这个模式!
-
Full GC:收集整个堆,包括Young gen、old gen、perm gen
Full GC除了正常情况下满了之后进行,还有就是当发生一次young GC的时候发现晋升大小大于old gen剩余空间时候会转化为Full GC。(除了CMS 的concurrent collection模式,其他收集器的old gen GC会收集整个堆)再或者,如果有perm gen 并且分配空间时候空间不足,也会FullGC。System.GC()、heap dump的GC 默认会Full GC
通常来说 Major GC 和 Full GC 是等价的,可以看到堆内存图的标注是只收集老年代,并不是不对而是多年来的词语为了方便理解混淆了而已。
4. 哪些对象可回收?如何判断对象已死亡?
堆中几乎放着所有的对象实例,对堆垃圾回收前的第一步就是要判断那些对象已经死亡(即不能再被任何途径使用的对象)。
4.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加 1;当引用失效,计数器就减 1;任何时候计数器为 0 的对象就是不可能再被使用的。 这个方法实现简单,效率高,但是目前主流的虚拟机中并没有选择这个算法来管理内存,其最主要的原因是它很难解决对象之间相互循环引用的问题。
4.2 可达性分析算法
基本思想就是通过一系列的称为 “GC Roots” 的对象作为起点,从这些节点开始向下搜索,节点所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连的话,则证明此对象是不可用的。
4.3 引用
无论是通过引用计数法判断对象引用数量,还是通过可达性分析法判断对象的引用链是否可达,判定对象的存活都与**“引用”**有关。
1.强引用(StrongReference)
以前我们使用的大部分引用实际上都是强引用,这是使用最普遍的引用。如果一个对象具有强引用,那就类似于必不可少的生活用品,垃圾回收器绝不会回收它。当内存空间不足,Java 虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足问题。
2.软引用(SoftReference)
如果一个对象只具有软引用,那就类似于**可有可无的生活用品。如果内存空间足够,垃圾回收器就不会回收它,如果内存空间不足了,就会回收这些对象的内存。**只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。
软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,JAVA 虚拟机就会把这个软引用加入到与之关联的引用队列中。
3.弱引用(WeakReference)
如果一个对象只具有弱引用,那就类似于可有可无的生活用品。
弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。
弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。
4.虚引用(PhantomReference)
"虚引用"顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收。
虚引用主要用来跟踪对象被垃圾回收的活动。
虚引用与软引用和弱引用的一个区别在于: 虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。**程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。**程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
特别注意,在程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速 JVM 对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。
4.4 不可达的对象并非“非死不可”
不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑阶段”,要真正宣告一个对象死亡,至少要经历两次标记过程;可达性分析法中不可达的对象被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行 finalize 方法。当对象没有覆盖 finalize 方法,或 finalize 方法已经被虚拟机调用过时,虚拟机将这两种情况视为没有必要执行。被判定为需要执行的对象将会被放在一个队列中进行第二次标记,除非这个对象与引用链上的任何一个对象建立关联,否则就会被真的回收。
5. 如何判断一个常量是废弃常量?类是无用类?
- 运行时常量池主要回收的是废弃的常量。那么,我们如何判断一个常量是废弃常量呢?
假如在字符串常量池中存在字符串 “abc”,如果当前没有任何 String 对象引用该字符串常量的话,就说明常量 “abc” 就是废弃常量,如果这时发生内存回收的话而且有必要的话,“abc” 就会被系统清理出常量池了。
- 判定一个类是否是“无用的类”的条件则相对苛刻许多。类需要同时满足下面 3 个条件才能算是 “无用的类” :
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的
ClassLoader
已经被回收。 - 该类对应的
java.lang.Class
对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。
二、垃圾回收算法
垃圾回收算法主要包括:
- 标记-清除
- 复制算法
- 标记-整理
- 分代收集算法
1.标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象(注意是不回收!!!),在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
- 效率问题
- 空间问题(标记清除后会产生大量不连续的碎片)
2.复制算法
为了解决效率问题,“复制”收集算法出现了。它**将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。**这样就使每次的内存回收都是对内存区间的一半进行回收。
这种收集算法也有缺点:内存总是被分为两块,所以实际用到的内存只有真实内存的一半
3.标记-整理
根据老年代的特点提出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
这种算法的缺陷:除了标记还需要移动,效率降低
相对于标记-清除算法:整理后大大减少了内存碎片
4. 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将 java 堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
HotSpot 为什么要分为新生代和老年代?
比如在新生代中,每次收集都会有大量对象死去,所以可以选择复制算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
三、 垃圾收集器
垃圾收集器是内存回收的具体实现。 直到现在为止还没有最好的垃圾收集器出现,更加没有万能的垃圾收集器,我们能做的就是根据具体应用场景选择适合自己的垃圾收集器
1. Serial 收集器
Serial(串行)收集器收集器是最基本、历史最悠久的垃圾收集器了。它是一个单线程收集器, 它的 “单线程” 的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程( “Stop The World” ),直到它收集结束。
新生代采用复制算法,老年代采用标记-整理算法
与其他单线程收集器相比它的优点就是: 简单而高效 Serial 收集器由于没有线程交互的开销,自然可以获得很高的单线程收集效率。 Serial 收集器对于运行在 Client 模式下的虚拟机来说是个不错的选择。
2. ParNew 收集器
ParNew 收集器其实就是 Serial 收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和 Serial 收集器完全一样。
新生代采用复制算法,老年代采用标记-整理算法。
它是许多运行在 Server 模式下的虚拟机的首要选择,除了 Serial 收集器外,只有它能与 CMS 收集器(真正意义上的并发收集器,后面会介绍到)配合工作。
- 并行(Parallel) :指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。
- 并发(Concurrent):指用户线程与垃圾收集线程同时执行(但不一定是并行,可能会交替执行),用户程序在继续运行,而垃圾收集器运行在另一个 CPU 上。
3. Parallel Scavenge 收集器
Parallel Scavenge 收集器也是使用复制算法的多线程收集器,它看上去几乎和ParNew都一样。 那么它有什么特别之处呢?
Parallel Scavenge 收集器关注点是吞吐量(高效率的利用 CPU)。CMS 等垃圾收集器的关注点更多的是用户线程的停顿时间(提高用户体验)。所谓吞吐量就是 CPU 中用于运行用户代码的时间与 CPU 总消耗时间的比值。 Parallel Scavenge 收集器提供了很多参数供用户找到最合适的停顿时间或最大吞吐量,如果对于收集器运作不太了解,手工优化存在困难的时候,使用Parallel Scavenge收集器配合自适应调节策略,把内存管理优化交给虚拟机去完成也是一个不错的选择。
新生代采用复制算法,老年代采用标记-整理算法。
是JDK1.8默认收集器
使用java -XX:+PrintCommandLineFlags -version命令查看
-XX:InitialHeapSize=262921408 -XX:MaxHeapSize=4206742528 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseParallelGC
java version "1.8.0_211"
Java(TM) SE Runtime Environment (build 1.8.0_211-b12)
Java HotSpot(TM) 64-Bit Server VM (build 25.211-b12, mixed mode)
JDK1.8默认使用的是Parallel Scavenge + Parallel Old,如果指定了-XX:+UseParallelGC参数,则默认指定了-XX:+UseParallelOldGC,可以使用-XX:-UseParallelOldGC来禁用该功能
4. Serial Old 收集器
Serial 收集器的老年代版本,它同样是一个单线程收集器。它主要有两大用途:一种用途是在 JDK1.5 以及以前的版本中与 Parallel Scavenge 收集器搭配使用,另一种用途是作为 CMS 收集器的后备方案。
5. Parallel Old 收集器
Parallel Scavenge 收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及 CPU 资源的场合,都可以优先考虑 Parallel Scavenge 收集器+Parallel Old 收集器。
6. *CMS 收集器
CMS(Concurrent Mark Sweep)收集器 是 HotSpot 虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
是一种以获取最短回收停顿时间为目标的收集器。它非常符合在注重用户体验的应用上使用。
从名字中的Mark Sweep这两个词可以看出,CMS 收集器是一种 “标记-清除”算法实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤:
- 初始标记: 暂停所有的其他线程(还是会暂停一小会),并记录下直接与 root 相连的对象,速度很快 ;
- 并发标记: 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
- 重新标记: 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短
- 并发清除: 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。
两次STW。从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点:
-
对 CPU 资源敏感:在并发阶段虽然不会导致用户线程停顿,但是会因为占用了一部分线程使应用程序变慢
为了解决这种情况,虚拟机提供了一种“增量式并发收集器”
的CMS收集器变种, 就是在并发标记和并发清除的时候让GC线程和用户线程交替运行,尽量减少GC 线程独占资源的时间,这样整个垃圾收集的过程会变长,但是对用户程序的影响会减少。(效果不明显,不推荐)
-
无法处理浮动垃圾;
-
它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。(导致Full GC)
什么是浮动垃圾?
CMS在并发清理阶段,用户线程还在运行, 伴随着程序的运行自然也会产生新的垃圾,这一部分垃圾产生在标记过程之后,CMS无法再当次过程中处理,所以只有等到下次gc时候在清理掉,这一部分垃圾就称作“浮动垃圾”
7.*G1 收集器
G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.
被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备一下特点:
- 并行与并发:G1 能充分利用 CPU、多核环境下的硬件优势,使用多个 CPU(CPU 或者 CPU 核心)来缩短 Stop-The-World 停顿时间。部分其他收集器原本需要停顿 Java 线程执行的 GC 动作,G1 收集器仍然可以通过并发的方式让 java 程序继续执行。
- 分代收集:虽然 G1 可以不需要其他收集器配合就能独立管理整个 GC 堆,但是还是保留了分代的概念。
- 空间整合:与 CMS 的“标记–清理”算法不同,G1 从整体来看是基于“标记整理”算法实现的收集器;从局部上来看是基于“复制”算法实现的。
- 可预测的停顿:这是 G1 相对于 CMS 的另一个大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内。
G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
总结对比 CMS、G1
CMS | G1 | |
---|---|---|
过程 | 初始标记、并发标记、重新标记、并发清除 | 初始标记、并发标记、最终标记、筛选回收 |
回收算法 | 标记-清除 | 标记-整理 |
面向场景 | 回收停顿时间为目标,注重用户体验 | 针对配备多颗处理器及大容量内存的机器 |
停顿时间 | 较短 | 较短,可预测模型,用户可指定 |
收集策略 | 不需要其他收集器配合 | |
并行与并发 | 第一款真正意义上的并发收集器,收集线程与用户线程(基本上)同时工作 | 充分利用 CPU、多核环境,来缩短 Stop-The-World,可以通过并发的方式让 java 程序继续执行。 |