Java虚拟机内存回收

1、怎么判断对象已死?

方法1:引用计数法

道理很简单,GC时查看对象的引用计数,如果引用计数为0,说明孤家寡人一个,回收掉就好了。然而引用计数在对付互相引用时无能为力:比如项羽和虞姬其实是互相联系在一起的,刘邦(GC)如果只看引用关系的话就会放过他们两个,但实际上这两个都应该被回收掉。

方法2:根搜索算法

GC Roots Tracing。简言之就是如果某对象没有任何引用链可以溯源到GC Roots,那么就回收之。

怎么算是引用?

如果reference类型的变量存储的是另一块内存的地址,就称这块内存代表着一个引用。但这种非黑即白的表述不够灵活,因为我们希望在内存还充裕的时候保持这些对象。JDK1.2之后引入了几种引用类型:

  • 强引用,如Object o = new Object()
  • Soft Reference,只有当堆内存即将溢出时,才会回收这些对象;如果回收了还是内存不足,才会爆OOM
  • Weak Reference,只能活到下一次GC,不管内存是否充裕(有啥用呢?还不是个死。)
  • Phantom Reference,目的是在对象被回收时获得系统通知。

我觉得我还可以再抢救一下

在GC Roots Racing中查出不可达的对象,是否直接就发通知书呢?实际上,虚拟机此时会做一个判断:这个对象是否需要被调用finalize方法。如果对象没有覆盖Object的finalize方法,或者覆盖了但是finalize方法已经被调用过了,那么虚拟机就会回收该对象。

如果对象覆盖了finalize方法但没有被调用过,那么虚拟机会将该对象挂到F-Q上;之后虚拟机会自动拉起一条低优先级的线程,来调用挂到F-Q上对象的finalize方法,如果对象在其finalize方法中将自己跟GC Roots又建立起了关系,那么恭喜,这个对象不会被回收掉,否则该对象会被GC二次标记,等死吧。 finalize方法在对象的生命周期里只会被调用一次,并且不保证一定执行完成,如果超时了就对不起了。

注意,不建议使用finalize方法,这只是个历史遗留的特性;如果有需要关闭外部资源,try-finaly是更好的选择。

方法区用得着回收吗

  • 废弃常量:如果常量池中的常量没有任何变量引用,那么会被回收
  • 无用的类:1、该类无任何实例;2、加载该类的class loader已经被回收;3、该类对应的java.lang.Class没有任何引用

在大量使用反射、动态代理等情况下,方法区回收还是很有必要的。可以通过-Xnoclassgc参数控制。

内存回收算法

1、标记-清除法

如前所述,GC在第一轮扫描时会标记(Mark)出来所有待回收的对象,然后再清除(Sweep)掉这些对象。弊端一是效率不高,而是会造成内存中有大量碎片,影响大内存对象的申请。

标记-清除法是最原始的回收算法。

2、复制法

复制法的出发点是为了提高效率、避免出现内存碎片,其实现方式为:将内存一分为二,对象申请内存时只使用其中一半,当GC时将活着的对象拷贝到另外一半内存,然后将现在使用的这段内存回收掉。 但复制法的代价太高了:内存只能用一半。实际上虚拟机在新生代的回收使用了复制法,但做了改进:由于新生代的内存在GC时可能只有极少数的对象还处于存活状态,所以复制目的地不需要“一半”这么大。HotSpot虚拟机以8:1的比例来分割内存,内存分为1块Eden区(8/10),2块Survivor区(各1/10),内存回收的时候将Eden+Survivor的存活对象拷贝到另一块Survivor区。如果不幸存活对象太多导致Survivor空间存储不下,虚拟机会从老年代进行分配担保。 这里有一个问题,为什么要设计2块Survivor区呢? 答案是很“显然”的,如果只有1块Survivor区,那么下次回收时,这块Survivor上还是有对象是活跃的,既然是复制法,那也只能复制到另外一块Survivor上了。考虑到Survivor空间不大,而更可能的是其对象在下次GC的时候需要被回收的可能性很高,所以也没有必要对Survivor区去做Mark-Sweep/Compact了。

3、标记-整理法

