为什么要分代
分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在Java程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如Http请求中的Session对象、线程、Socket连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。因此,分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
如何分代
图片
如图所示:
虚拟机中的共划分为三个代:年轻代(Young Generation)、年老点(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是Java类的类信息,与垃圾收集要收集的Java对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
对象分类
这种算法并不是一种新的算法,而是根据对象的存活周期的不同而将内存分为几块,分别为新生代、老年代和永久代。
新生代:朝生夕灭的对象(例如:方法的局部变量等)。
老年代:存活得比较久,但还是要死的对象(例如:缓存对象、单例对象等)。
永久代:对象生成后几乎不灭的对象(例如:加载过的类信息)。
内存区域
回想一下之前jvm对内存的划分,我们可能就已经猜到了,新生代和老年代都在java堆,永久代在方法区。
java堆对象的回收
现在,我们来看看分代收集算法是如何针对堆内存进行回收的。
新生代:采用复制算法,新生代对象一般存活率较低,因此可以不使用50%的内存作为空闲,一般的,使用两块10%的内存
作为空闲和活动区间,而另外80%的内存,则是用来给新建对象分配内存的。一旦发生GC,将10%的活动区间与另外80%中存
活的对象转移到10%的空闲区间,接下来,将之前90%的内存全部释放,以此类推,下面还是用一张图来说明:
解释下,堆大小=新生代+老年代,新生代与老年代的比例为1:2,新生代细分为一块较大的Eden空间和两块较小的Survivor空间,分别被命名为from和to。
老年代:老年代中使用“标记-清除”或者“标记-整理”算法进行垃圾回收,回收次数相对较少,每次回收时间比较长。
方法区对象回收
永久代指的是虚拟机内存中的方法区,永久代垃圾回收比较少,效率也比较低,但也必须进行垃圾回收,否则永久代内存
不够用时仍然会抛出OutOfMemoryError异常。永久代也使用“标记-清除”或者“标记-整理”算法进行垃圾回收。
什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC有两种类型:Scavenge GC和Full GC。
Scavenge GC
一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden去能尽快空闲出来。
Full GC
对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个对进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。在对JVM调优的过程中,很大一部分工作就是对于FullGC的调节。有如下原因可能导致Full GC: