Java 堆是垃圾收集器主要的回收区域。
Java 堆分新生代和老年代,分别占堆空间的三分之一和三分之二。
新生代里边又分 Eden 区和 Survivor 区。Survivor 区又分 Survivor From 和 Survivor To。Eden 区占新生代 80%,Survivor 中的 From 和 To 各占 10%。支持参数调整 -XX:SurvivorRatio=8。
Eden 区
中文翻译过来叫伊甸园,是对象起初创建的区域。Eden 区内的对象绝大部分是会被回收的。当 Eden 区没有足够空间时,虚拟机会发起一次 Minor GC,Minor GC 相比 Major GC 更频繁,回收速度也更快。
通过 Minor GC 之后,Eden 会被清空,Eden 区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到 Survivor 的 From 区(若 From 区不够,则直接进入 Old 区)。
Survivor 区
中文翻译过来叫 存活区,是 Eden 区和 Old 区的一个缓冲区。Survivor 区分 Survivor From 区和 Survivor To 区。
为什么需要这么一个缓冲区?假设 Eden 区每进行一次 Minor GC,就把存活的对象移动到老年代,很快老年就会被填满。而往往很多对象虽然在第一次 Minor GC 的时候没有被回收,但有可能第二次第三次就被回收了。
所以,Survivor 的存在意义就是减少被送到老年代的对象,进而减少 Major GC 的发生。Survivor 的预筛选保证,只有经历16次 Minor GC 还能在新生代中存活的对象,才会被送到老年代。
那为啥要两个呢?主要是解决内存碎片化的问题。复制算法就是解决内存碎片化的问题。
我们先假设一下,Survivor 如果只有一个区域会怎样。Minor GC 执行后,Eden 区被清空了,存活的对象放到了 Survivor 区,而之前 Survivor 区中的对象,可能也有一些是需要被清除的。问题来了,这时候我们怎么清除它们?在这种场景下,我们只能标记清除,而我们知道标记清除最大的问题就是内存碎片,在新生代这种经常会消亡的区域,采用标记清除必然会让内存产生严重的碎片化。因为 Survivor 有2个区域,所以每次 Minor GC,会将之前 Eden 区和 From 区中的存活对象复制到 To 区域。第二次 Minor GC 时,From 与 To 职责对换,这时候会将 Eden 区和 To 区中的存活对象再复制到 From 区域,以此反复。
这种机制最大的好处就是,整个过程中,永远有一个 Survivor space 是空的,另一个非空的 Survivor space 是无碎片的。那么,Survivor 为什么不分更多块呢?比方说分成三个、四个、五个?显然,如果 Survivor 区再细分下去,每一块的空间就会比较小,容易导致 Survivor 区满,两块 Survivor 区可能是经过权衡之后的最佳方案。
Old 区
老年代占据着2/3的堆内存空间,只有在 Major GC 的时候才会进行清理,每次 GC 都会触发“Stop-The-World”。内存越大,STW 的时间也越长,所以内存也不仅仅是越大就越好。由于复制算法在对象存活率较高的老年代会进行很多次的复制操作,效率很低,所以老年代这里采用的是标记 — 整理算法。
以下几种情况也会进入老年代。
大对象
大对象指需要大量连续内存空间的对象,这部分对象不管是不是“朝生夕死”,都会直接进到老年代。这样做主要是为了避免在 Eden 区及2个 Survivor 区之间发生大量的内存复制。当你的系统有非常多“朝生夕死”的大对象时,得注意了。
设置大对象参数
-XX:PretenureSizeThreshold
长期存活对象
虚拟机给每个对象定义了一个对象年龄(Age)计数器。正常情况下对象会不断的在 Survivor 的 From 区与 To 区之间移动,对象在 Survivor 区中每经历一次 Minor GC,年龄就增加1岁。当年龄增加到15岁时,这时候就会被转移到老年代。当然,这里的15,JVM 也支持进行特殊设置。
设置长期存活对象
-XX:MaxTenuringThreshold
空间担保
就是当年轻代空间不足时,就会向老年代借用内存,默认是启用空间担保的。
设置是否开启空间担保
-XX:+HandlePromotionFailure
动态对象年龄
虚拟机并不重视要求对象年龄必须到15岁,才会放入老年区,如果 Survivor 空间中相同年龄所有对象大小的总合大于 Survivor 空间的一半,年龄大于等于该年龄的对象就可以直接进去老年区,无需等你“成年”。
逃逸分析与栈上分配
我们都知道当执行一个方法时,栈帧入栈,方法执行完就出栈,此栈帧就销毁,无需垃圾回收。那如果将一部分对象分配到栈上,那么就不需要垃圾回收器来回收,这个性能是大大提升的。
所以这部分对象就需要用到逃逸分析法,通过逃逸分析法来判断哪些是非逃逸的对象,然后将这部分对象分配到栈上。
逃逸分析,就是分析对象的作用域。如果对象在方法体内有效的,就是说在方法结束就销毁,那么此对象就是非逃逸的。
public class Demo0 {
private Demo0 obj;
/* 方法返回Demo0对象,发生逃逸 */
public Demo0 getObj(){
return obj == null? new Demo0() : obj;
}
/* 为成员属性赋值,发生逃逸 */
public void setObj(){
this.obj = new Demo0();
}
/* 对象的作用于仅在当前方法有效,没有发生逃逸 */
public void use(){
Demo0 d = new Demo0();
}
/* 引用成员属性的值,发生逃逸 */
public void use2(){
Demo0 d = getObj();
}
}