jvm学习笔记:3.5Java内存分配策略

      一般把java堆分成新生代和老年代。垃圾回收器在新生代使用复制算法时:将新生代分成Eden和2个survivor。

jvm学习笔记:3.5Java内存分配策略
 

简单来说,对象内存分配主要是在堆中分配。但是分配的规则并不是固定的,取决于使用的收集器组合以及JVM内存相关参数的设定

一,对象优先在新生代Eden区分配

/**
 * 
 * 类描述:对象优先在eden分配,以及minor gc垃圾清理
 * jvm:-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
 * @since  jdk1.7
 * @version 1.0
 */
public class ObjEden {

	private static int _1M = 1024 * 1024;
	
	public static void main(String[] args) {
		byte[] b1, b2, b3, b4;
		b1 = new byte[2 * _1M];
		b2 = new byte[2 * _1M];
		b3 = new byte[2 * _1M];
		b4 = new byte[3 * _1M];
	}
}

 这段代码执行的jvm参数:-XX:+PrintGCDetails -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8

-XX:+PrintGCDetails:控制台打印垃圾回收日志

-Xms20M:限制堆大小20m

-Xmx20M:堆大小最大20m

-Xmn10M:新生代大小10m,意味着老年代也是10m

-XX:SurvivorRatio=8:新年代的 Eden区和一个Survivor区的比例是8:1

执行结果:

[GC [PSYoungGen: 6815K->386K(9216K)] 6815K->6530K(19456K), 0.0051716 secs] [Times: user=0.02 sys=0.02, real=0.01 secs] 
[Full GC [PSYoungGen: 386K->0K(9216K)] [PSOldGen: 6144K->6412K(10240K)] 6530K->6412K(19456K) [PSPermGen: 3030K->3030K(21248K)], 0.0083140 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
Heap
 PSYoungGen      total 9216K, used 3399K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
  eden space 8192K, 41% used [0x00000000ff600000,0x00000000ff951f98,0x00000000ffe00000)
  from space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
  to   space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
 PSOldGen        total 10240K, used 6412K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  object space 10240K, 62% used [0x00000000fec00000,0x00000000ff2430f0,0x00000000ff600000)
 PSPermGen       total 21248K, used 3047K [0x00000000f9a00000, 0x00000000faec0000, 0x00000000fec00000)
  object space 21248K, 14% used [0x00000000f9a00000,0x00000000f9cf9e70,0x00000000faec0000)

 

 注:当b3分配完成后,新生代将使用6M内存(6144KB,b1+b2+b3),同时申请b4的4M=4096KB内存,此时新生代的可用内存为9216-6144=3072KB,不足以分配b4的空间,则触发一次Minor GC回收新生代内存空间,由于b1、b2以及b3都为存活状态,并且剩余的一个Survivor区无法装下b1、b2和b3,则新生代会租借老年代的区域,并将b1、b2和b3移动至租借区域,然后新生代完成Minor GC。由于此时新生代已经没有对象存放其中,剩余大量内存,则b4将在新生代中分配

二,大对象直接进入老年代

为了避免内存回收时大对象在Eden区和2个Survivor区之间的拷贝(ParNew收集器使用复制算法),同时为了避免为了提供足够的内存空间而提前触发的GC,虚拟机提供了-XX:PretenureSizeThreshold该设置只对Serial和ParNew收集器生效)参数,大于该参数设置值的对象将直接在老年代分配

[java]  view plain copy
  1. //-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails   
  2. //-XX:PretenureSizeThreshold=2097152  
  3. public class test {  
  4.     static int mb = 1024*1024;  
  5.       
  6.     public static void main(String[] args) {  
  7.         byte[] b1 = new byte[3*mb];  
  8.         System.out.println("b1 over");  
  9.     }  
  10. }  

由于设置超过2M(2*1024*1024=2097152B)的对象直接在老年代分配,故b1将分配在老年代上

[plain]  view plain copy
  1. b1 over  
  2. Heap  
  3.  par new generation   total 9216K, used 507K [0x03b50000, 0x04550000, 0x04550000)//新生代几乎为空  
  4.   eden space 8192K,   6% used [0x03b50000, 0x03bcef00, 0x04350000)  
  5.   from space 1024K,   0% used [0x04350000, 0x04350000, 0x04450000)  
  6.   to   space 1024K,   0% used [0x04450000, 0x04450000, 0x04550000)  
  7.  tenured generation   total 10240K, used 3072K [0x04550000, 0x04f50000, 0x04f50000)//老年代使用了3*1024K内存  
  8.    the space 10240K,  30% used [0x04550000, 0x04850010, 0x04850200, 0x04f50000)  
  9.  compacting perm gen  total 12288K, used 2110K [0x04f50000, 0x05b50000, 0x08f50000)  
  10.    the space 12288K,  17% used [0x04f50000, 0x0515f8c8, 0x0515fa00, 0x05b50000)  
  11. No shared spaces configured.  

三,长期存活对象将进入老年代
由于虚拟机垃圾收集是基于“分代算法”的,故虚拟机必须能够识别哪些对象存放在新生代,哪些对象应该存放在老年代

虚拟机设计了一个对象年龄计数器,如果对象在Eden区出生并且经过第一次Minor GC后依然存活,并且可以被Survivor区容纳,就会被复制至Survivor区并将对象年龄设置为1。以后对象每熬过一次Minor GC,对象年龄便+1。当对象年龄超过对象晋升老年代的年龄阀值(该阀值默认为15)时,便会晋升至老年代,何时晋升,我们接下来研究

虚拟机提供了-XX:MaxTenuringThreshold参数设置晋升阀值

[java]  view plain copy
  1. //-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails   
  2. //-XX:MaxTenuringThreshold=1  
  3. public class test {  
  4.     static int mb = 1024*1024;  
  5.       
  6.     public static void main(String[] args) {  
  7.         System.out.println("step 1");  
  8.         byte[] b1 = new byte[1*mb/4];  
  9.         System.out.println("step 2");  
  10.         byte[] b2 = new byte[4*mb];  
  11.         System.out.println("step 3");  
  12.         byte[] b3 = new byte[4*mb];//GC  
  13.         System.out.println("step 4");  
  14.         b3 = null;  
  15.         System.out.println("step 5");  
  16.         b3 = new byte[4*mb];//GC  
  17.     }  
  18. }  

b1、b2正常分配。在step3,新生代将没有足够的内存分配b3所需的4M空间,故引发一次Minor GC。b1只有256KB,可以放置在Survivor区中,故复制b1到Survivor区中,b2为4M,无法放置到Survivor区中,故租借老年代4M内存放置b2,回收新生代内存空间,b1经历了一次Minor GC后依然存活,故年龄变为1。

在step4,分配给b3对象的内存空间依然被占用,只是将b3对象的引用置为空,由于不涉及到内存分配,故而不涉及到GC,因此对象的年龄也不会发生变化

在step5,重新给b3对象分配4M空间,由于新生代没有足够内存,故引发Minor GC,step3分配给b3的4M内存空间由于不再与存活对象相关联,将被回收,同时,由于b1的年龄到达对象晋升老年代的年龄设置,b1将被移动至老年代

[plain]  view plain copy
  1. step 1  
  2. step 2  
  3. step 3  
  4. {Heap before GC invocations=0 (full 0):  
  5.  par new generation   total 9216K, used 4695K [0x03b80000, 0x04580000, 0x04580000)//b1+b2  
  6.   eden space 8192K,  57% used [0x03b80000, 0x04015f50, 0x04380000)  
  7.   from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)  
  8.   to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)  
  9.  tenured generation   total 10240K, used 0K [0x04580000, 0x04f80000, 0x04f80000)//此时老年代为空  
  10.    the space 10240K,   0% used [0x04580000, 0x04580000, 0x04580200, 0x04f80000)  
  11.  compacting perm gen  total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000)  
  12.    the space 12288K,  17% used [0x04f80000, 0x0518e450, 0x0518e600, 0x05b80000)  
  13. No shared spaces configured.  
  14. [GC [ParNew: 4695K->409K(9216K), 0.0049519 secs] 4695K->4505K(19456K), 0.0049944 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
  15. Heap after GC invocations=1 (full 0):  
  16.  par new generation   total 9216K, used 409K [0x03b80000, 0x04580000, 0x04580000)//b1  
  17.   eden space 8192K,   0% used [0x03b80000, 0x03b80000, 0x04380000)  
  18.   from space 1024K,  39% used [0x04480000, 0x044e6610, 0x04580000)  
  19.   to   space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)  
  20.  tenured generation   total 10240K, used 4096K [0x04580000, 0x04f80000, 0x04f80000)//b2  
  21.    the space 10240K,  40% used [0x04580000, 0x04980010, 0x04980200, 0x04f80000)  
  22.  compacting perm gen  total 12288K, used 2105K [0x04f80000, 0x05b80000, 0x08f80000)  
  23.    the space 12288K,  17% used [0x04f80000, 0x0518e450, 0x0518e600, 0x05b80000)  
  24. No shared spaces configured.  
  25. }  
  26. step 4  
  27. step 5  
  28. {Heap before GC invocations=1 (full 0):  
  29.  par new generation   total 9216K, used 4669K [0x03b80000, 0x04580000, 0x04580000)//b1+b3(step3)  
  30.   eden space 8192K,  52% used [0x03b80000, 0x03fa9098, 0x04380000)  
  31.   from space 1024K,  39% used [0x04480000, 0x044e6610, 0x04580000)  
  32.   to   space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)  
  33.  tenured generation   total 10240K, used 4096K [0x04580000, 0x04f80000, 0x04f80000)//b2  
  34.    the space 10240K,  40% used [0x04580000, 0x04980010, 0x04980200, 0x04f80000)  
  35.  compacting perm gen  total 12288K, used 2111K [0x04f80000, 0x05b80000, 0x08f80000)  
  36.    the space 12288K,  17% used [0x04f80000, 0x0518fe08, 0x05190000, 0x05b80000)  
  37. No shared spaces configured.  
  38. [GC [ParNew: 4669K->43K(9216K), 0.0008256 secs] 8765K->4548K(19456K), 0.0008701 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
  39. Heap after GC invocations=2 (full 0):  
  40.  par new generation   total 9216K, used 43K [0x03b80000, 0x04580000, 0x04580000)//step3分配的b3对象空间被回收  
  41.   eden space 8192K,   0% used [0x03b80000, 0x03b80000, 0x04380000)  
  42.   from space 1024K,   4% used [0x04380000, 0x0438ad90, 0x04480000)  
  43.   to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)  
  44.  tenured generation   total 10240K, used 4505K [0x04580000, 0x04f80000, 0x04f80000)//b1+b2  
  45.    the space 10240K,  43% used [0x04580000, 0x049e6590, 0x049e6600, 0x04f80000)  
  46.  compacting perm gen  total 12288K, used 2111K [0x04f80000, 0x05b80000, 0x08f80000)  
  47.    the space 12288K,  17% used [0x04f80000, 0x0518fe08, 0x05190000, 0x05b80000)  
  48. No shared spaces configured.  
  49. }  
  50. Heap  
  51.  par new generation   total 9216K, used 4303K [0x03b80000, 0x04580000, 0x04580000)//b3(step5)  
  52.   eden space 8192K,  52% used [0x03b80000, 0x03fa8fe0, 0x04380000)  
  53.   from space 1024K,   4% used [0x04380000, 0x0438ad90, 0x04480000)  
  54.   to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)  
  55.  tenured generation   total 10240K, used 4505K [0x04580000, 0x04f80000, 0x04f80000)//b1+b2  
  56.    the space 10240K,  43% used [0x04580000, 0x049e6590, 0x049e6600, 0x04f80000)  
  57.  compacting perm gen  total 12288K, used 2116K [0x04f80000, 0x05b80000, 0x08f80000)  
  58.    the space 12288K,  17% used [0x04f80000, 0x051913c8, 0x05191400, 0x05b80000)  
  59. No shared spaces configured.  

如果修改MaxTenuringThreshold的值为2,从打印日志中可以发现,最终老年代的内存使用量为4096KB=4M,也就是说b1没有晋升至老年代

上面是Minor GC的运行状况,如果是Full GC呢:

[java]  view plain copy
  1. //-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintHeapAtGC -XX:+PrintGCDetails   
  2. //-XX:MaxTenuringThreshold=1  
  3. public class test {  
  4.     static int mb = 1024*1024;  
  5.       
  6.     public static void main(String[] args) {  
  7.         byte[] b1 = new byte[1*mb/4];  
  8.         System.gc();  
  9.     }  
  10. }  

这里我们使用的是Full GC,也就是老年代的GC。

Full GC通常至少伴随着一次Minor GC(并非绝对),看下面日志,这里的Minor GC应该至少发生了2次,一次Minor GC是不会把b1移动至老年代的

[plain]  view plain copy
  1. {Heap before GC invocations=0 (full 0):  
  2.  par new generation   total 9216K, used 599K [0x03b80000, 0x04580000, 0x04580000)//b1  
  3.   eden space 8192K,   7% used [0x03b80000, 0x03c15f40, 0x04380000)  
  4.   from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)  
  5.   to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)  
  6.  tenured generation   total 10240K, used 0K [0x04580000, 0x04f80000, 0x04f80000)//老年代为空  
  7.    the space 10240K,   0% used [0x04580000, 0x04580000, 0x04580200, 0x04f80000)  
  8.  compacting perm gen  total 12288K, used 2104K [0x04f80000, 0x05b80000, 0x08f80000)  
  9.    the space 12288K,  17% used [0x04f80000, 0x0518e278, 0x0518e400, 0x05b80000)  
  10. No shared spaces configured.  
  11. [Full GC (System) [Tenured: 0K->404K(10240K), 0.0069434 secs] 599K->404K(19456K), [Perm : 2104K->2104K(12288K)], 0.0069992 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]   
  12. Heap after GC invocations=1 (full 1):  
  13.  par new generation   total 9216K, used 0K [0x03b80000, 0x04580000, 0x04580000)//新生代为空  
  14.   eden space 8192K,   0% used [0x03b80000, 0x03b80000, 0x04380000)  
  15.   from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)  
  16.   to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)  
  17.  tenured generation   total 10240K, used 404K [0x04580000, 0x04f80000, 0x04f80000)//b1  
  18.    the space 10240K,   3% used [0x04580000, 0x045e5130, 0x045e5200, 0x04f80000)  
  19.  compacting perm gen  total 12288K, used 2104K [0x04f80000, 0x05b80000, 0x08f80000)  
  20.    the space 12288K,  17% used [0x04f80000, 0x0518e278, 0x0518e400, 0x05b80000)  
  21. No shared spaces configured.  
  22. }  
  23. Heap  
  24.  par new generation   total 9216K, used 327K [0x03b80000, 0x04580000, 0x04580000)  
  25.   eden space 8192K,   4% used [0x03b80000, 0x03bd1f98, 0x04380000)  
  26.   from space 1024K,   0% used [0x04380000, 0x04380000, 0x04480000)  
  27.   to   space 1024K,   0% used [0x04480000, 0x04480000, 0x04580000)  
  28.  tenured generation   total 10240K, used 404K [0x04580000, 0x04f80000, 0x04f80000)  
  29.    the space 10240K,   3% used [0x04580000, 0x045e5130, 0x045e5200, 0x04f80000)  
  30.  compacting perm gen  total 12288K, used 2116K [0x04f80000, 0x05b80000, 0x08f80000)  
  31.    the space 12288K,  17% used [0x04f80000, 0x05191190, 0x05191200, 0x05b80000)  
  32. No shared spaces configured.  

四:动态对象年龄判定

为了使内存分配更加灵活,虚拟机并不要求对象年龄达到MaxTenuringThreshold才晋升老年代

如果Survivor区中相同年龄所有对象大小的总和大于Survivor区空间的一半,年龄大于或等于该年龄的对象在Minor GC时将复制至老年代

