JVM之垃圾收集器

Java的垃圾收集器是区别于C++语言的一个重要特征。在C++里面,内存的分配以及回收,都是程序员可控的,这带来的好处就是,只要你处理得当,内存空间就不会存在大量浪费,但同时,这也是C++程序员最痛苦的地方,每一个内存的分配和回收都需要自己去处理,稍不注意就是删库跑路(手动滑稽);而针对这一点,Java就完全将内存的分配以及回收交给了JVM,Java程序员只需要在适当的地方创建对象,分配内存;在对象生命周期结束后,对象的回收不需要程序员亲自处理,JVM自己会在特定时间去进行回收处理。而JVM针对不必存活的对象的回收就是依赖垃圾收集器(GC收集器)。

目录

一、判断对象所在内存是否需要回收

1、引用计数算法

2、可达性分析算法

二、垃圾收集算法

1、标记-清除

2、复制算法

3、标记-整理

4、分代收集算法

三、安全点

四、收集器

1、Serial收集器

2、ParNew收集器

3、Paranllel Scavenge收集器

4、Serial Old以及Parallel Old收集器

5、CMS收集器

6、G1收集器

五、内存分配

1、对象优先在Eden分配

2、大对象直接进入老年代

3、长期存活的对象进入老年代

4、动态对象年龄判定

5、空间分配担保


一、判断对象所在内存是否需要回收

在GC收集器回收整理内存时,肯定不是想回收哪块就回收哪块,它针对的内存一般是你不再使用的对象(其实GC收集器不止回收堆上的内存,还会回收方法区、栈里面的内存,只是相比之下,堆内存的回收的成果要好很多,所以笔者这里总结的都是以堆为例)。

1、引用计数算法

判断对象是否应该存活,一种算法就是通过判断,这个对象是否被其他地方引用过,如果被引用了,那么它就应该存活下去,GC回收器就不会回收它的内存。而引用计数算法就是,当一个对象被另一个地方引用时,它的引用计数器就会+1,如果失去一个引用就会-1,当该值为0时,就表明它没有被引用。

虽然该算法实现简单,判定效率也很高,但是,在主流的JVM中并没有使用该方法管理内存。因为这会存在一个问题:相互引用(这里笔者就不贴出代码和例子了,如需了解自行百度或者留言)。

2、可达性分析算法

在主流的JVM中,都是通过可达性分析来判定对象是否存活,是否该被清理的。这个算法的思路就是,通过一些称为GC Roots(这是复数形式,说明有很多起始点)的对象作为起始点,从这些节点向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明这些对象不可用,可以被清除。读者可以把这些路径看成很多个多叉树,每一个GC Root都是一个树的根节点,这些树的所有节点就是可用的对象,而没有被任何一棵树链接的对象就是不可用的,不能被访问到的,那么这个对象也就没有存在的必要了。

关于GC Roots,在Java语言中,可作为GC Roots的对象包括下面几种:

(1、JVM栈中引用的对象

(2、方法区中类静态属性引用的对象

(3、方法区中常量引用的对象

(4、本地方法栈中JNI引用的对象

这里读者可能会发现,在JVM的内存模型的5个块中,除了程序计数器,其他4块,只有堆区域没有被包含进去,而堆里面存在了大量的对象,为什么没有包含进去?其实可以这样理解JVM堆与栈的关系:堆只负责存储对象以及对象的数据,它不负责使用,当需要使用对象时,都是在栈里面通过存入指向这个对象的引用来使用这个对象以及它的数据。所以,如果一个对象,没有被使用,那么它在栈里面就找不到任何指向它的引用。

综上,就可以从JVM栈中为根节点开始寻找它引用的对象,这样,栈中所需要的所有在堆里面的对象就可以被分析标志出来,那么剩下的(如果忽略掉方法区和本地方法栈的话)对象就是不可用的,或者不可访问到的,就可以被清理掉。

二、垃圾收集算法

1、标记-清除

算法如其名,进行可达性分析之后,能够标记出需要被清理的对象,然后直接清理。

这个算法有两个主要的不足:首先是效率不高,其次是,标记-清除,会产生大量不连续的内存碎片,这样导致以后分配较大对象而找不到足够的连续内存,就不得不提前触发另一次垃圾收集动作。

2、复制算法

为了解决效率问题,该算法将可用内存分为两块,每次只使用其中一块,当其中一块内存用完了,就将还存活着的对象复制到另外一块,然后把已使用过的内存空间一次清理掉。这样就不需要考虑内存碎片等复杂情况,不过弊端就是会缩小内存空间的一半,因为另一半并没有服务于用户。

3、标记-整理

标记之后,将存活的对象都向一端移动,然后直接清理掉端边界以外的内存,这样就不会存在内存碎片。

4、分代收集算法

大家所熟知的新生代、老年代,就是在不同的年代中使用不同的收集算法,以达到高效GC的算法。

三、安全点

所谓安全点,就是在这个节点开始GC可以得到高效率的回收内存(JVM一般情况下只会在达到安全点的时候才会进行一次GC操作)。这也是为什么说Java里面内存的回收以及GC的启动是不受外部控制的(你以为JVM暴露了System.gc()这个方法,你就可以随心所欲启动GC回收了吗?too young too simple。当你调用这个方法的时候,是否执行是看JVM心情的。心情好就照顾下你的情绪,执行一次,心情不好,懒得鸟你,你调任你调,照样不执行)。

这里涉及到俩个方案:①抢先式中断②主动式中断

所谓抢先式,就是GC发生时,首先把所有线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让它跑到安全点上(几乎不使用这种方法了)。

而主动式就是,GC发生,需要中断线程时,不直接对线程操作,只是设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时,就自己中断挂起(轮询点设置在安全点,那么中断时就保证在安全点了)。

安全区域:这个区域中,任意地方GC都是安全的,引用关系不会发生变化(如果发生变化,那么之前的垃圾收集算法进行的标志,哪些对象不可访问,没有被使用,就可能会出错)。

四、收集器

1、Serial收集器

在进行GC时,必须暂停其他所有工作线程,直到收集结束 。

新生代中采用复制算法,老年代中采用标记-整理算法。

虽然当它工作时,会暂停其他所有的工作线程,但是因为它的高效,其实停顿时间是很少的(可控在几十毫秒到100多毫秒),至少在不频繁GC的情况下,基本是察觉不到的。

Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。

2、ParNew收集器

这个收集器相当于是Serial的多线程版,Serial在进行GC时,只会使用一个CPU(即使处理器是多核的,它不管,管你上天入地,它就用一个CPU,一个线程去GC),而ParNew就是使用多条线程进行GC。该收集器算是运行在Server模式下的虚拟机中,首选的新生代收集器,其中一个性能无关的原因就是,除了Serial收集器,它是唯一一个能与CMS收集器(下面会提及)配合工作的。

3、Paranllel Scavenge收集器

新生代收集器,也是使用复制算法的多线程收集器。和ParNew的不同,它的特别主要体现在该收集器关注点和其他收集器不同,其他收集器在乎的是GC时,用户线程的停顿时间,而它则是达到一个可控制的吞吐量(就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间))。

4、Serial Old以及Parallel Old收集器

分别是Serial以及Parallel Scavenge收集器的老年代版本。Serial Old收集器主要意义在于给Client模式下的JVM使用。Parallel Old,如果新生代选择Parallel Scavenge收集器,老年代除了Serial Old收集器之外别无选择。

5、CMS收集器

以获取最短回收停顿时间为目标的收集器。大部分集中应用在B/S系统的服务端。

CMS收集器是基于标记-清除算法实现的,运作过程大致分为4个步骤:

①初始标记②并发标记③重新标记④并发清除(如需了解每个步骤什么内容,请自己百度,手动滑稽)

耗费时间的②和④因为是和用户线程并发执行的,所以停顿时间在就会给用户很好的体验。

CMS收集器3个明显缺点:

对CPU资源非常敏感。

无法处理浮动垃圾(因为GC标记之后,是并发清除,所以在GC时,用户线程还在不断制造垃圾,而这些垃圾是标志之后发生的,那么就没有被标记过,GC时自然无法回收这些没有被标记,新产生的垃圾)。

基于 标记-清除 的算法,那么不可避免就会产生碎片化的内存空间。

6、G1收集器

特点:

①并行并发②分代收集③空间整合:基于 标记-整理 算法④可预测停顿

G1收集器运作步骤:

①初始标记②并发标记③最终标记④筛选回收

五、内存分配

1、对象优先在Eden分配

大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC(指发生在新生代的垃圾收集动作,该动作比较频繁,回收速度也比较快,而Full GC/Major GC指放生在老年代的GC动作,该GC速度一般比Minor GC慢10倍)。如果Minor GC之后,发现新生代内存还是不足以分配给新对象,那么之前保存在新生代的对象就会提前进入老年代。

2、大对象直接进入老年代

大对象,指需要大量连续内存空间的java对象,比如数组,很长的字符串。JVM有一个参数(PretenureSizeThreshold),可以设置,对象大于这个参数那么直接进入老年代。

3、长期存活的对象进入老年代

再进行多次GC后,如果一个对象还存活着,那么它就该进入老年代,一般这个参数MaxTenuringThreshold设置为15,就是15次GC后还存活的对象(每一次GC之后存活的对象,年龄都会+1)。

4、动态对象年龄判定

如果相同年龄的对象所占内存空间的总和,大于Survivor空间的一半,那么该年龄和大于该年龄的对象会被转移到老年代(Survivor)

5、空间分配担保

每次发生Minor GC时,都会进行一次判定,如果老年代最大连续可用的空间大于Minor GC的对象内存总和,那么这一次Minor GC是安全的,如果不大于,则根据参数值HandlePromotionFailure的值,来进行“冒险”(老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小,如果大于,则进行Minor GC,否则还是发起Full GC)或者Full GC。

关于JVM的垃圾回收机制总结就到这里,如果读者有什么好的建议或者指正的地方恳请留言。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值