jvm的垃圾回收机制:gc,是java提供的对于内存自动回收的机制。
在 java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此将内存回收,也可以叫做死亡对象的回收。gc回收的是“堆上的内存”。
一、死亡对象的判断算法
1.1 引用计数算法
思想:
给对象增加一个引用计数器,每当有一个地方引用它时,计数器就+1;当引用失效时,计数器就-1; 任何时刻计数器为0的对象就是不能再被使用的,即对象已"死"。
引用计数法实现简单,判定效率也比较高,在大部分情况下都是一个不错的算法。比如python语言就采用引用计数法进行内存管理。
在主流的jvm中没有选用引用计数法来管理内存,最主要的原因就是引用计数法无法解决对象的循环引用问题。
1.2 可达性分析算法
思想:
通过一系列称为"gc roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称之为"引用链",当一个对象到 gc roots 没有任何的引用链相连时 (从gc roots到这个对象不可达)时,证明此对象是不可用的。如下:
对象 object5 - object7 之间虽然彼此还有关联,但是它们到 gc roots 是不可达的,因此他们会被判定为可回收对象。
在java语言中,可作为 gc roots 的对象包含下面几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 jni(native方法)引用的对象。
在 jdk1.2 时,java 对引用的概念做了扩充,分为以下四种,这四种引用的强度依次递减:
- 强引用 : 强引用指的是在程序代码之中普遍存在的,类似于"object obj = new object()"这类的引用,只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象实例。
- 软引用 : 软引用是用来描述一些还有用但是不是必须的对象。对于软引用关联着的对象,在系统将要发生内存溢出之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在jdk1.2之后,提供了softreference类来实现软引用。
- 弱引用 : 弱引用也是用来描述非必需对象的。但是它的强度要弱于软引用。被弱引用关联的对象只能生存到下一次垃圾回收发生之前。当垃圾回收器开始进行工作时,无论当前内容是否够用,都会回收掉只被弱引用关联的对象。在jdk1.2之后提供了weakreference类来实现弱引用。
- 虚引用 : 虚引用也被称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。在jdk1.2之后,提供了phantomreference类来实现虚引用。
二、垃圾回收算法
2.1 标记-清除算法
"标记-清除"算法是最基础的收集算法。算法分为"标记"和"清除"两个阶段 : 首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。后续的收集算法都是基于这种思路并对其不足加以改进而已。
- 效率问题 : 标记和清除这两个过程的效率都不高。
- 空间问题 : 标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行中需要分配较大对象时,无法找到足够连续内存而不得不提前触发另一次垃圾收集。
2.2 复制算法
复制"算法是为了解决"标记-清理"的效率问题。
它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另一块上面,然后再把已经使用过的内存区域一次清理掉。这样做的好处是每次都是对整个半区进行内存回收,内存分配时也就不需要考虑内存碎片等复杂情况,只需要移动堆顶指针,按顺序分配即可。
此算法实现简单,运行高效。算法的执行流程如下图 :
2.3 标记-整理算法
复制收集算法在对象存活率较高时会进行比较多的复制操作,效率会变低。因此在老年代一般不能使用复制算法。 针对老年代的特点,提出了一种称之为"标记-整理算法"。标记过程仍与"标记-清除"过程⼀致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉端边界以外的内存。
流程图如下:
2.4 分代算法
分代算法和上面 3 种算法不同,分代算法是通过区域划分,实现不同区域和不同的垃圾回收策略,从而实现更好的垃圾回收。
当前 jvm 垃圾收集都采用的是"分代收集(generational collection)"算法,这个算法并没有新思想,只是根据对象存活周期的不同将内存划分为几块。
一般是把java堆分为新生代和老年代。在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此我们采用复制算法;而老年代中对象存活率高、没有额外空间对它进行分配担保,就必须采用"标记-清理"或者"标记-整理"算法。
哪些对象会进入新生代?哪些对象会进入老年代?
- 新生代:一般创建的对象都会进入新生代;
- 老年代:大对象和经历了 n 次(一般情况默认是 15 次)垃圾回收依然存活下来的对象会从新生代移动到老年代。
面试题 : 请问了解minor gc和full gc么,这两种gc有什么不一样吗?
- minor gc 又称为新生代gc : 指的是发生在新生代的垃圾收集。因为java对象大多都具备朝生夕灭的特性,因此minor gc(采用复制算法)非常频繁,一般回收速度也比较快。
- full gc 又称为老年代gc 或者 major gc : 指发生在老年代的垃圾收集。出现了major gc,经常会伴随至少一次的minor gc (并非绝对,在parallel scavenge收集器中就有直接进行full gc的策略选择过程)。major gc的速度一般会比minor gc慢10倍以上。
三、垃圾收集器
收集算法是内存回收的方法论,垃圾收集器是内存回收的具体实现。
垃圾收集器的作用:垃圾收集器是为了保证程序能够正常、持久运行的一种技术,它是将程序中不用的死亡对象也就是垃圾对象进行清除,从而保证了新对象能够正常申请到内存空间。
以下这些收集器是 hotspot 虚拟机随着不同版本推出的重要的垃圾收集器:
上图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们之间可以搭配使用。所处的区域,表示它是属于新生代收集器还是老年代收集器。
- 并行(parallel) : 指多条垃圾收集线程并行工作,用户线程仍处于等待状态。
- 并发(concurrent) : 指用户线程与垃圾收集线程同时执行 (不一定并行,可能会交替执行),用户程序继续运行,而垃圾收集程序在另外一个cpu上。
- 吞吐量:就是cpu用于运行用户代码的时间与cpu总消耗时间的比值。
- 吞吐量 = 运行用户代码时间/(运行用户代码时间 + 垃圾收集时间)
例如:虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
3.1 cms收集器(老年代收集器,并发gc)
特性:
cms(concurrent mark sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的java应用集中在互联网站或者b/s系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。cms收集器就非常符合这类应用的需求。
cms收集器是基于“标记—清除”算法实现的,它的整个过程分为4个步骤:
- 1. 初始标记(cms initial mark):初始标记仅仅只是标记一下gc roots能直接关联到的对象,速度很快,需要“stop the world”。
- 2. 并发标记(cms concurrent mark):并发标记阶段就是进行gc roots tracing的过程。
- 3. 重新标记(cms remark):重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“stop the world”。
- 4. 并发清除(cms concurrent sweep):并发清除阶段会清除对象。 由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,cms收集器的内存回收过程是与用户线程一起并发执行的。
优点:
- cms是一款优秀的收集器,它的主要优点在名字上已经体现出来了:并发收集、低停顿。
缺点:
- cms收集器对cpu资源非常敏感;
- cms收集器无法处理浮动垃圾;
- cms收集器会产生大量空间碎片。
3.2 g1收集器(唯一一款全区域的垃圾回收器)
g1(garbage first)垃圾回收器是用在heap memory很大的情况下,把heap划分为很多很多的 region块,然后并行的对其进行垃圾回收。
g1垃圾回收器在清除实例所占用的内存空间后,还会做内存压缩。
年轻代垃圾收集 :
在g1垃圾收集器中,年轻代的垃圾回收过程使用复制算法。把eden区和survivor区的对象复制到新的survivor区域。 如下图:
老年代垃收集:
- 对于老年代上的垃圾收集,g1垃圾收集器也分为4个阶段,基本跟cms垃圾收集器一样,但略有不同:
- 初始标记(initial mark)阶段:同cms垃圾收集器的initial mark阶段一样,g1也需要暂停应用程序的执行,它会标记从根对象出发,在根对象的第一层孩子节点中标记所有可达的对象。但是g1的垃圾收集器的initial mark阶段是跟minor gc一同发生的。也就是说,在g1中,你不用像在cms那样,单独暂停应用程序的执行来运行initial mark阶段,而是在g1触发minor gc的时候一并将年老代上的initial mark给做了。
- 并发标记(concurrent mark)阶段:在这个阶段g1做的事情跟cms一样。但g1同时还多做了一件事情,就是如果在concurrent mark阶段中,发现哪些tenured region中对象的存活率很小或者基本没有对象存活,那么g1就会在这个阶段将其回收掉,而不用等到后面的clean up阶段。这也是garbage first名字的由来。同时,在该阶段,g1会计算每个 region的对象存活率,方便后面的clean up阶段使用 。
- 最终标记(cms中的remark阶段):在这个阶段g1做的事情跟cms一样, 但是采用的算法不同,g1采用一种叫做satb(snapshot-at-the-begining)的算法能够在remark阶段更快的标记可达对象。
- 筛选回收(clean up/copy)阶段:在g1中,没有cms中对应的sweep阶段。相反,它有一个clean up/copy阶段,在这个阶段中,g1会挑选出那些对象存活率低的region进行回收,这个阶段也是和minor gc一同发生的,如下图所示:
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论