【Java虚拟机】垃圾回收之路(Garbage Collection)

本文介绍了java虚拟机的垃圾收集机制.

垃圾回收(Garbage Collection)的诞生时间早于java语言,经过半个多世纪的发展已经相当的成熟.而我们学习它的主要目的在于分析内存泄漏/内存溢出等出现的原因,以便解决相关问题.它也让我们能够在垃圾收集成为追求更高并发量的瓶颈的时候,能够对它进行有效的监控调节.

我们知道java虚拟机在内存中分为程序计数器/java虚拟机栈/本地方法栈/java堆/方法区等区域,其中计数器和栈都是随着线程产生和死亡的.而java堆和方法区则是垃圾收集的战场,其中堆中的垃圾收集尤为重要.方法区中存储的对象相对都是永久存在的,所以效率没有堆那么高.我们常说的垃圾收集其实是在说堆中的垃圾收集.

一. 如何判定一个对象需要被回收

首先我们需要知道一个对象被判"死"的依据.这个依据有两种方法来判别:引用计数法和可达性分析法.

  1. 引用计数法:在对象头的中包含一个引用计数器,当该对象被引用一次,则计数器+1;取消引用-1.引用计数器为0时,对象就成为了垃圾收集的目标.该方法存在一个弊端:当两个对象互相引用的时候,就算与外界已经断开了引用,也是无法被GC收集到的,更多的时候我们采用下面的方法;

  2. 可达性分析法:在内存中存在一个个的GC Roots对象,这些Roots通过与其他对象的引用来形成引用链.当某对象和任何Roots都没有可达的引用链的时候,对象成为垃圾收集的目标.可作为的GC Roots的对象包括以下类型:

    • 虚拟机栈(局部变量表)中引用的对象
    • 方法区中常量引用的对象
    • 方法区中静态变量引用的对象
    • 本地方法栈中引用的对象

通过以上两个方法,GC器已经知道哪些对象是可回收的对象了.我们了解到这两个方法都与"引用"有关.事实上,在JDK1.2之前,对于引用的定义仅限于"如果reference类型的数据中存储的是另一块内存的起始地址,就称为这块内存代表一个引用".但是渐渐地发现,很多时候我们没有办法形容一些鸡肋的引用,比如那些在内存足够的时候可以保存在内存中,当内存经过垃圾收集后还是十分紧张时,可以抛弃的对象.JDK1.2后扩充了"引用"的概念,并添加了"强软弱虚"四种引用,效果依次递减.

强引用(Strong Reference):类似于"Object o = new Object()",无论如何都不会被回收的引用对象.

软引用(SoftReference):有用但是非必要的对象.在内存即将要发生溢出异常的时候,可以标记这类引用进行二次回收.

弱引用(WeakReference):比软引用弱,甚至活不过下一次垃圾收集.无论内存是否溢出都会清除.

虚引用(PhantomReference):最弱的引用.存不存在这种引用关系不会对生存时间产生影响,也无法通过此类引用取得对象的实例.唯一目的在于在对象被回收的时候提供一个通知.

二. 被回收前的最后一步(完成遗愿finalize方法)

Object类中有一个finalize()方法,重写了finalize的对象在被回收前会被执行该方法,接下来我们来更加具体地分析这其中都发生了什么.

首先,我们已知对象A是确定即将被回收的,但在此之前它仍然需要至少经历2次被标记的过程:JVM确定A没有与任何GC Roots存在可达的引用链,于是对A进行标记并筛选,筛选的条件是该对象是否有必要进行finalize().没有覆盖或者已经执行过了都可认为是"没有必要进行".

如果是需要进行finalize()方法的对象,则会进入一个F-Queue的队列,并依次进入JVM自动建立的低优先级的Finalizer线程来执行.这里的"执行"指的仅仅是触发该对象的该方法,如果遇到死循环等执行缓慢的情况,则会自动跳出并执行下一个.稍后GC对F-Queue中的对象进行小规模的二次标记.拥有两次标记的对象再难逃被回收的命运了.

三. 垃圾收集的算法

上面我们以单个对象的角度来观察对象被回收前所发生的一系列的动作,接下来我们通过整体的垃圾收集器来观察这一块堆内存中进行垃圾收集的算法.

有三种实现垃圾收集机制的算法:标记-清除算法/复制算法/标记-整理算法.

标记-清除算法

标记需要回收的对象,然后将其直接清除.因为这样会产生很多的内存碎片,所以需要维护一个空闲列表来告诉JVM哪块部分是可用的,可以分配给新的对象.这是最基本的垃圾收集算法,后续的算法都是基于这种思路来改进的.
在这里插入图片描述这种算法的缺点也很明显:首先是空间问题,清除后产生的大量的内存碎片可能会导致占用较大的对象分配不到空间而提前进行一个垃圾收集.其次是效率问题:清除和标记的两个过程的效率都不是很高.

