jvm的轻量级爽口讲解--内存管理子系统(俗称垃圾回收)〇贰
前情提要
上回,我们就jvm虚拟机内存的问题一路问下来,整出了整个jvm虚拟的知识点大纲。这次我们就问题树的最小子树,继续往下问,继续往下回答。就上次的某些问题问的有些突然,那些新概念像是不要钱的一样直接涌过来,让人有点懵,这次,笔者会尽可能的把它从哪里来,到哪里去,它的整个来龙去脉给它溜全了。
jvm内存为什么使用可达性分析算法而不是使用引用计数器算法实现内存块的标记
标记之后再回收?
关于整理的书籍,笔者有一本书推荐给大家,那就是 近藤麻理惠的《怦然心动的人生整理魔法》,近藤麻理惠是日本比较出名的整理大师,笔者最喜欢书中大师最核心的思想,也和我们这一章节要讲的东西很有关系,那就是要学会丢弃,一旦我们判定手中的东西,以后不再使用就可以丢弃他们。
作为面向对象的特性,我们甚至可以直接用现实中的整理,类比内存整理,就像那本书的核心思想一样,我们首先要确定内存是否是需要丢弃的垃圾内存,在垃圾回收器这里这个方法我们把它叫做标记算法,嗯对,先标记内存是否可用,再去回收,没毛病。
那么我们目前哪些内存标记算法呢?一共两种一种是引用计数器算法,一种是可达性分析算法。
引用计数器算法&可达性分析算法
引用计数器算法
他的实现很简单,就是给内存中存在的对象添加一个计数器,但它被引用时,它的计数器数值就加一,当它的引用数值变成0,就会被标记为垃圾对象,对其进行回收。
引用计数器算法,在垃圾回收领域应用很广泛,包括 Action Script3的FlashPlayer,Python语言以及游戏脚本领域得到许多应用的Squirrel都是使用引用计数器算法,但是在Java领域中却不是这样,我们主流的Java虚拟机(Hotspot,J9等)都没有选用引用计数器算法来标记内存,
主要原因是,这个看似实现简单的原理,却有很多意外情况需要处理,比如说比较突出的循环引用问题,
举个例子,我们新建两个实例A和B,
A实例里面的一个变量引用B,B实例的一个变量引用A,
然后我们把A,B实例置空,
如果我们的语言使用的引用计数器算法,那么我们的A,B变量将不会被回收,因为实例本身为空了,但是变量的引用还为1。
可达性分析算法
通过一系列称为“GC Roots”的根对象作为起始节点,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路程称为“引用链”。
感觉整个描述比较抽象,不过没关系,一般抽象的概念,在使用过程当中会变得具体,之后笔者会讲分代收集从某些方面是如何应用可达性分析算法,以及如何进行并发状态下的可达性收集,当讲到这一部分的时候,读者可以再会头看看这里。
(为了便于回头查找,笔者先把这行文字标识为粉色)
因为读者表述能力不足,下面这个关于GC Roots一般都包括哪些只能硬背了。
- 在虚拟机栈中(栈帧中的本地变量表)中引用的对象,譬如各个线程被调用方法堆栈中的参数、局部变量、临时变量等。
- 在方法区中类静态属性引用的对象,譬如Java类的引用类型静态变量。
- 在方法区中常量引用的对象,譬如字符串常量池里的引用
- 在本地方法栈帧中JNI(即通常所说的Native方法)引用的对象
- Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻的异常对象(比如NullPonitExeception、OutofMemoryError)等,还有系统类加载器。
- 所有被同步锁(synchronized 关键字)持有的对象
- 反映Java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
为了更形象的表示GC Root的标记过程笔者给出图片
图片如上。我们通过GC Root节点往下查找他引用的节点,然后再继续查找引用的节点引用的节点禁止套娃,就这样将所有关于GC Root的引用链路查找完毕,最后将那些没有在引用链路中的节点标识为垃圾节点,进行回收
Stop the world?并行 and 并发?
既然可达性分析算法已经讲了,那我们顺便聊聊,标记算法的重点特性,我们知道在jvm虚拟机运行过程中的线程分为两大类,分别是 用户线程和垃圾回收线程
两个线程都会对我们上面的引用链路进行操作,那么问题就出来了,
如果垃圾回收线程在引用链路查找过程中,用户突然对查找过的引用进行变更操作,那么势必会使回收的变量产生问题,
所以,当垃圾回收线程在进行标记的过程中,会让用户线程进入短时间的停顿。这个现象被形象的称之为Stop the world。所以说Java程序运行过程中,如果使用内存量大的话,用户会感到有明显的停
顿。
对,就是停顿,这是我们对GC回收优化的一个重要指标,如果你是Web端的服务程序,那么停顿时间这个指标是你的优化首选,毕竟没有哪个用户希望,在他使用服务的过程中经常出现明显的卡顿。
好的,讲到停顿,那么我们可不可以不进行停顿呢?答案是当然的,我们可以让用户线程和垃圾回收线程进行并发执行。
什么?并发?对就是读者所理解的让回收线程和用户线程抢占CPU的时间片,但是这样子还会出现上面那种情况,用户修改标记过后的引用链路,导致回收了错误的对象或者叫变量。这个…由于篇幅有限,这个问题的答案以及原理我们放到下一篇当中。(还有一个优化指标吞吐量和这个有关系)
回收算法&内存分代
既然我们聊完标记了,那么标记之后的回收我们也来聊一聊,应该怎么回收这些垃圾,对垃圾进行分类,哪种垃圾的用哪种算法回收效率最高。
关于标题的三个回收算法想必大家都已经很熟悉了,这里就简单用图提点一下概念。重点讲为什么?
回收-清除算法
回收算法中简单,最基础的算法,其实就是把标记过后的内存直接清除掉,这样的处理的有点是处理方式简单,回收效率高,但是正因为简单,没有考虑之后的内存插入,可能会导致后面,大对象的插入,会让计算机查找很长时间寻找连续的内存,具体怎么回收直接看下面的简图。
标记-复制算法
为了标记清除算法的缺陷,基于标记-清除算法,又出现了标记-复制算法,简单来说,就是预留一半的内存区域,回收之后,将存活的内存对象紧密排列到预留的区域当中,这样当然可以回收过后得到规整的可用内存区域。但是缺点也很明显,我们需要两倍的内存空间来做,能不能减少预留的空间呢?当然可以,要解决这个问题,我们需要先回到GC的分代问题,为什么jvm内存要分年轻代,老年代?相信大家都知道,jvm堆内存主要分为年轻代,老年代,而年轻代,基本都是使用的标记-复制算法,所谓年轻代,就是内存区域中的大部分对象都是“朝生夕死”的。内存赋值之后,很快就会被标记为可回收,那么我们在年轻代进行回收的时候,就会回收到大量的垃圾对象,而余下存活的对象很少,这样我们就可以把标记-复制算法的预留区域设置的少一点,降低一点标记-复制算法所消耗的内存,在最常用的jvm Hotspot虚拟机当中,年轻代默认的主内存(eden区)和预留内存(suvivor1区和suvivor2区)的内存比例就达到了8:1:1的比例,也就是两个预留区总共占用百分之20的内存
标记-整理算法
除了设置预留区域之外,我们还可以在内存回收之后进行内存的整理,这样我们也可以使用比较规整的内存区域,其抽象的过程就如下图,但是,如果我们内存回收之后,各个存活对象之间的空白区域很多,那么整理对于我们的回收线程是特别消耗时间和性能的事情,所以我们要找那种回收过程中要回收垃圾对象比较少的区域,这样回收之后,空白区域预留的比较少,可以消耗比较较少的计算资源进行整理,看到这里,你应该能反应到了吧,对就是老年代,这种回收方式对于老年代的内存是再合适不过了,因为老年代中的对象存活率比较高,产生的垃圾对象相对较少。
好的,此篇博客基本就讲到这里,我把这此所讲述的问题列出来,当然有些问题仅仅是解决了一半而已,比如说,停顿具体是怎样实现的,并发标记是如何做的,还有一个没有列出来的,记忆集是怎么来的等等,这些问题笔者会在下一篇中写出答案,以下是我的问题列表:
- 垃圾 标记算法有哪些,为什么使用可达性分析算法,而不是引用计数器算法?
- jvm回收线程在标记过程中是否会造成停顿,为什么?
- 垃圾回收算法有哪些,分别什么作用,他和内存分代有什么关系?