内存分配策略:
1.对象优先在Eden分配
2.大对象直接进入老年代
3.长期存活的对象将进入老年代
4.动态对象年龄判定
5.空间分配担保
测试一:
public class Main {
public static void main(String[] args) {
byte[] b1 = new byte[4*1024*1024];
}
}
-verbose:gc -XX:+PrintGCDetails 日志信息打印如下:
[GC (System.gc()) [PSYoungGen: 22477K->648K(38400K)] 42957K->21136K(125952K), 0.0094183 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[Full GC (System.gc()) [PSYoungGen: 648K->0K(38400K)] [ParOldGen: 20488K->529K(87552K)] 21136K->529K(125952K), [Metaspace: 2664K->2664K(1056768K)], 0.0059815 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 38400K, used 333K [0x00000000d5d00000, 0x00000000d8780000, 0x0000000100000000)
eden space 33280K, 1% used [0x00000000d5d00000,0x00000000d5d534a8,0x00000000d7d80000)
from space 5120K, 0% used [0x00000000d7d80000,0x00000000d7d80000,0x00000000d8280000)
to space 5120K, 0% used [0x00000000d8280000,0x00000000d8280000,0x00000000d8780000)
ParOldGen total 87552K, used 529K [0x0000000081600000, 0x0000000086b80000, 0x00000000d5d00000)
object space 87552K, 0% used [0x0000000081600000,0x0000000081684458,0x0000000086b80000)
Metaspace used 2671K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
可以看到垃圾收集器用的是ParNew。
制定选用serial收集器。-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC
打印信息如下:
[GC (Allocation Failure) [DefNew: 22576K->529K(39296K), 0.0011803 secs] 22576K->529K(126720K), 0.0012257 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
[Full GC (System.gc()) [Tenured: 0K->528K(87424K), 0.0027805 secs] 21687K->528K(126720K), [Metaspace: 2664K->2664K(1056768K)], 0.0028362 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 39424K, used 701K [0x0000000081600000, 0x00000000840c0000, 0x00000000ab950000)
eden space 35072K, 2% used [0x0000000081600000, 0x00000000816af738, 0x0000000083840000)
from space 4352K, 0% used [0x0000000083840000, 0x0000000083840000, 0x0000000083c80000)
to space 4352K, 0% used [0x0000000083c80000, 0x0000000083c80000, 0x00000000840c0000)
tenured generation total 87424K, used 528K [0x00000000ab950000, 0x00000000b0eb0000, 0x0000000100000000)
the space 87424K, 0% used [0x00000000ab950000, 0x00000000ab9d40e0, 0x00000000ab9d4200, 0x00000000b0eb0000)
Metaspace used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
选用的垃圾收集器是根据我们jdk所处的环境所指定的。若作为server服务的话,默认为ParNew;若作为客户端,收回的内存小,性能高,默认serial。
可以看到是Server。
回归正题。在GC日志中我们看到:红色底色的地方,肯定对象是优先分配到Eden中的。
测试二:
public class Main {
public static void main(String[] args) {
byte[] b1 = new byte[10*1024*1024];
byte[] b2 = new byte[10*1024*1024];
byte[] b3 = new byte[10*1024*1024];
}
}
Heap
def new generation total 39296K, used 33516K [0x0000000081600000, 0x00000000840a0000, 0x00000000ab950000)
eden space 34944K, 95% used [0x0000000081600000, 0x00000000836bb020, 0x0000000083820000)
from space 4352K, 0% used [0x0000000083820000, 0x0000000083820000, 0x0000000083c60000)
to space 4352K, 0% used [0x0000000083c60000, 0x0000000083c60000, 0x00000000840a0000)
tenured generation total 87424K, used 0K [0x00000000ab950000, 0x00000000b0eb0000, 0x0000000100000000)
the space 87424K, 0% used [0x00000000ab950000, 0x00000000ab950000, 0x00000000ab950200, 0x00000000b0eb0000)
Metaspace used 2670K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
再增加一个b4,发现eden space并没有增加。
所以:进行限制-verbose:gc -XX:+PrintGCDetails -XX:+UseSerialGC -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
把JVM设置了不可扩展内存20MB,其中新生代10MB,老年代10MB,而新生代区域的分配比例是8:1:1,使用Serial/Serial Old组合收集器
[GC (Allocation Failure) [DefNew: 984K->529K(9216K), 0.0009530 secs][Tenured: 0K->528K(10240K), 0.0014717 secs] 984K->528K(19456K), [Metaspace: 2663K->2663K(1056768K)], 0.0024785 secs] [Times: user=0.02 sys=0.00, real=0.00 secs]
[Full GC (Allocation Failure) [Tenured: 528K->517K(10240K), 0.0009990 secs] 528K->517K(19456K), [Metaspace: 2663K->2663K(1056768K)], 0.0010177 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.fuya.test7.Main.main(Main.java:7)
Heap
def new generation total 9216K, used 246K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 3% used [0x00000000fec00000, 0x00000000fec3d890, 0x00000000ff400000)
from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 517K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 5% used [0x00000000ff600000, 0x00000000ff681430, 0x00000000ff681600, 0x0000000100000000)
Metaspace used 2695K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 291K, capacity 386K, committed 512K, reserved 1048576K
可见发生了内存溢出。因为我们4个对象,每个对象10M.若改成3个2M,1个4M
byte[] b1 = new byte[2*1024*1024];
byte[] b2 = new byte[2*1024*1024];
byte[] b3 = new byte[2*1024*1024];
byte[] b4 = new byte[4*1024*1024];
[GC (Allocation Failure) [DefNew: 7128K->529K(9216K), 0.0035183 secs] 7128K->6673K(19456K), 0.0035578 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4708K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
from space 1024K, 51% used [0x00000000ff500000, 0x00000000ff5847c0, 0x00000000ff600000)
to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
tenured generation total 10240K, used 6144K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 60% used [0x00000000ff600000, 0x00000000ffc00030, 0x00000000ffc00200, 0x0000000100000000)
Metaspace used 2671K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 288K, capacity 386K, committed 512K, reserved 1048576K
情况分析:
有“GC ……”字眼就是GC日志记录,而Heap以下的信息是JVM关闭前的堆使用情况信息描述。其中GC代表MinorGC,如果是Full GC的话会直接写着Full GC。
[DefNew: 7128K->529K(9216K), 0.0035183 secs] 7128K->6673K(19456K), 0.0035578 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
DefNew代表的是Serival收集器,7128K->529K(9216K)中7128K代表新生代收集前使用内存,529K代表收集后的使用内存,而9216代表新生代的总分配内存0.0035183 secs为新生代收集时间。
外层的7128K->6673K(19456K)代表整个Java堆的收集前使用内存->收集后使用内存(总分配内存),0.0087927 secs为整个GC的收集时间。
在上述测试例子中,我把JVM设置了不可扩展内存20MB,其中新生代10MB,老年代10MB,而新生代区域的分配比例是8:1:1,使用Serial/Serial Old组合收集器。
从代码可以看出,allocation1、allocation2、allocation3一共需要6MB,而Eden一共有8MB,优先分配到Eden。但再分配allocation4的时候Eden空间不够,执行了一次Minor GC,在GC中,由于Survivor只有1MB,不够存放allocation1、allocation2、allocation3,所以直接迁移到老年代了,最后Eden空闲出来了就可以放allocation4了
Eden8M占了51%,大概4M; the space 10M,占了60%,6M
采用了内存分配担保
所谓大对象是指需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组(例如上面例子的byte[]数组)。大对象对虚拟机内存分配来说是一个坏消息,经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。虚拟机提供了一个-XX:PretenureSizeThreshold参数来设置大对象的界限,大于此值则直接分配在老年代去了
由于Minor GC跟Full GC是差别的,Minor的主要对象还是新生代,对象在Minor后并不都会直接进入老年代,除非Survivor空间不够,否则此存活对象会经过多次Minor GC后还生存的话才进入老年代,而虚拟机默认的Minor GC次数为15次,可通过-XX:MaxTenuringThreshold进行次数设置。
为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或者等于该年龄的对象直接可以进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象的空间,如果这个条件成立,那么Minor GC可以确保安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这个Minor GC是有风险的;如果小于或者HandlePromotionFailure设置不允许冒险,那么这时也要改为进行一次Full GC了。说白了就是虚拟机避免Full GC执行的次数而去做的检查机制。
取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
另外需要提醒,在JDK 6 Update 24之后,虚拟机已经不再使用HandlePromotionFailure参数了,规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC,否则将进行Full GC。