与Mark-Sweep类似,不同的是“清理”(Sweep)对付的是待回收的对象,而“整理”(Compact)是将存活对象向一端移动,然后清理掉端末尾的内存。 老年代适合这种场景。

4、分代回收

分代的出发点是:没有一种回收算法可以满足所有需求。对于新生代,由于存活率低,所以使用复制法;对于老年代,由于存活率高,所以使用Mark-Sweep/Compact法。

垃圾收集器

1、Serial收集器

单线程,GC时会暂停虚拟机中任务的运行,对于交互式的应用不够友好。

2、ParNew收集器

多线程,GC时也会暂停。可以作为新生代的收集器和老年代的CMS收集器搭配使用。

3、Parallel Scavenge收集器

与ParNew类似,但其关注点为提高吞吐量(即,是否更多的时间用来跑用户的任务),而ParNew更关注相应速度。另外一个重要区别是Parallel Scavenge收集器支持自适应,只需设置最大内存、期望的最短暂停时间或期望的吞吐量,由虚拟机自己检测具体的内存使用情况,自动调节。

4、Serial old收集器

老年代使用。

5、Parallel Old收集器

Parallel Scavenge的老年代版本,可以跟Parallel Scavenge搭配使用,高吞吐。

6、CMS收集器

Concurrent Mark Sweep。分为4个步骤:

  • 初始标记
  • 并发标记
  • 重新标记
  • 并发清理

其中,初始标记为stop the world,暂停了用户线程,但由于其只是标记GC Roots能到达的对象,所以速度很快;之后并发标记与用户线程一并运行Roots Racing;重新标记会并发执行,并暂停用户线程,目的是为了补并发标记中发生的变动;并发清理与用户线程一并运行。 CMS收集器的缺点:

  • CPU敏感,可能造成用户线程得到的CPU资源变少,降低响应速度
  • 无法处理浮动垃圾(即并发清理时同时运行的用户线程产生的待回收对象),所以不能等到老年代很满了的时候才做回收(68%)
  • 由于是Sweep,还是会有内存碎片,可能导致大对象申请时full GC

7、G1收集器

其思想是避免做全区的GC,而是将内存分为多个区域,优先回收垃圾最多的区域。

内存分配

对象优先分配到Eden区

通常对象new的时候会分配到Eden区,当Eden区内存不足时,会出发一次minor GC;如果仍然不足,则会出发内存分配担保机制,从老年代分配内存。

  • Minor GC:发生在新生代,由于新生代内存存活时间很短,所以Minor GC的频率很高。
  • Major GC:有时也叫作Full GC,发生在老年代。通常比Minor GC慢10倍。

大对象直接分配到老年代

典型的大对象如byte数组。应该尽量避免频繁的申请释放大对象。 由于新生代采用的是复制算法来回收内存,如果在GC的时候还有一些大对象不能被回收,那么只能拷贝到Survivor区,性能会很差,因此可以为虚拟机配置参数要求大于一定阈值的对象直接分配到老年代。 注意这是对于Serial OLD/ParNew OLD收集器来说的,对于Parallel Scavenge收集器不需要。

长期存活对象进入老年代

出发点是为了避免一些老人长期占据工作岗位,跟公务员退休类似。 新生代的对象在minor GC后进入Survivor区,每次minor GC成功存活的对象年龄+1,当年龄超过一定值(默认15),则转入老年代。

动态对象年龄判断

出发点跟上面的类似,避免大量老领导占据工作岗位,提前退休。 如果新生代的Survivor区,同样年龄的对象总的内存大小超过了Survivor区大小的一半,则将这些对象转入老年代。

内存分配担保

我们知道如果对象在复制到Survivor区时若Survivor空间不足,则会出发担保机制,将对象转入老年代;但老年代的能力也不是无限的,因此需要在minor GC时做一个是否需要Major GC 的判断:

  • 如果老年代的剩余空间 < 之前转入老年代的对象的平均大小,则触发Major GC
  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且允许担保失败,则直接Minor GC,不需要做Full GC
  • 如果老年代的剩余空间 > 之前转入老年代的对象的平均大小,并且不允许担保失败,则触发Major GC

出发点还是尽量为对象分配内存。但是一般会配置允许担保失败,避免频繁的去做Full GC。