前言一、对象在Eden区分配二、大对象直接进入老年代三、长期存活的对象将进入老年代四、老年代空间分配担保机制
前言
上篇文章解释了对象在栈上分配的相关内容,这篇是上一篇的后续,解释一下对象在Eden区分配,以及上一篇内存分配流程图中大对象和长期存活的对象该何去何从。
一、对象在Eden区分配
大多数情况下,对象在新生代中 Eden 区分配。当 Eden 区没有足够空间进行分配时,虚拟机将发起一次Minor GC。我 们来进行实际测试一下。
在测试之前我们先来看看 Minor GC和Full GC 有什么不同呢?
Minor GC/Young GC:指发生新生代的的垃圾收集动作,Minor GC非常频繁,回收速度一般也比较快。
Major GC/Full GC:一般会回收老年代 ,年轻代,方法区的垃圾,Major GC的速度一般会比Minor GC的慢 10倍以上。
Eden与Survivor区默认8:1:1
大量的对象被分配在eden区,eden区满了后会触发minor gc,可能会有99%以上的对象成为垃圾被回收掉,剩余存活 的对象会被挪到为空的那块survivor区,下一次eden区满了后又会触发minor gc,把eden区和survivor区垃圾对象回 收,把剩余存活的对象一次性挪动到另外一块为空的survivor区,因为新生代的对象都是朝生夕死的,存活时间很短,所 以JVM默认的8:1:1的比例是很合适的,让eden区尽量的大,survivor区够用即可, JVM默认有这个参数-XX:+UseAdaptiveSizePolicy(默认开启),会导致这个8:1:1比例自动变化,如果不想这个比例有变 化可以设置参数-XX:-UseAdaptiveSizePolicy
package com.daydayup.jvm;
public class GCTest {
public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2;
allocation1 = new byte[60000*1024];
// allocation2 = new byte[8000*1024];
// allocation3 = new byte[1000*1024];
// allocation4 = new byte[1000*1024];
// allocation5 = new byte[1000*1024];
// allocation6 = new byte[1000*1024];
}
}
//运行结果
[GC (Allocation Failure) [PSYoungGen: 65024K->904K(75776K)] 65024K->912K(249344K), 0.0006997 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
PSYoungGen total 75776K, used 2204K [0x000000076bd00000, 0x0000000771180000, 0x00000007c0000000)
eden space 65024K, 2% used [0x000000076bd00000,0x000000076be45378,0x000000076fc80000)
from space 10752K, 8% used [0x000000076fc80000,0x000000076fd62020,0x0000000770700000)
to space 10752K, 0% used [0x0000000770700000,0x0000000770700000,0x0000000771180000)
ParOldGen total 173568K, used 8K [0x00000006c3600000, 0x00000006cdf80000, 0x000000076bd00000)
object space 173568K, 0% used [0x00000006c3600000,0x00000006c3602000,0x00000006cdf80000)
metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
我们可以看出eden区内存几乎已经被分配完全(即使程序什么也不做,新生代也会使用至少几M内存)。假如我们再为 allocation2分配内存会出现什么情况呢?
package com.daydayup.jvm;
public class GCTest {
public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2;
allocation1 = new byte[60000*1024];
allocation2 = new byte[8000*1024];
// allocation3 = new byte[1000*1024];
// allocation4 = new byte[1000*1024];
// allocation5 = new byte[1000*1024];
// allocation6 = new byte[1000*1024];
}
}
//运行结果
[GC (Allocation Failure) [PSYoungGen: 65024K->920K(75776K)] 65024K->60928K(249344K), 0.0243280 secs] [Times: user=0.14 sys=0.00, real=0.02 secs]
Heap
PSYoungGen total 75776K, used 9570K [0x000000076bd00000, 0x0000000775100000, 0x00000007c0000000)
eden space 65024K, 13% used [0x000000076bd00000,0x000000076c572a78,0x000000076fc80000)
from space 10752K, 8% used [0x000000076fc80000,0x000000076fd66030,0x0000000770700000)
to space 10752K, 0% used [0x0000000774680000,0x0000000774680000,0x0000000775100000)
ParOldGen total 173568K, used 60008K [0x00000006c3600000, 0x00000006cdf80000, 0x000000076bd00000)
object space 173568K, 34% used [0x00000006c3600000,0x00000006c709a010,0x00000006cdf80000)
metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
简单解释一下为什么会出现这种情况: 因为给allocation2分配内存的时候eden区内存几乎已经被分配完了,我们刚刚讲 了当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC,GC期间虚拟机又发现allocation1无法存入 Survior空间,所以只好把新生代的对象提前转移到老年代中去,老年代上的空间足够存放allocation1,所以不会出现 Full GC。执行Minor GC后,后面分配的对象如果能够存在eden区的话,还是会在eden区分配内存。
package com.daydayup.jvm;
public class GCTest {
public static void main(String[] args) throws InterruptedException {
byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6;
allocation1 = new byte[60000*1024];
allocation2 = new byte[8000*1024];
allocation3 = new byte[1000*1024];
allocation4 = new byte[1000*1024];
allocation5 = new byte[1000*1024];
allocation6 = new byte[1000*1024];
}
}
//运行结果
[GC (Allocation Failure) [PSYoungGen: 65024K->872K(75776K)] 65024K->60880K(249344K), 0.0233776 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]
Heap
PSYoungGen total 75776K, used 13785K [0x000000076bd00000, 0x0000000775100000, 0x00000007c0000000)
eden space 65024K, 19% used [0x000000076bd00000,0x000000076c99c798,0x000000076fc80000)
from space 10752K, 8% used [0x000000076fc80000,0x000000076fd5a020,0x0000000770700000)
to space 10752K, 0% used [0x0000000774680000,0x0000000774680000,0x0000000775100000)
ParOldGen total 173568K, used 60008K [0x00000006c3600000, 0x00000006cdf80000, 0x000000076bd00000)
object space 173568K, 34% used [0x00000006c3600000,0x00000006c709a010,0x00000006cdf80000)
metaspace used 3221K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 350K, capacity 388K, committed 512K, reserved 1048576K
二、大对象直接进入老年代
大对象就是需要大量连续内存空间的对象(比如:字符串、数组)。JVM参数 -XX:PretenureSizeThreshold 可以设置大对象的大小,如果对象超过设置大小会直接进入老年代,不会进入年轻代,这个参数只在 Serial 和ParNew两个收集器下 有效。比如设置JVM参数:-XX:PretenureSizeThreshold=1000000 (单位是字节) -XX:+UseSerialGC ,再执行下上面的第一 个程序会发现大对象直接进了老年代 。
原因是为了避免为大对象分配内存时的复制操作而降低效率。
三、长期存活的对象将进入老年代既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在 老年代中。为了做到这一点,虚拟机给每个对象一个对象年龄(Age)计数器。
如果对象在 Eden 出生并经过第一次 Minor GC 后仍然能够存活,并且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,并将对象年龄设为1。对象在 Survivor 中每熬过一次 MinorGC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁,CMS收集器默认6岁,不同的垃圾收集器会略微有点不同),就会被晋升到老年代中。对象晋升到老年代 的年龄阈值,可以通过参数 -XX:MaxTenuringThreshold 来设置。
年轻代每次minor gc之前JVM都会计算下老年代剩余可用空间 ,如果这个可用空间小于年轻代里现有的所有对象大小之和(包括垃圾对象) 就会看一个“-XX:-HandlePromotionFailure”(jdk1.8默认就设置了)的参数是否设置了, 如果有这个参数,就会看看老年代的可用内存大小,是否大于之前每一次minor gc后进入老年代的对象的平均大小。
如果上一步结果是小于或者之前说的参数没有设置,那么就会触发一次Full gc,对老年代和年轻代一起回收一次垃圾, 如果回收完还是没有足够空间存放新的对象就会发生"OOM" 当然,如果minor gc之后剩余存活的需要挪动到老年代的对象大小还是大于老年代可用空间,那么也会触发full gc,full gc完之后如果还是没有空间放minor gc之后的存活对象,则也会发生“OOM”。
下面附担保机制的流程图:



