jvm的轻量级爽口讲解--内存管理子系统(俗称垃圾回收)〇叁
前情提要
jvm的轻量级爽口讲解–内存管理子系统(俗称垃圾回收)〇贰
其中对象的引用链路描述有误,现已经更改(应该是查找根节点引用的对象,而不是查找引用根节点的对象)
前言
1 | hey!guys,I'm back,关于之前两篇的blog,博主尽可能进行debug,毕竟一篇好的blog是需要经过不断修改打磨的, |
再聊并发可达性分析
上一篇关于并发可达性问题,我们只是简单的一笔带过,这次我们利用对象图,帮我们把问题描述的更加清晰一些,以便于更加清晰的理解和解决。首先我们先对对象在对象图中的颜色进行区分定义:
- 白色:表示对象还没有被垃圾回收器访问过,也没有被标记过。
- 黑色:表示对象已经被访问且标记为安全存活对象(即不是垃圾对象),不会再进行扫描。
- 灰色:表示对象已经被访问,但是此对象所引用的其他对象至少有一个还没有被扫描过。只有灰色对象所引用的白色对象扫描完毕,其才能被标记为黑色对象,所以说在标记过程中, 黑色对象不能直接指向白色对象。
好的,我们看一下标记过程的抽象图:
初始过程,黑色的方框对象为根节点,开始向下查找。
中间标记过程,就像是波纹~~(dio:纳尼?)~~一样,以灰色对象为波峰持续推进
最后直到所有的灰色对象都查找不到引用,将最后这批灰色对象标记为黑色。
好了,正常的标记过程就是如此,但如果有用户线程来捣乱,那就不一样了
那用户线程如何捣乱呢?把正在查找的灰色对象对白色对象切断?我们先试一下,以下虚线为切断的引用
好像结果并没有什么阻碍,最终查找的结果是正确的,因为用户已经切断了引用,而我们的标记结果也实时作出了改变。
那么添加黑色对象对已经查找的白色对象的引用呢?
也同样的不会出现什么问题,最后的结果和原来一致,那么,如果两个同时叠加呢?
唉?问题就出来了,如果我们切断灰色对象对白色对象的引用,然后用一个黑色对象引用此白色对象,会让这个白色对象到扫描的最后都不会标记为黑色,但他有黑色对象的引用,照这样的标记回收会使我们的回收掉本应该存活的对象。
现在,我们知道,要是并发收集出现错误,必须满足以下两个条件:
- 切断从灰色对象到黑色对象的引用
- 添加已经扫描过的黑色对象对白色对象的引用
只要我们破坏其中一个条件,并发标记便可以实现。
破坏第一个条件,当探测到切断灰色对象对白色对象的引用,把这个引用记录下来,然后再以记录过的灰色对象为根节点,再扫描一遍,这种方法叫做原始快照
破坏第二个条件,当添加黑色对象到白色对象的引用时,我们将这个黑色对象重置为灰色对象,再进行查找。这种方法叫做增量更新
CMS收集器是基于增量更新来做并发标记的,G1、shennandoah则是用原始快照来实现的,这些收集器,会在之后进行专门的一一讲解。
再聊谁实现
好了讲了这么多的理论,我们该讲讲实现了,其实还有一些理论还没有涉及,笔者会在下面以及后几篇补上,现在感觉理论比较枯燥,我们还是聊聊实际的吧。
接下来我们会聊到一些垃圾收集器,以及垃圾收集器所用到的之前的理论部分。
首先,我们先为垃圾收集器进行分类,Serial、ParNew,Parallel Scavenge,Serial Old,Parallel Old,CMS收集器,笔者称他们为经典的收集器,因为这些收集器都是专门管理新生代,或者老年代的。
而之后出现的G1,shennandoah,ZGC收集器,都是新生代,老年代并用的。
而 jdk11出现的Epsilon收集器比较特殊,它不做任何回收动作~~(我要你有何用?)~~,至于其作用笔者会在之后描述。
目前我们只聊了聊经典收集器的理论,所以我们从经典的收集器开始聊起,
- Serial 收集器(直译是电视连续剧?重在连续这个词)最早出现的垃圾回收器,它适用于单核的CPU,因此特别适合运行客户端方面的应用,虽然在之后的回收器层出不穷,但是在客户端应用方面,Serial收集器一直最低内存消耗发挥着作用。
* ParNew 收集器,专管新生代的多线程收集器JDK1.3 - Parallel Scavenge ParNew 收集器的进阶版一般Par开头的,都是多线程收集器,这款收集器主打可控的吞吐量,吞吐量?好像是一个新概念,没错,之前我们讲过,停顿时间是衡量垃圾回收性能的重要指标之一,另一个指标便是吞吐量,他是指用户运行时间在整个程序总运行时间(用户运行时间和垃圾回收时间)的占比。他有自己的参数JDK1.4
- Serial Old 收集器 Serial收集器的老年代版本
- Parallel Old 收集器专门管理老年代的收集器,在此收集器出现之前ParNew 收集器一直初一比较尴尬的境地,因为作为多线程的新生代收集器,他却只能与单线程的Serial 配合使用JDK6
- CMS 划时代意义的收集器,我们之前所说的并发回收就是从这个收集器开始的,之前的所有收集器,都只能通过停顿其他用户线程进行标记和回收,但是他是老年代的收集器,其次,作为老年代的收集器,它却无法配合Parallel Scavenge使用。
以上就是对我们所讲的理论支撑的垃圾回收器的简单介绍,那我们怎么使用他们呢?
很简单,只要启动java的时候加入配置参数就好
1 | java -XX:+回收器参数 运行的Main类 |
那么下面给出的回收器参数
参数 | 描述 |
---|---|
UseSerialGC | 使用Serial + Serial Old 收集器 |
UseParNewGC | 使用ParNew + Serial Old 收集器 |
UseConcMarkSweepGC | 使用ParNew + CMS+Serial Old 的收集器组合进行回收 ,如果CMS收集器并发收集失败,会切换到Serial Old 收集器 |
UseParallelGC | 使用Parallel Scavenge + Serial Old 收集器 |
UseParallelOldGC | 使用 Parallel Scavenge + Parallel Old 收集器 |
这是我们目前可以通过参数切换到的收集器参数,我们会在下一篇blog,将参数写的更详尽一些,包括一些收集器的配置参数,和JDK9之后的从参数改变。
再聊一下停顿
我们前面聊了聊并发标记是如何实现的,现在我们聊一下如何让用户线程停顿,为什么要讲这个?因为可能会出现停顿时间异常的现象,如果我们不懂其中的原理将没有办法定位问题所在。
首先我们先退一步,上一篇博客,我们讲到GC ROOT 其中有很多类型的引用都会被作为GC ROOT,那么随着应用的体量的增大,引用数量必定猛增不减,这种时候我们如果在垃圾回收的时候,再去查询GC ROOT的引用,
那必然是不行的,因此,我们需要先整一个数据结构,在类型加载的时候,就把相关的引用写入数据结构中,这样我们不必在海量的引用中查找,只取数据结构里面的数据便可,JVM最经典的HotSpot虚拟机就是利用OopMap数据结构这样进行工作的。
好了,GC Root的收集问题得到解决了,但是运行过程当中,引用关系肯定会改变,而JVM指令当中,大部分的指令都会造成引用关系的改变,我们不可能在每条指令后面都加一个OopMap结构,那对回收来说,空间成本将会变得额外的高昂。
因此,存储了OopMap数据结构外,在运行时,JVM会选择某些“特殊的位置”来记录引用信息,这些特殊的位置就叫做安全点,那么安全点应该如何设置呢?
安全点设置既不能太少而使回收程序等待时间过长,又不能太多增大程序运行负荷,因此安全点一般选在可长时间执行的地方,一般在方法调用、循环跳转、异常跳转等这些可以指令序列复用的地方。
简单总结一下问题
- 如何做到并发可达性分析,方法分为哪几种?
- 前期新生代,老年代分开管理的回收器都有那些?
- 安全点是什么?一般在哪里设置