JVM之垃圾回收算法

185 阅读8分钟

1.虚拟机中哪些区域需要垃圾回收

在虚拟机的内存结构中有提到堆是内存回收的主要区域,其次是方法区,但是方法区可以设置不进行垃圾回收.那其他剩下的三个区域虚拟机栈,本地方法栈,程序计数器需不需要进行垃圾回收呢,答案是不需要,因为其他的三个区域是线程私有的,会随着线程的结束而消亡,不需要过多考虑垃圾回收的问题.

2.垃圾回收的对象有哪些

既然是在堆和方法区中进行垃圾回收,那么回收的对象当然是堆和方法区存储的内容了

对于堆来说,垃圾回收的对象就是堆中存储的对象了,但是并不是所有的对象都会被回收掉,而是对于垃圾回收器来说,需要回收的是已死的对象,那么什么样的对象可以被称为已死的对象呢,这里我们主要有两种方式判断对象是否已死:引用计数器和根搜索:

引用计数器:

在每一个对象中都有一个reference变量来计算该对象被多少地方引用,每当该对象被引用时,变量就+1,当引用失效时,变量就-1,那么当引用为reference变量为0时,则判断该对象已死,垃圾回收器就可以回收该对象了.但是引用计数器有一个缺点就是当两个对象互相引用对方时,即使这两个对象没有被其他任何地方引用,也是不会被垃圾回收的.

根搜索法:

有一类对象可以被看作GCRoots,从这些对象依次往下搜索被它引用的对象,搜索的路径我们这里成为引用链,处在引用链中的对象就是可以存活的对象,反之当一个对象和GCRoots之间没有任何引用链时连接时,就说明该对象已死,可以被垃圾回收器回收.

  • 这里还要特别说明一下,即时被上面的方法判断为可以被回收,但是也不是一定被回收,当被判断回收时,会为对象打上一次标记,Object类中有一个finalize方法,在垃圾回收之前会执行这个方法,执行这个方法后会再次给对象打标记,如果对象被再次打上了标记,则就会被回收.所以如果这个类重写了finalize方法,并且在这个方法中再次将对象关联到某个存活的对象上,那么对象就不会被回收.但是finalize方法只会执行一次,也就是说如果已经执行过一次了,就不会再执行了. image.png

如上图所示,对象1,对象2,对象3就是可以存活的对象,对象4和对象5时需要被垃圾回收的对象.

能作为GCRoots的对象主要有以下几种:

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

对于方法区来说主要回收的对象就是废弃常量和无用的类信息,那么什么样的常量才能叫废弃常量呢,其实和堆中类回收类似,如果常量池中的一个字面量没有被任何地方引用,则说明这个字面量是废弃的字面量需要被回收.

类的回收则需要满足以下几个条件:

  1. 该类对应的对象都被回收
  2. 加载该类的类加载器被回收
  3. 该类的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射机制访问该类

3.垃圾回收算法

标记-清除法:

标记-清除法从算法名称上就能知道,先要标记需要清除的类,然后再进行清除.但是标记清除法存在一些缺点,因为堆中的需要被清除的对象都是无规则分布的,那么在对这些对象进行清除后就会产生很多的不连续的零碎内存,那么当一个比较大的对象要存储在堆中时,就可能会出现无法分配对应大小的连续内存来存储这个对象的情况,出现这种情况后就会触发堆内存的垃圾回收,需要运行垃圾回收线程,从而影响工作线程的运行,影响程序的执行的效率.

复制法:

复制法是将内存分为两个区域,使用的时候只用其中一个区域,在进行垃圾回收时,先将可以继续存活的对象复制到未使用的区域,再对已使用的进行整体的垃圾回收.复制法避免了标记-清楚发的零碎内存问题,但是也有自己的缺点,就是只能使用原有内存的一半,那么在运行大型的程序时内存压力就会比较大,可能会影响程序的运行.还有如果有些对象可以长期存活,那么复制法会将这些对象在内存上复制来复制去,也会影响效率.

标记-整理法:

标记-整理法就是将可以存活的对象移到内存的一端,对剩下的内存进行垃圾回收,这样既可以避免零碎空间的问题,也可以避免长期存活的对象被复制来复制去的问题,因为这些在上一次垃圾回收时对象已经处在内存的一端了,本次垃圾回收时不需要移动.

通过以上垃圾回收算法我们不难发现,垃圾回收和对象的生命周期有着很大的关联,不同的生命周期需要选择不同的垃圾回收算法.

4.堆中的垃圾回收

堆作为垃圾回收的主要区域正是实现了区分不同生命周期来进行特定的回收的思想,堆中根据不同的生命周期划分成了新生代和老年代两个区域,并且根据这两个区域不同的生命周期实现了不同的算法.

  • 新生代中的对象绝大多数生命周期都十分短暂,极少数能够长期存活
  • 而老年代中的对象生命周期都比较漫长,能够进入老年代的对象,大多数都是经过多轮垃圾回收依旧存活的.

那么依据上述的新生代老年代特点,实现什么算法就一目了然了,新生代实现的就是复制法,而老年代实现的就是标记-整理法了.

新生代又被分为Eden区和两个survivor区,在使用时只使用Eden区和其中一个survivor区,当要进行垃圾回收时,先将可以存活的对象复制到另一个没有使用的survivor区中,然后对正在使用的Eden区和survivor区进行垃圾回收,Eden区和两个survivor区的内存占比为8:1:1,新生代和老年代占堆大小分别为1/3和2/3.

那么假设新生代中有一个对象可以长期存活,那是不是每次垃圾回收后都会被复制到另一个没有使用的survivor中,一直在两个survivor中换来换去呢?其实不是的,每个对象都有一个计数器,每当进行一次垃圾回收时,计数器的值都会加一,当计数器的值达到我们设定的阈值后,也就是说这个对象在经过多次垃圾回收依旧存活后,将会直接被放入老年代中存储,假设计数器的阈值设置为15,那么这个对象在经历15垃圾回收后,直接放入老年代.在这15次回收中,第1次放入survivor中,剩下14次则是在两个survivor中换来换去了,除了长期存活的对象可以进入老年代,还有当survivor和Eden经过垃圾回收后依旧无法存储新到来的对象,则将此对象直接放入老年代中,若老年代经过垃圾回收后依旧无法存放,则会出现OOM.

  • 使用-XX:MaxTenuringThreshold命令来设置对象进入老年代的阈值,Hotspot默认是15
  • 用-XX:PretenureSizeThreshold来控制直接升入老年代的对象大小,大于这个值的对象会直接分配在老年代上
  • 用-XX:+UseAdaptiveSizePolicy开关来控制是否采用动态控制策略,如果动态控制,则动态调整Java堆中各个区域的大小以及进入老年代的年龄

5.什么时候进行垃圾回收

  • 当新生代因为内存不足无法存入新的对象时,会触发新生代的垃圾回收,新生代的垃圾回收也被称为MinorGC.
  • 当新生代垃圾回收后依旧无法存储新来的对象,则尝试将对象存入老年代.
  • 当老年代内存不足时,会触发FullGC,对整个堆进行整理,包括新生代和老年代。Full GC因为需要对整个堆进行回收,所以比Minor GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。