今天来讲说 Java 垃圾回收,高频面试问题。
java
提纲附上,话很少说,直接干货web

一、什么是垃圾回收?
垃圾回收(Garbage Collection,GC):就是释放垃圾占用的空间,防止内存泄露。对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。面试
二、垃圾在哪儿?

上图能够看到程序计数器、虚拟机栈、本地方法栈都是伴随着线程而生死,这些区域不须要进行 GC。算法
而方法区/元空间在 1.8 以后就直接放到本地内存了,假设总内存 2G,JVM 被分配内存 100M, 理论上元空间能够分配 2G-100M = 1.9G,空间仍是足够的,因此这块区域也不用管。数组
因此就只剩下堆了,java 对象实例和数组都是在堆上分配的,因此垃圾回收器重点照顾堆。安全
三、怎么发现它?
在发生 GC 的时候,Jvm 是怎么判断堆中的对象实例是否是垃圾呢?微信
这里有两种方式:多线程
一、引用计数法
就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加 1,每当有一个引用失效时,计数器的值就减 1。任什么时候刻只要对象的计数器值为 0,那么就能够被断定为垃圾对象。并发
这种方式,效率挺高,可是 Jvm 并无使用引用计数算法。那是由于在某种场合下存在问题jvm
好比下面的代码,会出现循环引用的问题:
public class Test {
Test test;
public Test(String name) {}
public static void main(String[] args) {
Test a = new Test("A");
Test b = new Test("B");
a.test = b;
b.test = a;
a = null;
b = null;
}
}
即便你把 a 和 b 的引用都置为 null 了,计数器也不是 0,而是 1,由于它们指向的对象又互相指向了对方,因此没法回收这两个对象。
二、可达性分析法
这才是 jvm 默认使用的寻找垃圾算法
。
它的原理是经过一些列称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜素所走过的路叫作称为引用链“Reference Chain”,当一个对象到 GC Roots 没有任何引用链时,就说这个对象是不可达的。

从上图能够看到,即便 Object5 和 Object6 之间相互引用,可是没有 GC Roots 和它们关联,因此能够解决循环引用的问题。
小知识点:
一、哪些能够做为 GC ROOTS 根呢?
-
虚拟机栈(栈帧中的本地变量表)中引用的对象 -
方法区中类静态属性引用的对象 -
方法区中常量引用的对象 -
本地方法栈中 JNI(即通常说的 Native 方法)引用的对象
二、不得不说的四种引用
-
强引用:就是在程序中广泛存在的,相似“Object a=new Object”这类的引用。 只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。 -
软引用:用来描述一些还有用可是并不是必须的对象。 直到内存空间不够时(抛出 OutOfMemoryError 以前),才会被垃圾回收,经过 SoftReference 来实现。 -
弱引用:比软引用还弱,也是用来描述非必须的对象的, 当垃圾回收器开始工做时,不管内存是否足够用,弱引用的关联的对象都会被回收 WeakReference。 -
虚引用:它是最弱的一种引用关系,它的惟一做用是用来做为一种通知。 采用 PhantomRenference 实现。
三、为何定义这些引用?
我的理解,其实就是给对象加一种中间态,让一个对象不仅有引用和非引用
两种状况,还能够描述一些“食之无味弃之惋惜”的对象。好比说:当内存空间足时,则能保存在内存中,若是内存空间在进行垃圾回收以后还不够时,才对这些对象进行回收
。
四、生存仍是死亡?
要真正宣告一个对象死亡,至少要经历两次标记过程和一次筛选。
一张图带你看明白:

五、垃圾收集算法
一、标记清除算法
分为两个阶段“标记”和“清除”,标记出全部要回收的对象,而后统一进行清除。

缺点:
-
在对象变多的状况下,标记和清除效率都不高 -
会产生空间碎片
二、复制算法
就是将堆分红两块彻底相同的区域,对象只在其中一块区域内分配,而后标记出那些是存活的对象,按顺序总体移到另一个空间,而后回收掉以前那个区域的全部对象。

