GC垃圾回收机制总结

本文参考(https://github.com/jeanboydev/Android-ReadTheFuckingSourceCode/blob/master/article/java/jvm/JVM-%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E6%9C%BA%E5%88%B6.md)
(https://blog.csdn.net/u013630349/article/details/78342645)文章,如若侵权请通知删除。

最近面试一直会被问到关于GC的问题,之前知道这方面知识,面试答的时候也能答出来,但是总是觉得没有条例的讲解和回答总会让人觉得自己这方面东拼西凑出来的,太杂乱了,所以今天有条理的总结一下GC的来龙去脉。这次仅是总结,相信以后还会从源码层次再次进行分析。

GC概述
垃圾回收是一种自动的存储管理机制。 当一些被占用的内存不再需要时,就应该予以释放,以让出空间,这种存储资源管理,称为垃圾回收(Garbage Collection)。 垃圾回收器可以让程序员减轻许多负担,也减少程序员犯错的机会。

接下来我从**什么时候?对什么东西?做了怎么样的处理?**三个方面来进行分析。

1. 首先第一个问题就是什么时候?换句话说就是GC的触发条件

GC触发的条件有两种:

(1)程序调用System.gc时可以触发;

(2)系统自身来决定GC触发的时机。

第二个条件“系统自身来决定”我最开始看到是一脸蒙逼的,什么叫自身决定,JAVA虚拟机它就算再只能也是人写的,所以肯定会有它内部的触发条件。要了解它的触发条件,就要先了解一下JVM内存空间,请看图片
在这里插入图片描述

程序计数器:线程私有。是一块较小的内存,是当前线程所执行的字节码的行号指示器。是Java虚拟机规范中唯一没有规定OOM(OutOfMemoryError)的区域。

Java栈:线程私有。生命周期和线程相同。是Java方法执行的内存模型。执行每个方法都会创建一个栈帧,用于存储局部变量和操作数(对象引用)。局部变量所需要的内存空间大小在编译期间完成分配。所以栈帧的大小不会改变。存在两种异常情况:若线程请求深度大于栈的深度,抛StackOverflowError。若栈在动态扩展时无法请求足够内存,抛OOM。

Java堆:所有线程共享。虚拟机启动时创建。存放对象实力和数组。所占内存最大。分为新生代(Young区),老年代(Old区)。新生代分Eden区,Servior区。Servior区又分为From space区和To Space区。Eden区和Servior区的内存比为8:1。 当扩展内存大于可用内存,抛OOM。
(1)新生代:所有新 new 出来的对象都会最先出现在新生代中,当新生代这部分内存满了之后,就会发起一次垃圾收集事件,这种发生在新生代的垃圾收集称为 Minor collections。 这种收集通常比较快,因为新生代的大部分对象都是需要回收的,那些暂时无法回收的就会被移动到老年代。全局暂停事件(Stop the World):所有小收集(minor garbage collections)都是全局暂停事件,也就是意味着所有的应用线程都需要停止,直到垃圾回收的操作全部完成。
(2)老年代:老年代用来存储那些存活时间较长的对象。 一般来说,我们会给新生代的对象限定一个存活的时间,当达到这个时间还没有被收集的时候就会被移动到老年代中。随着时间的推移,老年代也会被填满,最终导致老年代也要进行垃圾回收。这个事件叫做大收集(major garbage collection)。大收集也是全局暂停事件。通常大收集比较慢,因为它涉及到所有的存活对象。所以,对于对相应时间要求高的应用,应该将大收集最小化。此外,对于大收集,全局暂停事件的暂停时长会受到用于老年代的垃圾回收器的影响。

方法区:所有线程共享。用于存储已被虚拟机加载的类信息、常量、静态变量等数据。又称为非堆(Non – Heap)。方法区又称“永久代”。GC很少在这个区域进行,但不代表不会回收。这个区域回收目标主要是针对常量池的回收和对类型的卸载。当内存申请大于实际可用内存,抛OOM。
(3)永久代:永久代存储了描述应用程序类和方法的源数据,JVM 运行应用程序的时候需要这些源数据。 永久代由 JVM 在运行时基于应用程序所使用的类产生。 此外,Java SE 类库的类和方法可能也存储在这里。如果 JVM 发现有些类不在被其他类所需要,同时其他类需要更多的空间,这时候这些类可能就会被垃圾回收。

本地方法栈:线程私有。与Java栈类似,但是不是为Java方法(字节码)服务,而是为本地非Java方法服务。也会抛StackOverflowError和OOM。

看完上面的定义之后,接下来再看看GC的系统触发条件,它的触发条件分为
Minor GC ,Full GC,那么就逐个解释一下。

Minor GC触发条件:当Eden区满时,触发Minor GC。

Full GC触发条件:

(1)调用System.gc时,系统建议执行Full GC,但是不必然执行

(2)老年代空间不足

(3)方法区空间不足

(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存

2. 其次,当触发GC后,对什么东西进行的GC,那我们来看看下面这句话
自动垃圾回收机制就是寻找Java堆中的对象,并对对象进行分类判别,寻找出正在使用的对象和已经不会使用的对象,然后把那些不会使用的对象从堆上清除。
那那些会使用和不会使用的对象是什么呢?换句java语言就是,当一个对象没有被引用指向的时候就说明它就是不会使用的对象了。那么问题来了,怎么判断它到底有没有被引用指向呢?那么就有了这两种方式:引用计数法、可达性分析,那么我就逐个解释一下。

  • 引用计数法

引用计数算法是垃圾收集器中的早期策略。 在这种方法中,堆中的每个对象实例都有一个引用计数。当一个对象被创建时,且将该对象实例分配给一个引用变量,该对象实例的引用计数设置为 1。当任何其它变量被赋值为这个对象的引用时,对象实例的引用计数加 1(a = b,则b引用的对象实例的计数器加1),但当一个对象实例的某个引用超过了生命周期或者被设置为一个新值时,对象实例的引用计数减 1。
特别地,当一个对象实例被垃圾收集时,它引用的任何对象实例的引用计数器均减 1。 任何引用计数为0的对象实例可以被当作垃圾收集。
引用计数收集器可以很快的执行,并且交织在程序运行中,对程序需要不被长时间打断的实时环境比较有利,但其很难解决对象之间相互循环引用的问题。

  • 可达性分析

可达性分析算法是从离散数学中的图论引入的,程序把所有的引用关系看作一张图,通过一系列的名为 “GC Roots” 的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain)。 当一个对象到 GC Roots 没有任何引用链相连(用图论的话来说就是从 GC Roots 到这个对象不可达)时,则证明此对象是不可用的。
在这里插入图片描述

说到GC roots(GC根),在JAVA语言中,可以当做GC roots的对象有以下几种:

     1、虚拟机栈中的引用的对象。

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

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

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

第一和第四种都是指的方法的本地变量表,第二种表达的意思比较清晰,第三种主要指的是声明为final的常量值。
3.最后也就是对那些没有引用指向的对象**做了怎么样的处理?**呢?GC怎么回收这些没用的对象呢?
这时候就要看看垃圾回收算法。它总共有四种类型:
( 1 )标记-清除算法
a.标记,也就是垃圾收集器会找出那些需要回收的对象所在的内存和不需要回收的对象所在的内存,并把它们标记出来,简单的说,也就是先找出垃圾在哪儿?
在这里插入图片描述
所有堆中的对象都会被扫描一遍,以此来确定回收的对象,所以这通常会是一个相对比较耗时的过程。
b.清除,垃圾收集器会清除掉上一步标记出来的那些需要回收的对象区域。
在这里插入图片描述
存在的问题就是碎片问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。它的缺点就是内存不连续。
( 2 )复制算法
标记清除算法每次执行都需要对堆中全部对象扫面一遍效率不高,为解决效率问题,复制算法将内存按容量划分为大小相等的两块,每次只是用其中的一块。 当这一块使用完了,就将还存活的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。 这样使得每次都对半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。紧接着它的缺点就来了会减少可用内存。
在这里插入图片描述
( 3 )标记-整理算法
由于简单的标记清除可能会存在碎片的问题,所以又出现了压缩清除的方法,也就是先清除需要回收的对象,然后再对内存进行压缩操作,将内存分成可用和不可用两大部分。
在这里插入图片描述
( 4 )分代收集算法
分代算法是上面几个算法的综合, Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。 而老年代中因为对象存活率较高、没有额外的空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来回收。

最后再说一说垃圾收集器的分类吧:

串行收集器(serial collector)只有一条GC线程,在运行是会暂停用户程序(stop the world)

并行收集器(parallel collector)有多条GC线程,也需要暂停用户程序

并发收集器(concurrent collector)有一条或多条GC线程,需要在部分阶段暂停用户程序

JAVA提供了多种类型的垃圾收集器, JVM 中的垃圾收集一般都采用“分代收集”,不同的堆内存区域采用不同的收集算法,主要目的就是为了增加吞吐量或降低停顿时间。

1.Serial (用于新生代,采用复制算法)(串行收集器)
2.Serial Old(用于老年代,采用标记整理算法)(串行收集器)
3.ParNew(用于新生代,采用复制算法)(并行收集器)
4.Parallel Old(用于老年代,采用标记整理算法)(并行收集器)
5.Parallel Scavenge(用新生代,采用复制算法)(并行收集器)
6.CMS(用于老年代,采用标记清除算法)(并发收集器)
7.G1(jdk1.7以后的版本推出的,维持高回收率,减少停顿,类似于concurrenthashmap的分区概念)
(java9默认gc算法是G1,且把CMS标记为废弃)

这里着重讲一下CMS,因为它是并发的,速度很快。

CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。

基于“标记清除”算法,并发收集、低停顿,运作过程复杂,分4步:

1)初始标记:仅仅标记GC Roots能直接关联到的对象,速度快,但是需要“Stop The World”

2)并发标记:就是进行追踪引用链的过程,可以和用户线程并发执行。

3)重新标记:修正并发标记阶段因用户线程继续运行而导致标记发生变化的那部分对象的标记记录,比初始标记时间长但远比并发标记时间短,需要“Stop The World”

4)并发清除:清除标记为可以回收对象,可以和用户线程并发执行

1)、3)会造成停顿,由于整个过程耗时最长的并发标记和并发清除都可以和用户线程一起工作,所以总体上来看,CMS收集器的内存回收过程和用户线程是并发执行的。

JVM的两种模式

client模式和server模式

默认是client模式,server模式启动较慢,但是长时间运行的话,运行会越来越快

  • 8
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值