JVM总结(二)JVM的垃圾回收策略和算法

Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的“高墙”,墙外面的人想进去,墙里面的人却想出来。

说明:本篇博客主要是针对《深入理解Java虚拟机》该书进行的总结和归纳。


本篇博客主要总结如下问题:

  1. 哪些内存需要回收?
  2. 如果判定一个对象已经成为垃圾,可以被回收?
  3. 如何回收?(JVM对垃圾收集有哪些算法)?
  4. 一个对象从创建到销毁,JVM是如何进行内存分配的,又是如何内存回收的?

一.哪些内存需要回收?

在上一篇JVM总结(一)Java内存区域划分中,已经讲解了JVM的内存划分,其中程序计数器、虚拟机栈和本地方法栈三个区域是线程私有的,这几个区域不需要过多考虑回收的问题,因为方法结束或线程结束时,内存自然就跟随着回收了。

所以真正需要重点关注的是堆和方法区部分的内存。这部分内存的分配和回收都是动态的,所以垃圾回收器所关注的主要是这部分的内存。

二.如果判定一个对象已经成为垃圾,可以被回收?

判定一个对象是否可回收常见的两种算法:引用计数法和根搜索算法。

引用计数法

原理是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器就加1;当引用失效时,计数器就减1;任何时刻计数器为0的对象就是不可能再被使用的。

但实际上,Java语言并没有选用该算法来管理内存。最主要的原因是因为它很难解决对象之间循环引用的问题。

举例:
A对象引用了B对象,B对象也引用了A对象,但除此之外再无其他引用指向这两个对象。实际上就是这两个对象组成了一对 垃圾,应该被回收。如果使用引用计数法,此时他们各自的计数器都不为0,所以无法通知GC收集器回收他们。

根搜索算法

根搜索法又叫可达性分析算法,Java使用该算法来判定一个对象是否存活。这种算法的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,就证明此对象是不可用的,可被回收的。
根搜索算法

如上图就很好的说明了根搜索算法的思想,就是通过GC Roots 为节点向下搜索,能被链上的蓝色对象说明是可用对象,而灰色圆圈代表的为不可用对象,是会在GC时可以被回收的对象。

哪些对象可以用来做“GC Roots”的对象呢?

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

  • 虚拟机栈中引用的对象。
  • 方法区中静态属性引用的对象。
  • 方法区中的常量引用的对象。
  • 本地方法栈中引用的对象。

不可用的对象在GC的时候就会被马上回收吗?

其实不然。根搜索算法中不可达的对象并非马上回收,这时候他们处于“缓刑”阶段。真正宣告一个对象死亡,至少要经历两次标记过程。

  1. 当获取到不可达对象后,它将会被第一次标记并进行一次筛选。

    筛选规则:
    1) 对象是否覆盖了finalize()方法,如果没有则这个对象会被标记为“即将回收”。
    2) 对象是否进行了finalize()方法,如果该对象已经执行了finalize()方法,则不会被再次执行,也会被标记为“即将回收”。

  2. 如果对象经过筛选,并判定有必要执行finalize()方法,那么这个对象会有移到一个F-Queue队列中,并由虚拟机自动建立的一个线程去触发finalize()方法(并不保证fianlize()方法执行完成)。稍后GC将对F-Queue中的对象进行第二次标记,如果此时对象还没有通过finalize()方法与GC Roots对象关联起来,则将它标记为“即将回收”。

值得注意的是:finalize()方法是不可用对象被回收的最后一根救命稻草,但是再实际的开发过程中,我们基本不会去重写这个方法,有些开发人员甚至不知道这个方法的存在。愿意是我们根本没必要在这个方法中来处理相关的业务,因为这个方法只会被执行一次,如果下一次GC还是为不可用对象,就命中筛选的第二条规则,也会被回收。所以,大家知道其中的原理就够了。尽量不要在fianlize()方法中搞事情。(当然如果你在工作中遇到了这种使用场景,可给我留言^_^)

三.JVM如何回收垃圾(JVM对垃圾收集有哪些算法)?

从上面的结论中,我们知道了如果JVM是如何判定一个对象为可回收的。那么它是怎么被回收的呢?

3.1 标记-清除(Mark-Sweep)算法

这是一种最基本的回收算法,分为两个阶段–“标记”,“清除”。首先根据根搜索算法,标记可被回收的对象,然后统一回收掉所有被标记的对象。

标记-清除法

