对象的内存分配策略
测试环境jdk1.6 32位
对象的内存分配,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在
tlab上分配。少数情况下也可能会直接分配在老年代中,分配的规则不是百分百确定,其细节取决于当前使用的是那种垃圾收集器组合,还有虚拟机中于内存相关的参数配置,下面介绍的是几条普遍的内存分配规则。
对象优先在Eden分配
大多数的对象在新生代Eden区中分配内存空间,如果Eden空间不够,虚拟机将自动发起一次Minor GC 垃圾回收
测试工具:eclipse
1. 测试代码
package cn.erong.test;
public class Jtest {
private static final int _1M = 1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[2*_1M];
allocation2 = new byte[2*_1M];
allocation3 = new byte[2*_1M];
allocation4 = new byte[4*_1M];
}
}
2. 配置jvm参数
-XX:+PrintGCTimeStamps --打印gc时间信息
-XX:+PrintGCDetails --打印GC详细信息
-verbose:gc --开启gc日志
-Xloggc:d:/gc.log --设置gc日志的存放位置
-Xms20M --设置java堆最小为20M
-Xmx20M --设置java堆最大为20M
-Xmn10M --设置新生代内存大小10M
-XX:SurvivorRatio=8 --设置新生代内存区域中Eden区域和Survivor区域的比例
3. JVM参数加入到Run configurations->对应的application->Arguments->Vm arguments,然后run 运行
GC日志如下:
Java HotSpot(TM) Client VM (25.151-b12) for windows-x86 JRE (1.8.0_151-b12), built on Sep 5 2017 19:31:49 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 3567372k(1058760k free), swap 7133056k(3189624k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:SurvivorRatio=8 -XX:-UseLargePagesIndividualAllocation
0.096: [GC (Allocation Failure) 0.096: [DefNew: 6963K->483K(9216K), 0.0058886 secs] 6963K->6627K(19456K), 0.0060831 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 4743K [0x03c00000, 0x04600000, 0x04600000)
eden space 8192K, 52% used [0x03c00000, 0x040290e8, 0x04400000)
from space 1024K, 47% used [0x04500000, 0x04578d98, 0x04600000)
to space 1024K, 0% used [0x04400000, 0x04400000, 0x04500000)
tenured generation total 10240K, used 6144K [0x04600000, 0x05000000, 0x05000000)
the space 10240K, 60% used [0x04600000, 0x04c00030, 0x04c00200, 0x05000000)
Metaspace used 84K, capacity 2242K, committed 2368K, reserved 4480K
注意:
1. 新生代GC(Minor GC):指的是发生在新生代中的垃圾收集动作,很频繁
2. 老年代GC(Major GC/Full GC):指的发生在老年代中的GC,出现了Major GC,经常伴随至少一次Minor GC(但非绝对),速度上比上面的Minor GC要慢10倍以上
3. Allocation Failure 表示引起GC的原因是因为年轻代中没有足够的内存空间存放对象而触发的,这里的GC表示Minor GC
日志解读:
可以看出发生了一次Minor GC,发生前allocation1-allocation3是放在新生代中,从日志看出大概是占用了6M的空间,后面在创建allocation4由于内存空间不够(allocation4是4M,新生代中可用的为9M),这样空间不够会触发一次复制算法,另一个Survivor内存空间(用来存放存活对象的内存区域)为1M,这样无法放入,由于分担机制,将会把allocation1->allocation3放入到老年代中。最后的allocation4存放在Eden内存区域中。
从后面的内存区域的占有率可以验证这点
def new generation total 9216K, used 4743K [0x03c00000, 0x04600000, 0x04600000) ---新生代
eden space 8192K, 52% used [0x03c00000, 0x040290e8, 0x04400000) --eden使用内存区域为4M
from space 1024K, 47% used [0x04500000, 0x04578d98, 0x04600000)
to space 1024K, 0% used [0x04400000, 0x04400000, 0x04500000)
tenured generation total 10240K, used 6144K [0x04600000, 0x05000000, 0x05000000)--老年代 使用为6M
大对象直接进入老年代
大对象指的是,需要大量连续内存空间的Java对象,最典型的大对象是很长的字符串和数组。
对于虚拟机而言,出现大对象容易导致内存空间还有不少空间就提前出发垃圾收集,这样耗费系统资源
虚拟机提供一个-XX:PretenureSizeThreshold参数,令大于这个设置值的对象直接分配到老年代中。这样避免了新生代的多次的内存复制(新生代采用复制算法收集内存)
注意: PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个收集器,Parrallel Scavenge收集器一般不需要设置,如果遇到必须使用此参数,可以考虑ParNew和CMS收集器的组合
测试代码:
public class Jtest {
private static final int _1M = 1024*1024;
public static void main(String[] args) {
byte[] allocation4;
allocation4 = new byte[4*_1M];
}
}
JVM参数设置,在上面的案例的基础上加上:-XX:PretenureSizeThreshold=3145728
Heap
def new generation total 9216K, used 984K [0x03c00000, 0x04600000, 0x04600000)
eden space 8192K, 12% used [0x03c00000, 0x03cf6010, 0x04400000)
from space 1024K, 0% used [0x04400000, 0x04400000, 0x04500000)
to space 1024K, 0% used [0x04500000, 0x04500000, 0x04600000)
tenured generation total 10240K, used 4096K [0x04600000, 0x05000000, 0x05000000) --从这里看出,4M对象直接放入老年代中
the space 10240K, 40% used [0x04600000, 0x04a00010, 0x04a00200, 0x05000000)
Metaspace used 84K, capacity 2242K, committed 2368K, reserved 4480K
长期存活的对象将进入老年代
虚拟机是采用分代收集的思想来决定内存使用何种收集算法进行内存回收的。那么就需要区分哪些对象放在新生代中,哪些对象放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。
如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将移动到Survivor空间中,并且对象年龄设为1,并且没熬过一次Minor GC,年龄就会增加一岁,默认增加到15岁,自动晋升到老年代中
默认年龄可以设置: -XX:MaxTenuringThreshold
测试代码:
public class MaxAgeTest {
private static final int _1M = 1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[_1M/4];//在第二次GC时候进入到老年代中
allocation2 = new byte[4*_1M];
allocation3 = new byte[4*_1M];//触发一次Minor GC,allocation3放入Eden区
allocation3 = null;//手动触发一次GC,Eden区清空了
allocation4 = new byte[4*_1M];//Eden再次放入一个4M对象
}
}
jvm参数:
-Xms20M
-Xmx20M
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-verbose:gc
-Xloggc:d:/gc.log
-Xmn10M
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=1
动态对象年龄判定
虚拟机并不是永远要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代中,如果Survivor空间中相同年龄的对象的总和大于Survivor空间的一半,就将年龄大于等于该年龄的对象直接放入到老年代中
测试代码:
public class Jtest {
private static final int _1M = 1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3,allocation4;
allocation1 = new byte[_1M/4];
allocation2 = new byte[_1M/4];
allocation3 = new byte[4*_1M];
allocation4 = new byte[4*_1M];
allocation4 = null;
allocation4 = new byte[4*_1M];
}
}
JVM参数:
-Xms20m
-Xmx20m
-XX:+PrintGCTimeStamps
-XX:+PrintGCDetails
-verbose:gc
-Xloggc:d:/gc.log
-Xmn10M
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=15
GC日志:
0.095: [GC (Allocation Failure) 0.095: [DefNew: 5427K->995K(9216K), 0.0037095 secs] 5427K->5091K(19456K), 0.0039170 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.099: [GC (Allocation Failure) 0.099: [DefNew: 5091K->0K(9216K), 0.0013279 secs] 9187K->5090K(19456K), 0.0014083 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
Heap
def new generation total 9216K, used 4260K [0x03e00000, 0x04800000, 0x04800000)
eden space 8192K, 52% used [0x03e00000, 0x042290e8, 0x04600000)
from space 1024K, 0% used [0x04600000, 0x04600000, 0x04700000)
to space 1024K, 0% used [0x04700000, 0x04700000, 0x04800000)
tenured generation total 10240K, used 5090K [0x04800000, 0x05200000, 0x05200000)
the space 10240K, 49% used [0x04800000, 0x04cf8ac0, 0x04cf8c00, 0x05200000)
Metaspace used 84K, capacity 2242K, committed 2368K, reserved 4480K
从日志的结果可以看出,最后2个1/4M的对象和一个4M的对象放入到老年代中,一个4M的放入在新生代的Eden区中,为什么会这样?
第一次的GC,由于allocation4的创建而触发的,由于此时Eden区域中已经存放了allocation1->allocation3,内存空间不够。
这样理论上是allocation3是分担进入到老年代中,但是为什么allocation1,allocation2也是直接进入到了老年代中,而没有等到15岁呢,首次不是应该放在Survivor区域中吗?
因为两个对象加起来达到了Survivor的一半,并且是同年的,就直接进入老年代了
第二次GC,触发前allocation4在新生代中,手动将引用置null,触发一次GC,后面又创建了allocation4,最后还是放在Eden中
空间分配担保
前面有介绍过,进行新生代的垃圾收集Minor GC时候,将存活对象移动到另一块的Survivor区域中,如果该内存区域空间不够,就会通过担保机制,放入到老年代中
现在说下,这个担保细节?
1. 在JDK 1.6 update 24版本前,如果发生Minor GC,虚拟机会检查老年代最大可用连续空间是否大于新生代对象总和,如果成立的话, 那么Minor GC可以确保是安全的,不过不是,则是检查参数HandlePromotionFailure是否允许担保失败,如果允许,那么会检查老年代的最大可用连续空间是否大于历次晋升的对象的平均值,如果是,那么就会尝试进行一次可能有风险的Minor GC,如果小于或者HandlerPromotionFailure不允许冒险,就会进行一次老年代垃圾收集 Full GC.
2. 在JDK 1.6 update 24后,HandlerPromotionFailure参数是失败的,此时只要老年代的连续可用空间大于新生代对象总大小或者历次晋升的对象平均值就会进行Minor GC,否则进行Full GC.
总结下,JVM的垃圾收集策略
1. 对象优先在Eden区域分配,如果空间不够,触发一次 Minor GC
2. 大对象直接进入到老年代中,可以通过参数设置,只要对象大于这个参数-XX:PretenureSizeThreshold的值,直接进入老年代
3. 长期存活的对象将进入老年代,只要对象的年龄等于这个参数-XX:MaxTenuringThreshold的值,下次gc进入老年代
4. 如果Survivor年龄相同的对象的总空间大于Survivor空间的一半,只要年龄大于等于这个年龄的对象,直接进入老年代
5. 执行新生代垃圾收集时候,存在一个担保原则,老年代的连续可用空间大于新生代对象总大小或者历次晋升的对象平均值就会进行Minor GC,否则进行Full GC.