缺点:
-
虽然可以解决空间碎片的问题,可是空间少了一半。也太多了吧!!
三、标记整理算法

这种算法是,先找到存活的对象,而后将它们向空间的一端移动,最后回收掉边界之外的垃圾对象。
四、分代收集
其实就是整合了上面三种算法,扬长避短。
之因此叫分代,是由于根据对象存活周期的不一样将整个 Java 堆切割成为三个部分:
-
Young(年轻代) -
Eden(伊利园):新生对象 -
Survivor(幸存者):垃圾回收后还活着的对象 -
Tenured(老年代):对象屡次回收都没有被清理,会移到老年代 -
Perm(永久代):存放加载的类别还有方法对象,java8 以后移除了永久代,替换为元空间(Metaspace)
在新生代中,每次垃圾收集都有大量的对象死去,只有少许的存活,那就选用 复制算法 ,由于复制成本很小,只须要复制少许存活对象。
老年代中,存活对象较多,没有额外的空间担保,就得使用 标记清除 或者 标记整理 。
六、垃圾收集器
在说垃圾回收器以前须要了解几个概念:
一、几个概念
吞吐量
CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
好比说虚拟机总运行了 100 分钟,用户代码时间 99 分钟,垃圾回收时间 1 分钟,那么吞吐量就是 99%。
STW
全称 Stop-The-World,即在 GC 期间,只有垃圾回收器线程在工做,其余工做线程则被挂起。
为何须要 STW 呢?
在 java 程序中引用关系是不断会变化的,那么就会有不少种状况来致使垃圾标识出错。
想一想一下若是一个对象 A 当前是个垃圾,GC 把它标记为垃圾,可是在清除前又有其余引用指向了 A,那么此刻又不是垃圾了。
那么,若是没有 STW 的话,就要去无限维护这种关系来去采集正确的信息,显然是不可取的。
安全点
从线程角度看,安全点能够理解成是在代码执行过程当中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的。
好比:方法调用、循环跳转、异常跳转等这些地方才会产生安全点。
若是有须要,能够在这个位置暂停,好比发生 GC 时,须要暂停全部活动线程,可是线程在这个时刻,尚未执行到一个安全点,因此该线程应该继续执行,到达下一个安全点的时候暂停,等待 GC 结束。
串行、并行
串行:是指垃圾回收线程在进行垃圾回收工做,此时用户线程处于等待状态。
并行:是指用户线程和多条垃圾回收线程分别在不一样 CPU 上同时工做。
二、回收器
下面是一张很经典的图,展现了 7 种不一样分代的收集器,若是两个收集器之间存在连线,说明能够搭配使用。

Serial
Serial 收集器是一个单线程收集器,在进行垃圾回收器的时候,必须暂停其余工做线程,也就是发生 STW。在 GC 期间,应用是不可用的。

特色:一、采用复制算法 二、单线程收集器 三、效率会比较慢,可是由于是单线程,因此消耗内存小
ParNew
ParNew 是 Serial 的多线程版本,也是工做在新生代,能与 CMS 配合使用。
在多 CPU 的状况下,因为 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减小 STW 的时间,提高应用的响应速度。

特色:一、采用复制算法 二、多线程收集器 三、效率高,能大大减小 STW 时间。
Parallel Scavenge
Parallel Scavenge 收集器也是一个使用复制算法,多线程,工做于新生代的垃圾收集器,看起来功能和 ParNew 收集器基本同样。