[java]  view plain copy
  1. //-XX:+UseParNewGC -Xms20m -Xmx20m -Xmn10m  -XX:MaxTenuringThreshold=10  
  2. //-XX:+PrintTenuringDistribution  
  3. public class Test {  
  4.     static int mb = 1024*1024;  
  5.       
  6.     public static void main(String[] args) {  
  7.         System.out.println("step 1");  
  8.         byte[] b1 = new byte[1*mb/4];  
  9.         byte[] b3 = new byte[4*mb];  
  10.         byte[] b4 = new byte[4*mb];//GC  
  11.         System.out.println("step 2");  
  12.         byte[] b2 = new byte[1*mb/4];//可以尝试1*mb/2,然后观察日志  
  13.         b4 = null;  
  14.         System.out.println("step 3");  
  15.         b4 = new byte[4*mb];//GC  
  16.         System.out.println("step 4");  
  17.         b4 = null;  
  18.         b4 = new byte[4*mb];//GC  
  19.     }  
  20. }  

先来介绍一个设置-XX:+PrintTenuringDistribution,这个参数很有意思,会在Minor GC时打印Survivor区内存容量的一半,晋升老年代年龄阀值,Survivor区中的对象大小以及对象年龄

根据启动参数的设置,Survivor大小的一半是524288B,也就是512KB。第一次GC后,b1依然存活,故年龄变为1。第二次GC后,b1和b2依然存活,故b1的年龄变为2,b2的年龄为1。b1+b2的大小加起来超过了Survivor区容量的一半,此时会修改Survivor区晋升老年代年龄阀值为2(如果移动年龄为2的对象可以使Survivor去的内存使用降至512KB以内,则只移动年龄为2的对象,否则将会同时移动年龄为1的对象)。第三次GC时,将年龄等于晋升阀值的对象移动至老年代,执行GC,GC结束后,b1依然在Survivor区(当然可能从Survivor from区拷贝至了Survivor to区),此时b1的年龄变为2。这时Survivor区的使用内存没有达到512M,修改Survivor区晋升老年代年龄阀值为参数设置的10。

[plain]  view plain copy
  1. step 1  
  2.   
  3. Desired survivor size 524288 bytes, new threshold 10 (max 10)  
  4. - age   1:     412800 bytes,     412800 total  
  5. step 2  
  6. step 3  
  7.   
  8. Desired survivor size 524288 bytes, new threshold 2 (max 10)  
  9. - age   1:     262160 bytes,     262160 total  
  10. - age   2:     412800 bytes,     674960 total  
  11. step 4  
  12.   
  13. Desired survivor size 524288 bytes, new threshold 10 (max 10)  
  14. - age   1:        136 bytes,        136 total  
  15. - age   2:     262160 bytes,     262296 total  

最后,为什么在第三次GC后,Survivor区还存在一个大小为136B,年龄为1的被使用内存空间?

我猜测,虽然Minor GC时Survivor区没有足够的空间完成GC时会租借老年代的内存,但是在Survivor区依然保存了一个指向老年代租借内存起始地址的引用

五:空间分配担保

这个前面已经出现过多次了,由于新生代使用复制算法,当Minor GC时如果存活对象过多,无法完全放入Survivor区,就会向老年代借用内存存放对象,以完成Minor GC

 

在触发Minor GC时,虚拟机会先检测之前GC时租借的老年代内存的平均大小是否大于老年代的剩余内存,如果大于,则将Minor GC变为一次Full GC,如果小于,则查看虚拟机是否允许担保失败(-XX:+/-HandlePromotionFailure。从jdk6.0开始,允许担保失败已变为HotSpot虚拟机所有收集器默认设置,虚拟机将不再识别该参数设置,详见JDK-6990095 : Deprecate and eliminate -XX:-HandlePromotionFailure),如果允许担保失败,则只执行一次Minor GC,否则也要将Minor GC变为一次Full GC(直到GC结束时才能确定到底有多少对象需要被移动至老年代,所以在GC前,只能使用粗略的平均值进行判断)

你可能感兴趣的:(java内存分配)