一、为什么要分代
分代的垃圾回收策略,基于一个事实:不同的对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。
在 Java 程序运行的过程中,会产生大量的对象,其中有些对象是与业务信息相关,比如 Http 请求中的 Session 对象、线程、Socket 连接,这类对象跟业务直接挂钩,因此生命周期比较长。但是还有一些对象,主要是程序运行过程中生成的临时变量,这些对象生命周期会比较短,比如:String 对象,由于其不变类的特性,系统会产生大量的这些对象,有些对象甚至只用一次即可回收。
试想,在不进行对象存活时间区分的情况下,每次垃圾回收都是对整个堆空间进行回收,花费时间相对会长,同时,因为每次回收都需要遍历所有存活对象,但实际上,对于生命周期长的对象而言,这种遍历是没有效果的,因为可能进行了很多次遍历,但是他们依旧存在。分代垃圾回收采用分治的思想,进行代的划分,把不同生命周期的对象放在不同代上,不同代上采用最适合它的垃圾回收方式进行回收。
二、如何分代

虚拟机中的共划分为三个代:年轻代(Young Generation)、年老代(Old Generation)和持久代(Permanent Generation)。其中持久代主要存放的是 Java 类的类信息,与垃圾收集要收集的 Java 对象关系不大。年轻代和年老代的划分是对垃圾收集影响比较大的。
2.1、年轻代
所有新生成的对象首先都是放在年轻代的。年轻代的目标就是尽可能快速的收集掉那些生命周期短的对象。年轻代分三个区。一个 Eden 区,两个 Survivor 区 (一般而言)。大部分对象在 Eden 区中生成。当 Eden 区满时,还存活的对象将被复制到 Survivor 区(两个中的一个),当这个 Survivor 区满时,此区的存活对象将被复制到另外一个 Survivor 区,当这个 Survivor 区也满了的时候,从第一个 Survivor 区复制过来的并且此时还存活的对象,将被复制 年老区 (Tenured) 。需要注意,Survivor 的两个区是对称的,没先后关系,所以同一个区中可能同时存在从 Eden 复制过来的对象,和从前一个 Survivor 复制过来的对象,而复制到年老区的只有从第一个 Survivor 区过来的对象。而且,Survivor 区总有一个是空的。同时,根据程序需要,Survivor 区是可以配置为多个的(多于两个),这样可以增加对象在年轻代中的存在时间,减少被放到年老代的可能。
新生代有划分为 Eden、From Survivor 和 To Survivor 三个部分,他们对应的内存空间的大小比例为 8:1:1,也就是,为对象分配内存的时候,首先使用 Eden 空间,经过 GC 后,没有被回收的会首先进入 From Survivor 区域,任何时候,都会保持一个 Survivor 区域(From Survivor 或 To Survivor)完全空闲,也就是说新生代的内存利用率最大为 90%。From Survivor 和 To Survivor 两个区域会根据 GC 的实际情况,进行互换,将 From Survivor 区域中的对象全部复制到 To Survivor 区域中,或者反过来,将 To Survivor 区域中的对象全部复制到 From Survivor 区域中。
2.2、年老代
在年轻代中经历了 N 次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
GC 过程中,当某些对象经过多次 GC 都没有被回收,可能会进入到年老代。或者,当新生代没有足够的空间来为对象分配内存时,可能会直接在年老代进行分配。
2.3、持久代
用于存放静态文件,如今 Java 类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些 class,例如 Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。持久代大小通过 -XX:MaxPermSize=<N> 进行设置。
永久代实际上对应着虚拟机运行时数据区的 “方法区”,这里主要存放类信息、静态变量、常量等数据。一般情况下,永久代中对应的对象的 GC 效率非常低,因为这里的的大部分对象在运行都不要进行 GC,它们会一直被利用,直到 JVM 退出。
三、什么情况下触发垃圾回收
由于对象进行了分代处理,因此垃圾回收区域、时间也不一样。GC 有两种类型:Scavenge GC 和 Full GC。
一般情况下,当新对象生成,并且在 Eden 申请空间失败时,就会触发 Scavenge GC,对 Eden 区域进行 GC,清除非存活对象,并且把尚且存活的对象移动到 Survivor 区。然后整理 Survivor 的两个区。这种方式的 GC 是对年轻代的 Eden 区进行,不会影响到年老代。因为大部分对象都是从 Eden 区开始的,同时 Eden 区不会分配的很大,所以 Eden 区的 GC 会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使 Eden 去能尽快空闲出来。
对整个堆进行整理,包括 Young、Tenured 和 Perm。Full GC 因为需要对整个块进行回收,所以比 Scavenge GC 要慢,因此应该尽可能减少 Full GC 的次数。在对 JVM 调优的过程中,很大一部分工作就是对于 FullGC 的调节。有如下原因可能导致 Full GC:
年老代(Tenured)被写满。
持久代(Perm)被写满。
System.gc ()被显示调用。上一次 GC 之后 Heap 的各域分配策略动态变化。
评论