JVM——垃圾回收机制

2018-09-07  本文已影响0人  Samuel_Tom

哪些内存需要回收

由于程序计数器、虚拟机栈、本地方法栈的生命周期都跟随线程的生命周期,当线程销毁了,内存也就回收了,所以这几个区域不用过多地考虑内存回收。由于堆和方法区的内存都是动态分配的,而且是线程共享的,所以内存回收主要关注这部分区域。

如何判断对象是否存活

引用计数法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1,如果引用失效,计数器值减1,所以当该计数器的值为0时,就表示该对象可以被回收了。但是存在两个对象之间相互循环引用的问题。

可达性分析算法

通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索的路径称为引用链,当一个对象到“GC Roots”没有任何引用链相连的话,也就是GC Roots到这个对象不可达时,证明此对象已经不可用,可以被回收了。

二次标记

在可达性分析算法中被判断是对象不可达时不一定会被垃圾回收机制回收,因为要真正宣告一个对象的死亡,必须经历两次标记的过程。如果发现对象不可达时,将会进行第一次标记,此时如果该对象调用了finalize()方法,那么这个对象会被放置在一个叫F-Queue的队列之中,如果在此队列中该对象没有成功拯救自己(拯救自己的方法是该对象有没有被重新引用),那么GC就会对F-Queue队列中的对象进行小规模的第二次标记,一旦被第二次标记的对象,将会被移除队列并等待被GC回收,所以finalize()方法是对象逃脱死亡命运的最后一次机会。

垃圾回收算法

标记—清除算法

首先标记出需要回收的对象,在标记完成后进行统一的回收(标记即二次标记的过程)。此算法有两个不足:一是效率问题,标记和清除两个过程效率都不高;二是空间问题,标记清除后会产生大量不连续的内存碎片,内存空间碎片太多的话会导致以后程序在运行中想要分配较大对象的时候,无法找到一块连续的内存空间而导致不得不进行又一次的GC回收(后续的垃圾回收算法都是基于此算法进行改进的)。

标记—清除算法

复制算法

把内存按容量划分为大小相等的两块区域,每次只使用其中的一块,当这一块的内存空间用完了,就把还存活的对象复制到另一块内存中去,然后把已经使用的过的内存空间一次性清理掉。这样每次都是对半个内存区域进行GC回收,并不会产生内存碎片,但是代价是把内存缩小了一半,效率比较低。

复制算法

标记—整理算法

标记算法一样,区别是清除的时候会把所有存活的对象向一端移动(向上和向左),然后清除掉端边界以外的内存。

标记—整理算法

分代收集算法

根据对象存活周期的不同将内存划分为几块(新生代或老生代),然后根据每个年代的特点采用最合适的收集算法。比如在新生代中,每次都有大量对象死去,就选择复制算法;而在老生代中对象的生存率高,没有额外的空间为它进行分配担保,所以采用标记—清除算法或者标记—整理算法来进行回收。

垃圾回收器

CMS收集器

CMS收集器是一种以获取最短回收停顿时间为目标的垃圾收集器,是基于“标记——清除”算法实现的,其回收过程主要分为四个步骤:

(1)初始标记:标记一下GC Roots能直接关联到的对象,速度很快;
(2)并发标记:进行GC Roots Tracing的过程,也就是标记不可达的对象,相对耗时;
(3)重新标记:修正并发标记期间因用户程序继续运作导致的标记变动,速度比较快;
(4)并发清除:对标记的对象进行统一回收处理,比较耗时;

由于初始标记和重新标记速度比较快,其它工作线程停顿的时间几乎可以忽略不计,所以CMS的内存回收过程是与用户线程一起并发执行的。
初始标记和重新标记两个步骤需要Stop the world;并发标记和并发清除两个步骤可与用户线程并发执行。  
“Stop the world”意思是垃圾收集器在进行垃圾回收时,会暂停其它所有工作线程,直到垃圾收集结束为止。

CMS的缺点

(1)对CPU资源非常敏感;也就是说当CMS开启垃圾收集线程进行垃圾回收时,会占用部分用户线程,如果在CPU资源紧张的情况下,会导致用户程序的工作效率下降。

