JVM GC垃圾回收机制

1,767 阅读6分钟

一、JVM GC概述

1、JVM GC回收哪个区域内的垃圾

JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。

2、JVM GC怎么判断对象可以被回收了

在Java程序中,当一个对象O被创建时,它被放在Heap里。当GC运行的时候,如果发现没有任何引用指向O, 它就会被回收以腾出内存空间,或者换句话说, 一个对象被回收, 必须满足以下条件:

  • 1.对象没有引用
  • 2.作用域发生未捕获异常
  • 3.程序在作用域正常执行完毕
  • 4.程序执行了System.exit()
  • 5.程序发生意外终止(被杀线程等)

综上所述:

  • 1.没有任何引用指向它
  • 2.GC

在Java程序中不能显式的分配和注销内存,因为这些事情JVM都帮我们做了,那就是GC。有些时候我们可以将相关的对象设置成null来试图显示的清除内存,但是并不是设置为null就会一定被标记为可回收,有可能会发生逃逸。将对象设置成null至少没有什么坏处,但是调用System.gc()会显著地影响系统性能,使用System.gc()时候并不是马上执行GC操作,而是会等待一段时间,甚至不执行,而且System.gc()如果被执行,会触发Full GC,这非常影响性能。

3、JVM GC什么时候执行

Eden区空间不够存放新对象的时候,执行Scavenge(Minro)GC。升到老年代的对象大于老年代剩余空间的时候执行Full(Major) GC。

4、每个空间的执行顺序

(1)绝大多数刚刚被创建的对象会存放在伊甸园空间(Eden)。

(2)在伊甸园空间执行第一次GC(Scanvage GC)之后,存活的对象被移动到其中一个幸存者空间 (Survivor)。

(3)此后,每次伊甸园空间执行GC后,存活的对象会被堆积在同一个幸存者空间。

(4)当一个幸存者空间饱和,还在存活的对象会被移动到另一个幸存者空间。然后会清空已经饱和的那个幸存者空间。

(5)在以上步骤中重复N次(N=MaxTenuringThreshold(年龄阀值设定,默认15))依然存活的对象,就会被移动到老年代。

从上面的步骤可以发现,两个幸存者空间,必须有一个是保持空的。如果两个两个幸存者空间都有数据,或两个空间都是空的,那一定是你的系统出现了某种错误。我们需要重点记住的是,对象在刚刚被创建之后,是保存在伊甸园空间的(Eden)。那些长期存活的对象会经由幸存者空间(Survivor)转存到老年代空间。也有例外出现,对于一些比较大的对象(需要分配一块比较大的连续内存空间)则直接进入到老年代,一般在年轻代空间不足的情况下发生。

5、有如下原因可能导致Full GC

  • 1.年老代被写满
  • 2.持久代被写满
  • 3.System.gc()被显示调用
  • 4.上一次GC之后Heap的各域分配策略动态变化

二、JVN GC调优

在学习Java GC 之前,我们需要记住一个单词:stop-the-world。它会在任何一种GC算法中发生。stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行。当stop-the-world发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。

经过n次垃圾回收存活的对象(这个n被称为年龄阀值,默认是15次)。老年代空间的构成其实很简单,它不像新生代空间那样划分为几个区域,它只有一个区域,里面存储的对象并不像新生代空间绝大部分对象都是朝闻道,夕死矣。这里的对象几乎都是从Survivor空间中熬过来的,它们绝不会轻易的狗带。因此,Full GC发生的次数不会有Scanvage GC那么频繁,并且做一次Full GC的时间比Scanvage GC 要更长(约10倍)。

所以GC调优主要是减少Full GC的触发次数,可以通过NewRatio(新生代与老年代的占比)控制新生代转 老年代的比例,也可通过MaxTenuringThreshold(年龄阀值设定)设置对象进入老年代的年龄阀值。

默认的所占空间比例年轻代(Young generation) :老年代(Old generation) = 1 : 2

默认新生代空间的分配:Eden : SurvivorA : SurvivorB = 8 : 1 : 1

总结:

  • 1.可以通过NewRatio控制新生代转老年代的比例
  • 2.通过MaxTenuringThreshold设置对象进入老年代的年龄阀值(默认是15次)。
  • 3.不要轻易调用System.gc()

三、JVM GC算法

1、复制算法

复制算法将内存划分为两个区间,使用此算法时,所有动态分配的对象都只能分配在其中一个区间(活动区间),而另外一个区间(空间区间)则是空闲的。

采用从根集合扫描,将存活的对象复制到空闲区间,当扫描完毕活动区间后,会将活动区间一次性全部回收。此时原本的空闲区间变成了活动区间。下次GC时候又会重复刚才的操作,以此循环。 复制算法在存活对象比较少的时候,极为高效,但是带来的成本是牺牲一半的内存空间用于进行对象的移动。

2、标记-清除算法

采用从根集合进行扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收,该算法不需要进行对象的移动,并且仅对不存活的对象进行处理,在存活的对象比较多的情况下极为高效,但由于标记-清除算法直接回收不存活的对象,并没有对还存活的对象进行整理,因此会导致内存碎片。

3、标记-整理算法

标记-整理算法是在标记-清除算法之上,又进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

采用从根集合扫描,对存活的对象进行标记,标记完毕后,再扫描整个空间中未被标记的对象进行直接回收, 将所有存活的对象往左端空闲空间移动,并更新对应的指针。虽然进行了对象的移动排序整理,因此成本更高,但却解决了内存碎片的问题。

4、总结

JVM为了优化内存的回收,使用了分代回收的方式,对于新生代内存的回收(Scavenge GC)主要采用 复制算法。而对于老年代的回收(Full GC),大多采用标记-整理算法。