GC的基本概念和GC算法

一、概述


垃圾收集(Garbage Collection) -- 一般主观上会认为做法是:找到垃圾,然后把垃圾扔掉。在VM中,GC的实现过程恰恰相反,GC的目的是为了追踪所有正在使用的对象,并且将剩余的对象标记为垃圾,随后标记为垃圾的对象会被清除,回收这些垃圾对象占据的内存,从而实现内存的自动管理。

二、分析

2.1 什么是垃圾

我们所说的垃圾是指没有任何引用的一个对象或者多个对象(这多个对象相互引用,但是没有一个与主对象挂钩,也就是根可达算法(下文会讲)无法找到这其中任何一个对象。

在C和C++都是需要开发者用代码手动回收内存的:C语言用free关键字来回收内存,C++用的是delete。但是手动回收内存容易出现两种类型问题:忘记回收(容易引发内存泄露)和多次回收。

后来诞生的java、python等都是自带了垃圾回收器的语音,开发者只管创建对象,对象的销毁不需要手动处理,由专门的垃圾回收器进行回收。

2.2 如何判断是垃圾

常用的方法有两种

(1)引用计数法:每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。

但是引用计数没办法解决垃圾之间互相引用的情况,当几块内存都没有外部引用,但是这几块内存之间相互引用的时候,这几块内存也应该视为垃圾,但是引用计数却不为0

(2)根可达(Root Searching):将根对象取出,由根对象出发往下查找,最终找不到的对象,都视为无法由根对象找到,也就是说找不到的对象就都视为垃圾。

  那么哪些对象是根对象呢?主要包含:JVM stack,native method stack,run-time constant pool(运行常量池里的对象),static references in method area(方法区里的静态引用)等。

2.3 常见的垃圾回收算法

主要有三种:

(1)标记-清除算法(Mark Sweep):首先标记处所有需要回收的对象,标记完成后统一回收所有被标记的对象。但这种方法有个严重的缺陷,标记清除后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集操作。

(2)复制算法(Copying):将可用内存分成两块,每次只使用其中的一块。当这一块内存用完,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间全部清理掉。这种方法虽然解决了碎片化问题,但浪费空间,每次只是使用一半空间。

(3)标记-整理(压缩)算法(Mark-Compact):标记过程与标记-清除算法一样,但不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。这种方式明显的缺点就是效率偏低。

2.4 JVM内存分代模型

JVM的模型

把heap分成年轻代和老年代是为了提高heap回收垃圾的效率。

年轻代的工作流程如图:

大部分对象刚创建的时候都会分配在年轻代的Eden区,只要年轻代空间不够就会触发MinorGC(只回收年轻代内存),minorGC采取复制算法进行回收,当JVM运行触发第一轮minorGC时,会将eden区存活的对象先复制到一个suprivor区。然后删除eden区对象,当触发下一轮minorGC时,又把suprivor区和eden区的存活的对象转移到另一个suprivor区,然后删除这两个区的所有对象。依次类推。至于为什么要用复制算法,包括老年代的标记整理算法,这是考虑到了避免内存碎片。如果对象内存不连续,会造成很多的空间浪费。

老年代的对象都是从年轻代根据一定的规则流转过来的。 具体有几类流转方式:

  • 超过指定年龄(参数-XX:MaxTenuringThreshold 配置,默认15),这里年龄指的是没有被垃圾回收,存活下来一次理解为增加一岁。流转到老年代。

  • 大对象直接进入,超过参数指定字节数(-XX:PretenureSizeThreshold)设置的字节数的大对象会直接进入老年代,这是因为对象越大,复制开销就越大。

  • 动态年龄判断规则进入,意思是不一定要到指定年龄再流转到15,如果某一年龄以上的对象到达一定大小,也会提前进入老年代。当躲过一轮GC的对象加起来超过surrvivor区50%,如年龄1+年龄2+年龄n一直累加,直到年龄n的时候发现加起来超过了surrvivor空间的50%,则年龄n以上的对象直接进入老年代

  • minorGC发生时,suprivor区放不下,超过的部分转移到老年代。这里涉及一个老年代分配担保规则,指的是每次MinorGC发生时,都会判断老年代可用内存大不大于,年轻代存活对象内存之和,如果大于则直接进行minorGC,如果小于则要看参数XX:HandlePromotionFailure是否启用(默认启用),如果启用则对老年代这次需要承载的转移对象内存进行预估(取前面minorGC被转移的平均内存大小),若大于则也进行MinorGC,若意料状况外转移内存超出了老年代可用空间,则进行FullGC,若fullGC还是不够,则抛出OOM错误。FullGC是采取的标记整理算法,指的是移动存活对象,让内存连续,然后删除需要回收的对象,为什么使用标记整理?因为认为老年代对象存活几率高,复制算法不划算。

值得一提的是,new新生代和old老年代的比例默认是1:2。但是这个比例是可以自己调节的参数。eden和survivor1,survivor2的默认比例是8:1:1,也是可以调整的。

对象分配过程如下:

(1) 当我们new出一个对象,JVM会首先尝试往栈上分配,如果能够分配得下,就分配到栈上分配到栈上的对象有好处就是不需要GC进行管理,什么时候不需要用到此对象了,将对象出栈就可以了。但是分配到栈上的对象是有要求的:第一,对象比较小,因为栈空间本来就不够大;第二,对象比较简答。

(2) 如果栈上分配不下,我们就判断这个对象是不是够大,如果足够大就直接放在老年代区,在老年代区的对象经过一次全量垃圾回收FGC(全堆范围的gc)后,才有可能被回收掉。

(3) 如果如果栈上分配不下并且对象不大,就会判断对象能否被存在线程本地分配缓冲区-TLAB(Thread Local Allocation Buffer)。但是不管放不放得下,都是放在新生代区的伊甸区eden。 但是因为堆是共享的,多个线程可以同时创建对象就可能会争夺同一块内存区域,所以为了保证线程安全,Eden区又被分配成一个个线程本地分配缓冲区,这个TLAB是线程私有的,每个线程都有自己的TLAB,避免了多线程环境下使用同步技术带来的性能损耗。

(4) 伊甸区eden的对象在经过一次GC后,如果被回收掉了,那就结束了生命周期。

(5) 伊甸区eden的对象在经过一次GC后,如果没有被回收掉,JVM在整个new新生代区都采用Copying(拷贝算法),将不是垃圾的对象拷贝到幸存者区survivor1,对比上面的堆内存逻辑分区图。幸存者区survivor1中的对象再经过一次GC后如果对象还存活,那么就拷贝到幸存者区survivor2并且清理掉幸存者区survivor1中的所有对象,再有GC就反复这个操作,直到对象的分代年龄达到了移到老年代的界限(一般分代垃圾回收器默认是15,CMS默认是6),就会被移到老年代中,老年代采用标记压缩(mark compact)算法,保证内存的连续性 。

2.5常见的垃圾回收器

 jdk从1.0到14.0一共诞生了10种垃圾回收器

分类如下:

(1) 分代模型:Serial,Serial Old,Parallel Scavenge,Parallel Old,ParNew,CMS

(2) 不分代模型:G1(虽然物理模型上没分代,但是逻辑层面上是分代的,jdk1.8及以上的版本建议使用G1,响应时间很快,但是1.8默认是PSPO<Parallel Scavenge和Parallel Old>),ZGC(Oracle官方支持),Shenandoah(小红帽公司开发)

 (3) 特殊模型:Epsilon(这种垃圾回收器不回收垃圾,只是跟踪垃圾的产生和回收,但是这个回收只是动作,其实没真正回收。Epsilon有两个用途:<1>用于调试;<2>内存很大,程序很小很快就能运行完成。

2.5.1 Serial

Serial 即串行的意思,也就是说它以串行的方式执行,它是单线程的收集器,只会使用一个线程进行垃圾收集工作,GC 线程工作时,其它所有线程都将停止工作(stw : stop-the-word )。

使用复制算法收集新生代垃圾。

其工作流程如下: 

它的优点是简单高效,在单个 CPU 环境下,由于没有线程交互的开销,因此拥有最高的单线程收集效率,所以,它是 Client 场景下的默认新生代收集器。

注: 这一收集器,随着内存越来越大,执行效率太慢了.好比一个人最开始住的是5平米的房子,打扫房子很快,当住上500平大房子的时候,清理一遍房子花的时间太长

2.5.2 CMS(Concurrent Mark Sweep)

是真正的并发收集器,在程序运行时在后台同时运行的一种收集器,不需要等老年代快慢的时候进行数据收集,而是同时运行,他只有在标记是才会STW。在一些对响应时间有很高要求的应用或网站中,用户程序不能有长时间的停顿,CMS 可以用于此场景。 

工作流程如图:


CMS采用了多种方式尽可能降低GC的暂停时间,减少用户程序停顿。
停顿时间降低的同时牺牲了CPU吞吐量 。因为并发情况占用大量cpu资源
这是在停顿时间和性能间做出的取舍,可以简单理解为"空间(性能)"换时间

2.5.3 Parallel Scavenge

并行收集器,使用CPU多核资源,多线程去处理标记和清除等操作。只有当堆(heap)快慢的时候才开始工作

使用场景:内存有限,CUP资源有限,app需要更高的吞吐量并接受延迟

2.5.4 G1(Garbage-1st)
在G1中,内存空间被分割成一个个的Region区,所谓新生代和老年代,都是由一个个region组成的。同时G1也不需要跟别的收集器一起配合使用,自己就可以搞定所有内存区域,首先估计每个Region中的垃圾数量,每次都从垃圾回收价值最大的Region开始回收,因此可以获得最大的回收效率。整体上来讲不是一个分代收集器,是一个通吃收集器。

如图G1结构如 下:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值