JVM垃圾回收与内存分配策略

Java 技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:

  • 给对象分配内存
  • 回收分配给对象的内存

堆区内存结构图(JDK1.8 之后移除了永久代,GC 只需要对堆区的新生代和老年代负责)
在这里插入图片描述
其中,Eden 区(就是伊甸园的意思,所有对象的起源,在后面介绍分配策略的时候你会更加明白为什么这么命名),From 区和 To 区是两块 Survivor 区(幸存者空间)。

相关性能调优参数:

  • -XX:SurvivorRatio:Eden 和 Survivor 的比值,默认8:1
  • -XX:NewRatio:老年代和新生代大小的比值,比如取 2,那么老年代的空间大小为新生代的 2 倍,就像我们上图所示的那样。
  • -Xms/-Xmm:设置堆区总内存大小,-Xms 设置初始值,-Xmm 设置可动态扩展的最大值。

一、垃圾回收策略

 
主要是如下这两种垃圾回收策略:

  • Minor GC回收新生代,因为新生代对象存活时间很短,具有朝生夕死的特性,因此 Minor GC频繁执行执行的速度一般也会比较。(使用复制算法
  • Full GC回收老年代和新生代,老年代对象其存活时间长,因此 Full GC 很少执行执行速度会比 Minor GC 很多。(使用标记 - 清除或者标记 - 整理算法

我画了几张图,简单的演示一下 Minor GC 的过程,这里我们先做几个假设以方便讨论:

  • 新对象都是在 Eden 上进行分配
  • 两块 Survivor 区与 Eden 区的大小比例都是 1:8 ,但是为了方便作图,图中大小比例并不准确
  • 不会发生空间分配担保(这个后面介绍到分配策略的时候你就知道了)
  • 新生代晋升到老年代的年龄阈值为 3 岁(这个后面介绍到分配策略的时候你就知道了)

注:下面图中两块 Survivor 区的 From 和 To 标识是来回交换的,我这里画图给忘了,但是真的是懒得再改了,怪麻烦的,给各位看官抱个歉。。。

假设我们现在的程序正在运行,在这个时间节点之前还未发生任何 GC,遇到一个待分配内存的对象,但是 Eden 空间不足了。

如下所示

在这里插入图片描述
此时触发 Minor GC ,将存活对象复制到一块 Survivor 上,之后清理 Eden 区所有对象。此外,对于存活对象,由于其扛过了一次 Minor GC,所以它的年龄会增加一岁(一次 Minor GC,相当于人类世界的一年),对应于图中的数字由 0 变为 1
在这里插入图片描述
程序又运行了一会儿,Eden 区再一次的被填满了
在这里插入图片描述
此时会再一次的触发 Minor GC,它会将 Eden 区和 From Survivor 区中的存活对象拷贝到另一块空闲的 Survivor 区中(图中的 To Survivor),然后干掉 Eden 区和 From Survivor 区中的所有对象。

如下所示

之后,又没地方了,但是上图中 To Survivor 区中有个 2 岁的对象还活着,我们在最开始假设的时候曾说过,3 岁是新生代晋升老年代的标识,那么这个对象将被移动到老年代空间,其他的过程同上。

在这里插入图片描述
Minor GC 后
在这里插入图片描述

扫描二维码关注公众号,回复: 6710609 查看本文章

Full GC 的触发条件

对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:

1. 调用 System.gc()

只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。

2. 老年代空间不足

老年代空间不足的常见场景为大对象直接进入老年代、长期存活的对象进入老年代等。

为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。

3. 空间分配担保失败

使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。

4. JDK 1.7 及以前的永久代空间不足

在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据。

当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。

为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。

这也是为什么 JDK1.8 之后要使用元空间替代永久代的原因(降低 Full GC 的频率)。

5. Concurrent Mode Failure

执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。

二、内存分配策略

1. 对象优先在 Eden 分配

大多数情况下,对象在新生代的 Eden 区上分配(所以取名叫 Eden,伊甸园,所有对象的起源,但在 JVM 内存分配策略中,只能说是大多数对象在此出生),当 Eden 空间不够时,发起 Minor GC。
 

2. 大对象直接进入老年代

大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。

经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。

相关性能调优参数:-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 和 Survivor 之间的大量内存复制。

3. 长期存活的对象进入老年代

为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中,默认 15 岁。

相关性能调优参数:-XX:MaxTenuringThreshold 用来定义年龄的阈值。
 

4. 动态对象年龄判定

虚拟机并不是永远要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
 

5. 空间分配担保

在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。

如果不成立的话虚拟机会查看 HandlePromotionFailure 的值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 的值不允许冒险,那么就要进行一次 Full GC。

猜你喜欢

转载自blog.csdn.net/u013568373/article/details/94376999