JVM 系列文章之 GC 算法浅析

Java的堆结构

再介绍垃圾回收算法之前,先来看看 Java中的堆,Java里的堆指的是用于存放 Java 对象的内存区域。JVM的堆被同一个JVM实例中所有的Java线程共享,它通常由某种自动管理机制所管理,这种机制通常叫做”垃圾回收”

在Java 中,堆被分为两个不同的区域: 新生代(Young),老年代(Old)。新生代又被划分为三个区域: Eden,From Survivor,To Survivor。在 JDK 1.7及以前,堆的内存模型大致为:
JVM 系列文章之 GC 算法浅析

  • 新生代:Young Generation,主要用来存放新生的对象
  • 老年代: Old Generation,主要存放应用程序声明周期长的内存对象
  • 永久代: 在Hotspot中,永久代又称为方法区,主要存放Class和Meta信息。

在JDK 1.8及以后,永久代被彻底移除,堆的内存模型大致如下:
JVM 系列文章之 GC 算法浅析

这种情况下,堆大小 = 新生代 + 老年代。其中堆的大小可以通过参数 -Xms,-Xmx来指定。

默认的,新生代(Young) 与老年代(Old)的比例的值是 1:2 (该值可以通过参数 -XX: NewRatio来指定),即: 新生代(Young) = 1/3的堆空间大小,老年代(Old) = 2/3的堆空间大小。

其中,新生代(Young)被细分为 Eden 和 两个 Survivor区域,这两个 Survivor区域分别被命名为 from 和 to,以示区分。

默认的,Eden:from:to = 8:1:1 (可以通过参数 -XX: SurvivorRatio来设定),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。

JVM每次只会使用 Eden和其中的一块 Survivor区域来为对象服务,所以无论什么时候,总有一块 Survivor区域是空闲着的,新生代实际可用的内存空间为 90% 的新生代空间。

标记 - 清除算法

在GC算法中,最简单的就是 “标记-清除”(Mark-Sweep)算法。它的原理比较简单,首先根据可达性分析算法对不可达对象进行标记,在标记完成后统一回收所有被标记的对象。标记-清除算法的执行过程如下图:
JVM 系列文章之 GC 算法浅析
标记-清除算法有两个缺点:
- 效率问题,标记和清除两个过程的效率都不高
- 空间问题,标记清除之后产生大量不连续的内存碎片,如果这时候有大对象需要连续的内存空间进行分配时,很可能会因为没有足够的连续内存空间而又触发一次 GC

基于Mark-Sweep的GC 多用于老年代

复制算法

复制算法的思路是它将可用内存按容量划分为大小相等的两块,每次只用其中的一块。当这块内存用完了,就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样每次都是对半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可。但是这种算法是用空间换时间,代价是将内存缩小为原来的一半,代价很高。而新生代的对象一般是存活时间较短的对象,GC频率较高,占内存较少,因此新生代一般都采用基于复制的GC。复制算法过程如下:
JVM 系列文章之 GC 算法浅析

HotSpot 虚拟机将新生代内存分为 一块较大的 Eden空间和两块较小的 Survivor空间,Eden和Survivor的大小比例是8:1。每次新生代中可用内存空间为整个新生代容量的 90%。我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survvivor 空间不够用时,需要依赖老年代进行分配担保

标记 - 整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率会变低,它比较适合收集新生代对象,至于老年代这种一般不选用复制算法。根据老年代的特点,可以使用 “标记-整理”算法或者”标记-清除”算法

标记 - 整理算法可以解决内存碎片的问题,而且思路也比较简单,它的思想就是,让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存,如下图所示:

JVM 系列文章之 GC 算法浅析

分代收集

当前商业虚拟机的垃圾收集都采用”分代收集”,将堆分为新生代和老年代,根据各个年代的特点采用最适当的收集算法:
- 新生代
- 复制收集算法
- 老年代
- 标记 - 清理算法
- 标记 - 整理算法

JVM堆内存设置参数

  • -XX:+ 启用选项 例如:-XX:+PrintGCDetails启动打印GC信息的选项,其中+号表示true,开启的意思
  • -XX:-不启用选项 ,例如:-XX:-PrintGCDetails关闭启动打印GC信息的选项,其中-号表示false,关闭的意思
  • -XX:=
  • -XX:=

常用堆参数

  • -Xms: 初始堆大小
  • -Xmx: 最大堆大小,默认为物理内存的1/4
  • -Xmn: 新生代大小,通常为 Xmx的 1/3或1/4。新生代 = Eden + 2个Survivor空间。实际可用空间为 = Eden + 1个 Survivor,即90%
  • -XX:NewSize = n:设置新生代大小
  • -XX:NewRatio = n: 设置新生代和老年代的比值,如 n = 3,表示新生代:老年代 = 1:3。
  • -XX:SurvivorRatio: 新生代中 Eden与Survivor的比值,默认值为 8。即Eden占新生代空间的 8/10,另外两个 Survivor各占 1/10
  • -XX:PermSize: 永久代(方法区)的初始大小,(前提是永久代存在的情况下,在JDK 1.8及以后,永久代被移除了)
  • -XX:MaxPermSize:永久代(方法区)的最大值
  • -XX:+PrintGCDetails:打印 GC 信息
  • -XX:+HeapDumpOnOutOfMemoryError:让虚拟机在发生内存溢出时 Dump 出当前的内存堆转储快照,以便分析用

更多JVM参数选项设置,请参考Oracle官方网站给出的相关信息: http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

参考资料 & 鸣谢