复制算法

将一整块堆内存空间分为大小相等的两块相同的区域,一次只使用一块.当这块区域被用完的时候,将这块区域中还存活的对象直接复制到另一块区域中去,并且清除这块内存,如此来保证内存空间的完整性.

在这里插入图片描述如此导致的问题:空间问题,每次只能使用半块内存,整整50%的内存都被浪费了,代价太大了.而在存活对象较多的区域(老年代),复制算法需要复制大量的对象,显然效率低下,这时候使用标记-整理算法较为合适.

标记-整理算法

通过标记找出需要回收的对象,并将仍旧存活的对象向一边移动,直接清除除了存活对象以外的内存.

在这里插入图片描述

四. 分代收集算法(Generation Collection)

分代收集算法的思想是在整合了上述算法的前提下,通过不同区域采用不同算法的方法来合理分配内存空间.

新生代 : 分为 eden/To Survivor/From Survivor3个区,使用复制算法.
老年代 : 使用标记-整理算法.

新生代中,98%的对象都是"朝生夕死"的,真正能够存活下来的对象少之又少,因此并不需要按照1:1的比例划分内存空间,而是将eden和Survivor按照8:1:1的比例划分.每次使用eden和一块Survivor空间,当内存不足的时候进行一次回收,将存活的对象复制到另一块Survivor上,最后清理掉eden和Survivor.而To和From没有明显的区别,在每次回收的时候轮流作为被复制的区域.

然而有的时候因为存活对象太多,也不能保证这10%的内存够用,所以会需要用到老年代进行分配担保.分配担保相当于在Survivor区不够用的时候,将存活对象直接分配到老年代中去.

老年代中,对象的存活率高,使用标记整理算法需要移动的次数不会很多.

五. 垃圾收集的内存分配和回收策略

在这里插入图片描述eden区

eden也就是伊甸的意思,是对象出生的地方.JVM在这里为新生对象分配内存,每当内存不足的时候,JVM就会发起一次Minor GC的小回收,速度快,范围小,仅局限在新生代.这样的MinorGC将执行一次复制算法,将eden区空闲出来,供JVM分配新的空间.如果启动了本地线程分配缓冲,则线程在各自的TLAB上进行分配.当对象过大或者survivor区没有足够空间存放对象时,少数的对象会直接进入老年代.

Survivor区

相当于eden区和老年代之间的一个缓冲区域,存活的对象在From和To中待够一定的时间才有进入老年代的资格(当然有的对象能够直接进老年代).每次执行Minor GC时,From和To的作用就会交替一次,用于存放下一次Minor GC时存活的对象,对象在这里必须存活大于15次小回收,才能够进入老年代.

为什么需要Survivor区?

如果没有survivor区,新生对象在一次Minor GC之后直接进入老年代,老年代很快就会被塞满,而大多对象其实并没有这么长的存活时间,这时候移入老年代,只会加重垃圾收集的负担.

survivor区的意义在于避免eden区的对象过多的涌入老年代,从而减少Major GC的次数.

为什么需要两个survivor区?

一个用不了,多个没必要.to和from区在每次的mimor GC中作筛选,将那些长久存活的对象留下来,得以进入老年代.让多数垃圾尽早清理出去.

old区

这里占据堆内存的2/3,只有在Major GC的时候才会进行垃圾回收,每次大回收都会牵动一次小回收,这就是"Stop-The-World".这里的对象存活率高,是采用标记-整理算法合适区域.

对象得以进入old区的方法:

  1. 大对象直接进入old区.需要连续内存空间的对象,不论是不是朝生夕死,都会直接进入老年代,避免了大对象在eden区和survivor这种发生大量的内存复制.写程序的时候应当避免的是产生大量朝生夕死的"短命大对象".
  2. 长期存活的对象将进入old区.对象的对象头中有一个GC分代年龄计数器,当对象出生和每经历一次Minor GC后存活,计数器+1,那些到了15岁仍旧存活的对象,有资格进入老年代.
  3. 动态对象年龄调节进入old区.为了调节不同程序的内存状况,当survivor区中相同年龄的对象所占空间总和大于survivor区一半时,大于等于该年龄的对象提前进入老年区.
  4. 分配担保进入old区.当一个MonorGC后存活的对象大于survivor区的时候,无法容纳的对象会直接进去老年区.

以上可知,大对象和分配担保对象是保送生.长期存活对象是刻苦生.动态对象进入是幸运晋级生.

参考资料:
  1. 《深入理解Java虚拟机》 周志明著
  2. 公众号:阿里巴巴中间件7月11日推文《咱们从头到尾说一次 Java 垃圾回收》
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值