如上图,从中我们可以看出来两个很明显的缺点,

  1. 效率不高,要到内存中去查找被标记的可回收对象。
  2. 标记清清除后会产生大量的内存碎片。空间的碎片太多会导致一个问题,当我有一个大的对象需要存储的时候,无法找到连续的内存空间,而不得不提前触发另一次的GC

3.2 复制(Copying)算法

为了解决效率问题,一种称为“复制”(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
复制算法

优点:
这种算法很明显处理了效率问题,只要将存活的对象复制到另一半,然后将已使用过的内存直接清理掉就可以了,并且不会有内存碎片的问题。
缺点:
其一,这种算法意味着,我每次都只能使用一半的内存空间,浪费的内存空间代价很大,是很明显的空间换时间的算法。其二,如果对象的存活率高,意味着对象要来回多次复制,效率就会降低。

3.3 标记-压缩(Mark-Compact)算法

为了提高内存空间的使用率,一种称为“标记-压缩(Mark-Compact)”(Copying)的收集算法出现了,它标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
标记-压缩算法

标记-压缩算法虽然效率没有复制算法高,但是它不需要额外内存对其进行空间担保,充分利用了内存空间,并且不会产生内存碎片。

3.4 分代收集算法(Generational Collection)

分代收集算法并没有什么新的思想,而是根据对象存活周期的不同将内存分为几块,再根据对象不同的存活周期使用不同的上面提及的算法进行处理。当前商业虚拟机的垃圾收集就是采用这种算法。

分代管理

如上图:JVM将堆内存分为两个区域,新生代和老年代。其中新生代中,每次垃圾收集时都会发现有大批的对象死去,只有少量的存活,那就选用复制算法,而老年代中因为对象存活率高,没有多余的空间担保,所以使用“标记-清除”算法,或者“标记-整理”算法来进行回收。

四.一个对象从创建到销毁,JVM是如何进行内存分配的,又是如何内存回收的?

上面已经讲了JVM主要是通过分代收集算法来回收垃圾的,但是一个对象创建出来,是放到哪个划分出来的内存中呢?其中对象的存活率又是如何来判定的呢?这里我们就必须知道JVM的内存分配策略。

严格意义上来说,内存的分配策略是由不同的GC收集器来决定的,但目前大都的GC收集器内存分配策略相似(G1除外)。所以在梳理GC收集器之前,我将这个内容的讲解前置。(当然这不是一种很严谨的做法,但是主要是为了使整个的的知识点串联起来^_^)

对象的分配,从大方向上来讲,就是在堆上分配(不考虑标量替换的栈上分配),而JVM将堆划分成了如图所示的几块区域。
GC策略

整个堆分为:新生代(Young Gen),老年代(Old Gen),方法区(又叫永久区Perm)。
其中:新生代又分为 Eden区和Survivors区,Survivors又分为s0(from区),s1(to区))
当然图中还遗漏了一个TLAB区,是JVM在内存新生代Eden Space中开辟的一小块区域,称作TLAB(Thread-local allocation buffer),由线程私有。

1.对象优先分配在线程的本地分配缓冲区

在前面我们提到,每个线程可以在堆中预先分配得到一片区域,作为本地线程分配缓冲区(TLAB)。当该线程执行时,有对象创建的话,就在该线程的TLAB中分配内存。当该线程的TLAB用完了才申请堆中的空闲内存。

Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。

2.堆中优先分配Eden

大多数情况下,对象都在新生代的Eden区中分配内存。而因为大部分的对象都是“朝生夕死”的,所以新生代又会频繁进行垃圾回收。当Eden区没有足够的空间的时候,会进行Minor GC(新生代GC,采用复制算法)。

3.大对象直接进入老年代

需要大量连续空间的对象,如:长字符串、数组等,会直接在老年代分配内存。这是因为,这样可以避免在新生代区频繁的GC时发生大量的内存赋值(新生代的GC是采用复制算法的)。

   虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接在老年代分配。

4.长期存活的对象“晋入”老年代

如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中(回顾复制算法)。

    对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代.如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
在MinorGC时,如果Eden和FromServivor中存活的对象在复制到ToServivor时放不下了,也会直接分配到老年代。

5.老年代垃圾回收

对象进入老年代之后,当进行MajorGC之后,如果对象为不可用,则会被回收。MarjorGC触发时常会伴随FullGC,但不是绝对的,不同的GC收集器表现不同(比如CMS)。关于这个后期会单独总结。MarjorGC往往使用“标记-清除”算法,或者“标记-整理”算法来进行回收。


《深入理解Java虚拟机:JVM高级特性与最佳实战》 周志明 著
Java对象内存分配策略
对象都是在堆上分配的吗?
JVM内存分配策略

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值