(2)无法处理浮动垃圾导致又一次FULL GC的产生;由于CMS并发回收垃圾时用户线程同时也在运行,伴随用户线程的运行自然会有新的垃圾产生,这部分垃圾出现在标记过程之后,CMS无法在当次收集过程中进行回收,只能在下一次GC时在进行清除。所以在CMS运行期间要确保内存中有足够的预留空间用来存放用户线程的产生的浮动垃圾,不允许像其它收集器一样等到老年代区完全填满了之后再进行收集;那么当内存预留的空间不足时就会产生又一次的FULL GC来释放内存空间,由于是通过Serial Old收集器进行老年代的垃圾收集,所以导致停顿的时间变长了(系统有一个阈值来触发CMS收集器的启动,这个阈值不允许太高,太高反而导致性能降低)。

(3)标记——清除算法会产生内存碎片;如果产生过多的内存碎片时,当系统虚拟机想要再分配大对象时,会找不到一块足够大的连续内存空间进行存储,不得不又一次触发FULL GC。

G1收集器

G1收集器是一款成熟的商用的垃圾收集器,是基于“标记——整理”算法实现的,其回收过程主要分为四个步骤:

(1)初始标记:标记一下GC Roots能直接关联到的对象,速度很快;
(2)并发标记:进行GC Roots Tracing的过程,也就是标记不可达的对象,相对耗时 ;
(3)最终标记:修正并发标记期间因用户程序继续运作导致的标记变动,速度比较快;
(4)筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划;

G1收集器的特点

(1)并发与并行:机型垃圾收集时可以与用户线程并发运行;
(2)分代收集:能根据对象的存活时间采取不同的收集算法进行垃圾回收;
(3)不会产生内存碎片:基于标记——整理算法和复制算法保证不会产生内存空间碎片;
(4)可预测的停顿:G1除了追求低停顿时间外,还能建立可预测的停顿时间模型,便于用户的实时监控;

CMS收集器与G1收集器的区别

(1)CMS采用标记——清除算法会产生空间碎片,G1采用标记——整理算法不会产生空间碎片;
(2)G1可以建立可预测的停顿时间模型,而CMS则不能;

内存分配策略

新生代:新生代包含一个Eden区和两个Survivor区。大多数情况下,新创建的对象会在新生代Eden区中分配,当Eden区没有足够的空间进行分配时,虚拟机会触发一次Minor GC,如果Survivor区空间允许的话,该对象将被分配到Survivor。

老年代:大对象直接在老年代中分配,大对象指需要大量连续内存空间的Java对象。

分代收集如何判定对象的年龄?

虚拟机给每个对象定义了一个对象年龄计数器,如果对象在Eden区出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间,并且对象年龄设置为1,。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当对象的年龄增加到一定程度(默认为15岁),就会被晋升到老年代中。

为什么新生代中要有Survivor区?

防止频繁触发FULL GC。如果没有Survivor,Eden区每进行一次Minor GC,存活的对象就会被送到老年代,这样会使老年代很快被填满,导致老年代触发FULL GC,由于老年代的内存空间远大于新生代,所以进行一次Full GC消耗的时间比Minor GC长得多。

为什么要设置两个Survivor区?

防止产生内存空间碎片。如果只有Survivor1,那么每一次当Eden区满时,触发Minor GC并把对象移入Survivor1中,如此循环对导致Survivor1中产生大量的空间碎片;所以需要有Survivor2,当Eden再一次满时,触发Minor GC,虚拟机会把 Eden中和Survivor1中的存活对象通过复制算法移入Survivor2中,这样Survivor2就不会产生内存碎片,同时Eden和Survivor1会清理内存,保证下一次Minor GC触发时的操作。

Minor GC和Full GC的区别?

(1)新生代GC(Minor GC):指发生在新生代的垃圾收集动作,因为Java对象大多数都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度比较快。
(2)老年代GC(Full GC):指发生在老年代的垃圾收集动作,速度非常慢,所以要尽量减少Full GC的发生。

引用类型

强引用:指向使用new关键字创建的对象的引用都是强引用,只要该对象的强引用还在,该对象永远都不会被GC回收;

软引用:当内存不足时,就会被回收;

弱引用:只要发生GC,就会被回收;

虚引用:随时都会被回收;

参考

《深入理解Java虚拟机》第2版 第3章 垃圾收集器与内存分配策略

推荐阅读

让你彻底明白JAVA中堆与栈的区别

上一篇下一篇

猜你喜欢

热点阅读