我们知道,目前主流的虚拟机实现都采用了分代收集的思想,把整个堆区划分为新生代和老年代;新生代又被划分成 Eden 空间、 From Survivor 和 To Survivor 三块区域。
看书的时候有个疑问,为什么非得是两个 Survivor 空间呢?要回答这个问题,其实等价于:为什么不是0个或1个 Survivor 空间?为什么2个 Survivor 空间可以达到要求?
为什么不是0个 Survivor 空间?
这个问题等价于:为什么需要 Survivor 空间。我们看看如果没有 Survivor 空间的话,垃圾收集将会怎样进行:一遍新生代 gc 过后,不管三七二十一,活着的对象全部进入老年代,即便它在接下来的几次 gc 过程中极有可能被回收掉。这样的话老年代很快被填满, Full GC 的频率大大增加。我们知道,老年代一般都会被规划成比新生代大很多,对它进行垃圾收集会消耗比较长的时间;如果收集的频率又很快的话,那就更糟糕了。基于这种考虑,虚拟机引进了“幸存区”的概念:如果对象在某次新生代 gc 之后任然存活,让它暂时进入幸存区;以后每熬过一次 gc ,让对象的年龄+1,直到其年龄达到某个设定的值(比如15岁), JVM 认为它很有可能是个“老不死的”对象,再呆在幸存区没有必要(而且老是在两个幸存区之间反复地复制也需要消耗资源),才会把它转移到老年代。
总之,设置 Survivor 空间的目的是让那些中等寿命的对象尽量在 Minor GC 时被干掉,最终在总体上减少虚拟机的垃圾收集过程对用户程序的影响。
为什么不是1个 Survivor 空间?
回答这个问题有一个前提,就是新生代一般都采用复制算法进行垃圾收集。原始的复制算法是把一块内存一分为二, gc 时把存活的对象从一块空间(From space)复制到另外一块空间(To space),再把原先的那块内存(From space)清理干净,最后调换 From space 和 To space 的逻辑角色(这样下一次 gc 的时候还可以按这样的方式进行)。
我们知道,在 HotSpot 虚拟机里, Eden 空间和 Survivor 空间默认的比例是 8:1 。我们来看看在只有一个 Survivor 空间的情况下,这个 8:1 会有什么问题。此处为了方便说明,我们假设新生代一共为 9 MB 。对象优先在 Eden 区分配,当 Eden 空间满 8 MB 时,触发第一次 Minor GC 。比如说有 0.5 MB 的对象存活,那这 0.5 MB 的对象将由 Eden 区向 Survivor 区复制。这次 Minor GC 过后, Eden 区被清理干净, Survivor 区被占用了 0.5 MB ,还剩 0.5 MB 。到这里一切都很美好,但问题马上就来了:从现在开始所有对象将会在这剩下的 0.5 MB 的空间上被分配,很快就会发现空间不足,于是只好触发下一次 Minor GC 。可以看出在这种情况下,当 Survivor 空间作为对象“出生地”的时候,很容易触发 Minor GC ,这种 8:1 的不对称分配不但没能在总体上降低 Minor GC 的频率,还会把 gc 的时间间隔搞得很不平均。把 Eden : Survivor 设成 1 : 1 也一样,每当对象总大小满 5 MB 的时候都必须触发一次 Minor GC ,唯一的变化是 gc 的时间间隔相对平均了。
上面的论述都是以“新生代使用复制算法”这个既定事实作为前提来讨论的。如果不是这样,比如说新生代采用“标记-清除”或者“标记-整理”算法来实现幸存对象的移动,好像确实是只需要一个 Survivor 就够了。至于主流的虚拟机实现为什么不考虑采用这种方式,我也不是很清楚,或许有实现难度、内存碎片或者执行效率方面的考虑吧。
为什么2个 Survivor 空间可以达到要求?
问题很清楚了,无论 Eden 和 Survivor 的比例怎么设置,在只有一个 Survivor 的情况下,总体上看在新生代空间满一半的时候就会触发一次 Minor GC 。那有没有提升的空间呢?比如说永远在新生代空间满 80% 的时候才触发 Minor GC ?
事实上是可以做到的:我们可以设两个 Survivor 空间( From Survivor 和 To Survivor )。比如,我们把 Eden : From Survivor : To Survivor 空间大小设成 8 : 1 : 1 ,对象总是在 Eden 区出生, From Survivor 保存当前的幸存对象, To Survivor 为空。一次 gc 发生后:
1)Eden 区活着的对象 + From Survivor 存储的对象被复制到 To Survivor ;
2) 清空 Eden 和 From Survivor ;
3) 颠倒 From Survivor 和 To Survivor 的逻辑关系: From 变 To , To 变 From 。
可以看出,只有在 Eden 空间快满的时候才会触发 Minor GC 。而 Eden 空间占新生代的绝大部分,所以 Minor GC 的频率得以降低。当然,使用两个 Survivor 这种方式我们也付出了一定的代价,如 10% 的空间浪费、复制对象的开销等。
分享到:
相关推荐
- **为什么需要两个Survivor区**:使用两个Survivor区可以有效避免内存碎片化的问题。在一个Survivor区情况下,Eden区和当前Survivor区内的对象在进行Minor GC时可能会导致内存碎片化。 - **两个Survivor区解决碎片...
2. 新生代(Young Generation):新生代进一步划分为Eden区和两个Survivor区(From空间和To空间)。大部分对象在Eden区创建,经历Minor GC后存活的对象会被转移到Survivor区。 3. 老年代(Old Generation):长期...
* -XX:SurvivorRatio=n: 年轻代中 Eden 区与两个 Survivor 区的比值 * -XX:MaxPermSize=n: 设置持久代大小 收集器设置: * -XX:+UseSerialGC: 设置串行收集器 * -XX:+UseParallelGC: 设置并行收集器 * -XX:+...
年轻代分为Eden和两个Survivor区,新生对象在Eden中生成,当Eden满时,存活对象会被复制到Survivor区,经过多次复制后进入老年代。持久代主要存储静态文件和元数据。 垃圾回收分为Scavenge GC和Full GC。Scavenge ...
堆内存进一步细分为新生代和老年代,新生代又分为Eden区和两个Survivor区,用于进行不同的垃圾回收策略。栈内存则由多个线程栈组成,每个线程有一个独立的栈,用于存储其方法调用帧。 其次,类加载机制是JVM的重要...
在HotSpot JVM中,新生代进一步细分为一个Eden区和两个Survivor区(通常标记为From和To),默认比例为8:1。新创建的对象首先被放置在Eden区,经过第一次垃圾回收后,存活的对象会被移动到一个Survivor区。之后每次...
新生代用于存放新创建的对象,通常划分为Eden区和两个Survivor区(From Survivor区和To Survivor区)。老年代用于存放经过多次垃圾回收仍然存活的对象。 内存分配指的是当应用程序创建对象时,JVM会为对象分配内存...
新生代又细分为Eden空间和两个Survivor空间(S0和S1),主要用来存放新创建的对象;老年代存放生命周期较长的对象。 - **PermGen Space与Metaspace**:在早期版本的JVM中,持久代(PermGen Space)用于存放类的元...
堆被划分为新生代(Young Generation)和老年代(Old Generation)两个部分,进一步细分为Eden空间、Survivor空间(From和To)。 - **运行时常量池(Runtime Constant Pool)**:每个类都有一个运行时常量池,它是...
新生代分为Eden和两个Survivor空间(From与To)。新生代对象首先在Eden区域分配,当进行Minor GC时,存活的对象会尝试移动到Survivor空间。这个过程称为晋升。目的是减少直接进入老年代的对象数量,从而降低Major GC...
堆被进一步划分为新生代(Young Generation)和老年代(Tenured/Old Generation),新生代又包括Eden区和两个Survivor区(From、To)。 2. **方法区(Method Area)**:也称为永久代或元空间,存储类信息、常量、...
例如,`-XX:SurvivorRatio=8`表示每个Survivor区是Eden区的1/8。这有助于控制对象晋升到老年代的速度。 4. **-XX:MaxTenuringThreshold**: 设置对象从新生代晋升到老年代的最大年龄。默认值为15,表示对象在...
新生代(Young Generation)包括Eden区和两个Survivor区(From和To),采用复制算法进行垃圾回收。老年代(Tenured Generation)使用标记-整理或标记-压缩算法。垃圾回收器有Serial、Parallel、CMS和G1等,各有优...
新域进一步划分为Eden区和两个辅助生存空间(Survivor Spaces,通常称为From和To空间)。新生成的对象首先分配在Eden区,当Eden空间不足时,GC启动Minor GC,将存活的对象复制到Survivor空间,然后清理Eden。在多次...
新生代又被细分为Eden区和两个Survivor区(From Space和To Space)。 **3.2 Java栈** 每个线程创建时都会为其创建一个独立的栈,用于存储局部变量、操作数栈、动态链接、方法出口等信息。线程私有的,生命周期与...
年轻代主要用于存放新生的对象,它由Eden和两个Survivor区组成,新生对象首先在Eden区分配内存,当Eden满时,经过垃圾回收未被引用的对象被清除,其余存活的对象被复制到Survivor区。当Survivor区也无法容纳时,这些...
例如,`-XX:SurvivorRatio=6`表示eden区与两个survivor区总和的比例为6:2。 7. **-Xss**: 设置每个线程的栈大小。例如,`-Xss512k`表示每个线程的栈大小为512KB。 #### 二、垃圾回收相关参数 1. **-XX:+...
新生代又细分为一个Eden区和两个Survivor区(S0、S1),默认比例为8:1:1。 - **Eden区**:所有新创建的对象首先都是在这里分配。 - **Survivor区**:当Eden区满后,还存活的对象会被移动到Survivor区之一。 - **...