我的新课
《C2C 电商系统微服务架构120天实战训练营》
在公众号
儒猿技术窝
上线了,感兴趣的同学,可以长按扫描下方二维码了解课程详情:
课程大纲请参见文末
大家好,我是狂聊,上一篇已经把 Jvm 的运行区数据和类加载机制聊完了。
今天来说说 Java 垃圾回收,高频面试问题。
提纲附上,话不多说,直接干货
1、什么是垃圾回收?
垃圾回收(Garbage Collection,GC):就是释放垃圾占用的空间,防止内存泄露。对内存堆中已经死亡的或者长时间没有使用的对象进行清除和回收。
2、垃圾在哪儿?
上图可以看到程序计数器、虚拟机栈、本地方法栈都是伴随着线程而生死,这些区域不需要进行 GC。
而方法区/元空间在 1.8 之后就直接放到本地内存了,假设总内存 2G,JVM 被分配内存 100M, 理论上元空间可以分配 2G-100M = 1.9G,空间还是足够的,所以这块区域也不用管。
所以就只剩下
堆
了,java 对象实例和数组都是在
堆
上分配的,所以垃圾回收器重点照顾
堆
。
3、怎么发现它?
在发生 GC 的时候,Jvm 是怎么判断堆中的对象实例是不是垃圾呢?
这里有两种方式:
1、引用计数法
就是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器的值就加 1,每当有一个引用失效时,计数器的值就减 1。任何时刻只要对象的计数器值为 0,那么就可以被判定为垃圾对象。
这种方式,效率挺高,但是 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,因为它们指向的对象又互相指向了对方,所以无法回收这两个对象。
2、可达性分析法
这才是 jvm 默认使用的
寻找垃圾算法
。
它的原理是通过一些列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜素所走过的路叫做称为引用链“Reference Chain”,当一个对象到 GC Roots 没有任何引用链时,就说这个对象是不可达的。
从上图可以看到,即使 Object5 和 Object6 之间相互引用,但是没有 GC Roots 和它们关联,所以可以
解决循环引用的问题
。
小知识点:
1、哪些可以作为 GC ROOTS 根呢?
-
-
-
-
本地方法栈中 JNI(即一般说的 Native 方法)引用的对象
2、不得不说的四种引用
-
强引用:就是在程序中普遍存在的,类似“Object a=new Object”这类的引用。
只要强引用关系还存在,垃圾回收器就不会回收掉被引用的对象。
-
软引用:用来描述一些还有用但是并非必须的对象。
直到内存空间不够时(抛出 OutOfMemoryError 之前),才会被垃圾回收,通过 SoftReference 来实现。
-
弱引用:比软引用还弱,也是用来描述非必须的对象的,
当垃圾回收器开始工作时,无论内存是否足够用,弱引用的关联的对象都会被回收 WeakReference。
-
虚引用:它是最弱的一种引用关系,它的唯一作用是用来作为一种通知。
采用 PhantomRenference 实现
。
3、为什么定义这些引用?
个人理解,其实就是给对象加一种中间态,让一个对象不只有
引用和非引用
两种情况,还可以描述一些“食之无味弃之可惜”的对象。比如说:当内存空间足时,则能保存在内存中,如果内存空间在进行垃圾回收之后还不够时,
才对这些对象进行回收
。
4、生存还是死亡?
要真正宣告一个对象死亡,至少要经历两次标记过程和一次筛选。
一张图带你看明白:
5、垃圾收集算法
1、标记清除算法
分为两个阶段“标记”和“清除”,标记出所有要回收的对象,然后统一进行清除。
缺点:
-
-
2、复制算法
就是将堆分成两块完全相同的区域,对象只在其中一块区域内分配,然后标记出那些是存活的对象,按顺序整体移到另外一个空间,然后回收掉之前那个区域的所有对象。
缺点:
-
虽然能够解决空间碎片的问题,但是空间少了一半。也太多了吧!!
3、标记整理算法
这种算法是,先找到存活的对象,然后将它们向空间的一端移动,最后回收掉边界以外的垃圾对象。
4、分代收集
其实就是整合了上面三种算法,扬长避短。
之所以叫分代,是因为根据
对象存活周期的不同
将整个 Java 堆切割成为三个部分:
-
-
-
Survivor(幸存者):垃圾回收后还活着的对象
-
Tenured(老年代):对象多次回收都没有被清理,会移到老年代
-
Perm(永久代):存放加载的类别还有方法对象,java8 之后移除了永久代,替换为元空间(Metaspace)
在新生代中,每次垃圾收集都有大量的对象死去,只有少量的存活,那就选用
复制算法
,因为复制成本很小,只需要复制少量存活对象。
老年代中,存活对象较多,没有额外的空间担保,就得使用
标记清除
或者
标记整理
。
6、垃圾收集器
在说垃圾回收器之前需要了解几个概念:
1、几个概念
吞吐量
CPU 用于运行用户代码的时间与 CPU 总消耗时间的比值。
比如说虚拟机总运行了 100 分钟,用户代码时间 99 分钟,垃圾回收时间 1 分钟,那么吞吐量就是 99%。
STW
全称 Stop-The-World,即在 GC 期间,只有垃圾回收器线程在工作,其他工作线程则被挂起。
为什么需要 STW 呢?
在 java 程序中引用关系是不断会变化的,那么就会有很多种情况来导致垃圾标识出错。
想想一下如果一个对象 A 当前是个垃圾,GC 把它标记为垃圾,但是在清除前又有其他引用指向了 A,那么此刻又不是垃圾了。
那么,如果没有 STW 的话,就要去无限维护这种关系来去采集正确的信息,显然是不可取的。
安全点
从线程角度看,安全点可以理解成是在代码执行过程中的一些
特殊位置
,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的。
比如:
方法调用
、
循环跳转
、
异常跳转
等这些地方才会产生安全点。
如果有需要,可以在这个位置暂停,比如发生 GC 时,需要暂停所有活动线程,但是线程在这个时刻,还没有执行到一个安全点,所以该线程应该继续执行,到达下一个安全点的时候暂停,等待 GC 结束。
串行、并行
串行:是指垃圾回收线程在进行垃圾回收工作,此时用户线程处于等待状态。
并行:是指用户线程和多条垃圾回收线程分别在不同 CPU 上同时工作。
2、回收器
下面是一张很经典的图,展示了 7 种不同分代的收集器,如果两个收集器之间存在连线,说明可以搭配使用。
Serial
Serial 收集器是一个单线程收集器,在进行垃圾回收器的时候,必须暂停其他工作线程,也就是发生 STW。在 GC 期间,应用是不可用的。
特点:1、采用复制算法 2、单线程收集器 3、效率会比较慢,但是因为是单线程,所以消耗内存小
ParNew
ParNew 是 Serial 的多线程版本,也是工作在新生代,能与 CMS 配合使用。
在多 CPU 的情况下,由于 ParNew 的多线程回收特性,毫无疑问垃圾收集会更快,也能有效地减少 STW 的时间,提升应用的响应速度。
特点:1、采用复制算法 2、多线程收集器 3、效率高,能大大减少 STW 时间。
Parallel Scavenge
Parallel Scavenge 收集器也是一个使用复制算法,多线程,工作于新生代的垃圾收集器,看起来功能和 ParNew 收集器基本一样。
但是它有啥特别之处呢?
关注点不同
-
ParNew 垃圾收集器关注的是尽可能缩短垃圾收集时用户线程的停顿时间,更适合用到与用户交互的程序,因为停顿时间越短,用户体验肯定就好呀!!
-
Parallel Scavenge 目标是达到一个可控制的吞吐量,所以更适合做后台运算等不需要太多用户交互的任务。
Parallel Scavenge 收集器提供了两个参数来控制吞吐量,
-
-XX:MaxGCPauseMillis:控制最大垃圾收集时间
-
特点:1、采用复制算法 2、多线程收集器 3、吞吐量优先
Serial Old
Serial 收集器是工作于新生代的单线程收集器,与之相对地,Serial Old 是工作于老年代的单线程收集器。
作用:
-
在 Client 模式下与 Serial 回收器配合使用
-
Server 模式下,则它还有两大用途:一种是在 JDK 1.5 及之前的版本中与 Parallel Scavenge 配合使用,另一种是作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用
它与 Serial 收集器配合使用示意图如下:
特点:1、标记-整理算法 2、单线程 3、老年代工作
Parallel Old
Parallel Old 是一个多线程的垃圾回收器,采用标记整理算法,负责老年代的垃圾回收工作,可以与 Parallel Scavenge 垃圾回收器一起搭配工作。真正的实现
吞吐量优先
示意图如下:
特点:1、标记-整理算法 2、多线程 3、老年代工作
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。
-
-
-
-
G1 的工作流程图:
-
初始标记:标记出来 GC Roots 能直接关联到的对象,修改 TAMS 的值以便于并发回收时新对象分配