可是它有啥特别之处呢?关注点不一样
-
ParNew 垃圾收集器关注的是尽量缩短垃圾收集时用户线程的停顿时间,更适合用到与用户交互的程序,由于停顿时间越短,用户体验确定就好呀!! -
Parallel Scavenge 目标是达到一个可控制的吞吐量,因此更适合作后台运算等不须要太多用户交互的任务。
Parallel Scavenge 收集器提供了两个参数来控制吞吐量,
-
-XX:MaxGCPauseMillis:控制最大垃圾收集时间 -
-XX:GCTimeRati:直接设置吞吐量大小
特色:一、采用复制算法 二、多线程收集器 三、吞吐量优先
Serial Old
Serial 收集器是工做于新生代的单线程收集器,与之相对地,Serial Old 是工做于老年代的单线程收集器。
做用:
-
在 Client 模式下与 Serial 回收器配合使用 -
Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及以前的版本中与 Parallel Scavenge 配合使用,另外一种是做为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
它与 Serial 收集器配合使用示意图以下:

特色:一、标记-整理算法 二、单线程 三、老年代工做
Parallel Old
Parallel Old 是一个多线程的垃圾回收器,采用标记整理算法,负责老年代的垃圾回收工做,能够与 Parallel Scavenge 垃圾回收器一块儿搭配工做。真正的实现吞吐量优先
示意图以下:

特色:一、标记-整理算法 二、多线程 三、老年代工做
CMS
CMS 能够说是一款具备"跨时代"意义的垃圾回收器,若是应用很重视服务的响应速度,但愿给用户最好的体验,则 CMS 收集器是很是合适的,它是以获取最短回收停顿时间为目标的收集器!
CMS 虽然工做在老年代,和以前收集器不一样的是,使用的标记清除算法
示意图以下:

垃圾回收的 4 个步骤:
-
初始标记:标记出来和 GC Roots 直接关联的对象,整个速度是很是快的,会发生 STW,确保标记的准确性。 -
并发标记:并发标记这个阶段会直接根据第一步关联的对象找到全部的引用关系,耗时较长,可是这个阶段会与用户线程并发运行,不会有很大的影响。 -
从新标记:这个阶段是为了解决第二步并发标记所致使的标错状况。并发阶段会和用户线程并行,有可能会出现判断错误的状况,这个阶段就是对上一个阶段的修正。 -
并发清除:最后一个阶段,将以前确认为垃圾的对象进行回收,会和用户线程一块儿并发执行。
缺点:
-
影响用户线程的执行效率:CMS 默认启动的回收线程数是(处理器核心数 + 3)/ 4 ,因为是和用户线程一块儿并发清理,那么势必会影响到用户线程的执行速度 -
会产生浮动垃圾:CMS 的第 4 个阶段 并发清除是和用户线程一块儿的,会产生新的垃圾,就叫浮动垃圾 -
会产生碎片化的空间:标记清除的缺点
G1
全称:Garbage-First
G1 回收的目标再也不是整个新生代或者是老年代。G1 能够回收堆内存的任何空间来进行,再也不是根据年代来区分,而是那块空间垃圾多就去回收,经过 Mixed GC 的方式去进行回收。
先看下堆空间的划分:

G1 垃圾回收器把堆划分红大小相同的 Region,每一个 Region 都会扮演一个角色,分别为 H、S、E、O。
-
E 表明伊甸区 -
S 表明 Survivor 区 -
H 表明的是 Humongous 区 -
O 表明 Old 区
G1 的工做流程图:

-
初始标记:标记出来 GC Roots 能直接关联到的对象,修改 TAMS 的值以便于并发回收时新对象分配 -
并发标记:根据刚刚关联的对像扫描整个对象引用图,和用户线程并发执行,记录 SATB(原始快照) 在并发时有引用的值 -
最终标记:处理第二步遗留下来的少许 SATB(原始快照) 记录,会发生 STW -
筛选回收:维护以前提到的优先级列表,根据优先级列表、用户设置的最大暂停时间来回收 Region
特色:
-
并行与并发:G1 能充分利用多 CPU、多核环境下的硬件优点,能够经过并发的方式让 Java 程序继续执行,进一步缩短 STW 的时间。 -
分代收集:分代概念在 G1 中依然得以保留,它可以采用不一样的方式去处理新建立的对象和已经存活了一段时间、熬过屡次 GC 的旧对象来得到更好的收集效果。 -
空间整合:G1 从总体上看是基于标记-整理算法实现的,从局部(两个 Region 之间)上看是基于复制算法实现的,G1 运行期间不会产生内存空间碎片。 -
可预测停顿:G1 比 CMS 厉害在能创建可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片断内,消耗在垃圾收集上的时间不得超过 N 毫秒。
七、内存分配与回收策略
上文说的一直都是回收内存的内容,那么怎么给对象分配内存呢?
堆空间的结构:

Eden 区
研究代表,有将近 98%的对象是朝生夕死,因此针对这一现状,大多数状况下,对象会在新生代 Eden 区中进行分配。
当 Eden 区没有足够空间进行分配时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
经过 Minor GC 以后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
Survivor 区
Survivor 区至关因而 Eden 区和 Old 区的一个缓冲,Survivor 又分为 2 个区,一个是 From 区,一个是 To 区。每次执行 Minor GC,会将 Eden 区和 From 存活的对象放到 Survivor 的 To 区(若是 To 区不够,则直接进入 Old 区)。
问题 1:为何须要 Survivor?
若是没有 Survivor 区,Eden 区每进行一次 Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满。而有不少对象虽然一次 Minor GC 没有消灭,但其实或许第二次,第三次就须要被清除。
这时候移入老年区,很明显不是一个明智的决定。
因此,Survivor 的存在乎义就是减小被送到老年代的对象,进而减小老年代 GC 的发生。Survivor 的预筛选保证,只有经历 15 次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
问题 2:为何须要 From 和 To 两个呢?
这种机制最大的好处就是能够解决内存碎片化,整个过程当中,永远有一个 Survivor 区是空的,另外一个非空的 Survivor 区是无碎片的。
假设只有一个 Survivor 区。
Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而以前 Survivor 区中的对象,可能也有一些是须要被清除的。
那么问题来了,这时候咱们怎么清除它们?
在这种场景下,咱们只能标记清除,而咱们知道标记清除最大的问题就是内存碎片,在新生代这种常常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。
由于 Survivor 有 2 个区域,因此每次 Minor GC,会将以前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,To 区 到 From 区 ,以此反复。
Old 区
老年代占据着 2/3 的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,因此内存也不只仅是越大就越好。
因为复制算法在对象存活率较高的老年代会进行不少次的复制操做,效率很低,因此在这里老年代采用的是标记整理算法。
下面三种状况也会直接进入老年代:
大对象
大对象指须要大量连续内存空间的对象,这部分对象无论是否是“朝生夕死”,都会直接进到老年代。这样作主要是为了不在 Eden 区及 2 个 Survivor 区之间发生大量的内存复制。当你的系统有很是多“朝生夕死”的大对象时,须要注意。
长期存活对象
虚拟机给每一个对象定义了一个对象年龄 Age 计数器。正常状况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增长 1 岁。当年龄增长到 15 岁时,这时候就会被转移到老年代。
动态对象年龄
虚拟机并不重视要求对象年龄必须到 15 岁,才会放入老年区,若是 Survivor 空间中相同年龄全部对象大小的总合大于 Survivor 空间的一半,年龄大于等于该年龄的对象就能够直接进去老年区。
空间分配担保
在发生 Minor GC 以前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象总空间。
若是条件成立的话,Minor GC 是能够确保安全的。
若是不成立,则虚拟机会查看 HandlePromotionFailure 设置是否担保失败,若是容许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。
若是大于,尝试进行一次 Minor GC。
若是小于或者 HandlePromotionFailure 不容许,则进行一次 Full GC。
End
看累了吧,学到了吧,那就关注一下呗!
学到就是赚到,欢迎在看、点赞、转发,您的承认是我原创的动力!
本文分享自微信公众号 - 程序IT圈(DeveloperIT)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。