这一节主要介绍对象创建时,在堆中的一些过程。
回忆下,我们之前说的,什么时候会发生垃圾回收?
除了在一些安全点处也许会发生垃圾回收(只是也许),如果在所需内存不足的情况下,一定会发生垃圾回收。
分配堆空间
首先通过设置参数,把堆空间设置为 20M,其中 新生代 10M,老年代 10M。
参数设置:
-Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails
结果为:
Heap
PSYoungGen total 9216K, used 1685K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 20% used [0x00000007bf600000,0x00000007bf7a5580,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen total 10240K, used 0K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf600000)
创建一个新对象
我们首先创建一个对象,这个对象占用 2M 的空间。
package heap;
public class CreateObject {
public static void main(String[] args) {
byte[] obj1 = new byte[1024 * 1024 * 2];
}
}
最后的输出:
Heap
PSYoungGen total 9216K, used 3733K [0x00000007bf600000, 0x00000007c0000000, 0x00000007c0000000)
eden space 8192K, 45% used [0x00000007bf600000,0x00000007bf9a5590,0x00000007bfe00000)
from space 1024K, 0% used [0x00000007bff00000,0x00000007bff00000,0x00000007c0000000)
to space 1024K, 0% used [0x00000007bfe00000,0x00000007bfe00000,0x00000007bff00000)
ParOldGen total 10240K, used 0K [0x00000007bec00000, 0x00000007bf600000, 0x00000007bf600000)
object space 10240K, 0% used [0x00000007bec00000,0x00000007bec00000,0x00000007bf600000)
可以看到,新生代 被占用了,老年代占用为 0K,没有被使用。
所以,new 的对象先放在 eden 区。
填满 eden 区
在填满 eden 区后,会发生什么呢?因为 survivor 区实在太小了,很难看到。所以,这里可以借助 Visual VM,来观察,更加直观。
程序如下:
package heap;
public class CreateObject {
public static void main(String[] args) {
while(true){
byte[] bytes = new byte[1024 * 512];
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
重点是看右边的记录图。注意,这里我们将每次创建对象的大小设置为了 0.5M。
当 Eden 满的时候,会调用垃圾回收器,调用垃圾回收器后,Eden 出现了低谷,Survivor 出现了一个增长。老年区也出现了一个增长。
当 Eden 满的时候,如果 Survivor 区有足够的空间容纳存活对象,那么可以把存活对象放入 Survivor,多的对象放入老年区。
现在,我们把对象的大小调大。设置为 2M,这样 Survivor 就无法存放下。
可以看到,在经过一次垃圾回收的时候(可以看到GC Time 上有波峰,说明执行了一次垃圾回收),但我们注意到,Survivor 区中并没有被占用。说明垃圾回收过程中,直接将存活对象放到了老年代中。
再来聊聊 survivor 区
对象通常在 Eden 区里诞生,如果经过第一次 MInor GC 后仍然存活,并且能够被 Survivor 容纳的话,该对象会被移动到 Survivor 区,并且将其年龄设置为 1 岁。对象在 Survivor 区每熬过一次 Minor GC,年龄就增加 1 岁,当它年龄增大到一定程度(默认是 15 岁),就会被晋升到老年代。
特殊情况
有些时候,如果用户创建了大对象,如很长的字符串或者元素很多的数组的时候。这种大对象都占用大量的内存,像这种大对象,有很大概率是长时间使用的,不然为什么要创建大对象。
如果大对象朝生夕灭,我们知道在 Java 8 中,新生代默认采用的 标记-复制 算法,那么对于大对象而言,是非常耗时的。
所以,如果 JVM 设置了一个阈值,那么当分配的对象大于这个阈值的时候,会直接被分配到老年代。