12.内存分配和回收策略

通俗的讲,对象的内存分配就是在堆上的分配,对象主要分配在新生代的Eden上,如果启动了本地线程分配缓冲,讲按线程优先在TLAB上分配。少数情况下也是直接在老年代中分配。

1.内存优先在Eden分配

一般情况下对象都是优先分配在Eden上,当Eden没有足够的空间进行分配时,jvm会发起一次Minor GC。如果还是没有足够的空间分配,后面还有另外的措施,下面会提到。

设置虚拟机的收集器日志参数-XX:+PrintGCDetails,在垃圾回收的时候会打印内存的回收日志,并且在进程退出的时候会输出当前内存各区域的分配情况。
下面来看下具体的例子,首先需要设置jvm的参数-Xms20m -Xmx20m -Xmn10m,这三个参数说明java堆大小为20M,且不可扩展,其中10M分配给新生代,剩下的10M分配给老年代。
-XX:SurvivorRatio=8是jvm默认的新生代中Eden和Survivor比例,默认为8:1。
原因是新生代中的对象98%都会在下一次GC的时候回收掉,所以很适合采用复制算法进行垃圾回收,所以新生代10M的内存中,8M是Eden,1M是Survivor,另外的1M是未使用配合复制算法的内存块,也是Survivor。

jvm参数如下

-verbose:gc 
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-Xms20m -Xmx20m -Xmn10m
-XX:+UseSerialGC

使用示例代码

public class ReflectTest {

    private static final int _1MB = 1024*1024;
    
    public static void testAllocation(){
        byte[] allocation1 , allocation2 , allocation3 , allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        ReflectTest.testAllocation();
    }
    
}

打出日志

[GC (Allocation Failure) [DefNew: 7291K->541K(9216K), 0.0026537 secs] 7291K->6685K(19456K), 0.0026909 secs] [Times: user=0.00 sys=0.02, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4719K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  52% used [0x00000000ff500000, 0x00000000ff587500, 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 2698K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

分配allocation4对象内存时新生代内存不足发生了MinorGC,因为都是存活对象,没有减少内存,只好把123晋升到老年代,在新生代安置allocation4

2.大对象直接进入老年代

大对象是指需要大量连续内存空间的Java对象,如字符串以及数组。虚拟机提供参数-XX:PretenureSizeThreshold参数来指定大对象,大于该值的对象都是大对象。如下我们指定大于3M的对象都是大对象,可以从打印日志中看出allocation3直接被分配到老年代中。
jvm参数如下

-verbose:gc 
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-Xms20m -Xmx20m -Xmn10m
-XX:+UseSerialGC
-XX:PretenureSizeThreshold=3145728

使用示例代码

public class ReflectTest {

    private static final int _1MB = 1024*1024;
    
    public static void testAllocation(){
        byte[] allocation1 , allocation2 , allocation3 , allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];
    }
    
    public static void main(String[] args) {
        ReflectTest.testAllocation();
    }
    
}

打出日志

Heap
 def new generation   total 9216K, used 7292K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  89% used [0x00000000fec00000, 0x00000000ff31f050, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4096K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  40% used [0x00000000ff600000, 0x00000000ffa00010, 0x00000000ffa00200, 0x0000000100000000)
 Metaspace       used 2662K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

可以看出没有发生GC,allocation4 直接被分配到老年代中

3.长期存活的对象进入老年代

既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代。为了做到这一点,虚拟机给每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活且能被Survivor容纳的话,将被移动到Survivor空间,并且对象年龄设为1,对象在Survivor区中每经过一次Minor GC,年龄就增加1岁,当年龄增加到一定程度(默认是15岁),就会晋升到老年代。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。
看如下实例
jvm参数如下

-verbose:gc 
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-Xms200m -Xmx200m -Xmn100m
-XX:+UseSerialGC
-XX:MaxTenuringThreshold=1
-XX:+PrintTenuringDistribution

使用示例代码

	public static void main(String[] args) {
		int _10MB = 10 * 1024 * 1024;
		/**
		 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
		 * -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
		 * -XX:+PrintTenuringDistribution 打印出对象具体年龄
		 * -Xms20M 设置堆大小为20M -Xmx20M 避免堆自动扩展
		 * -Xmn10M 设置年轻代大小 -XX:+PrintGCDetails 打印日志信息 -XX:SurvivorRatio=8
		 * 设置Eden和Survivor大小比值 -XX:MaxTenuringThreshold 当年龄大于该值时,放入老年代
		 */
		// TODO Auto-generated method stub
		@SuppressWarnings("unused")
		byte[] allocation1, allocation2, allocation3, allocation4;
		allocation1 = new byte[_10MB / 4];
		allocation2 = new byte[4 * _10MB];
		allocation3 = new byte[4 * _10MB]; // 新生代执行MinorGC,将allocation1移入Survivor,将allocation2移入老年代
		allocation3 = null;
		allocation3 = new byte[4 * _10MB]; // 直接在新生代上分配空间
	}

打出日志

[GC (Allocation Failure) [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 1)
- age   1:    3175752 bytes,    3175752 total
: 48435K->3101K(92160K), 0.0145482 secs] 48435K->44061K(194560K), 0.0145799 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 1)
: 44061K->0K(92160K), 0.0015922 secs] 85021K->44060K(194560K), 0.0016144 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 92160K, used 41779K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,  51% used [0x00000000f3800000, 0x00000000f60cce50, 0x00000000f8800000)
  from space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
  to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 tenured generation   total 102400K, used 44060K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
   the space 102400K,  43% used [0x00000000f9c00000, 0x00000000fc707190, 0x00000000fc707200, 0x0000000100000000)
 Metaspace       used 2697K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

分析一波,
新生代总共划分100m,因为设定了XX:SurvivorRatio=8,所以是80m的eden,10m的from和10m的to(划大点免得jvm自己占用的内存影响结果)
allocation1 = 2.5m,在哪个Survivor区中都能容纳,先在eden里分配allocation1 =2.5m,然后allocation2 =40m,这是剩余空间为
80m-40m-2.5m(实际上还有jvm自身启动的消耗),小于40m,所以分配allocation3 = 40m时,eden空间不足,发生MinorGC
这时候allocation1被丢到survivor区中,allocation2放不下了直接丢到老年代,allocation3丢到新生代,那还在新生代存活的allocation1经过了一次gc,寿命+1;

再把allocation3置为空,再重新分配40m空间,那新生代因为有jvm的空间消耗还是不够40m,再GC一次,这次把之前的40m丢掉,在新生代重新分配40m再扔给allocation3,注意此时survivor区里仍然是10m,放2.5m的allocation1 绰绰有余,但是allocation1 age到了设定的1,所以allocation1 也在这次MinorGC中被丢到老年代,我们可以看到,第二次MinorGC后新生代直接从44061K->0K

如果换一下参数XX:MaxTenuringThreshold=15,就会得到不一样的结果,如下示例:
jvm参数如下

-verbose:gc 
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-Xms200m -Xmx200m -Xmn100m
-XX:+UseSerialGC
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution

使用示例代码

	public static void main(String[] args) {
		int _10MB = 10 * 1024 * 1024;
		/**
		 * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails
		 * -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
		 * -XX:+PrintTenuringDistribution -Xms20M 设置堆大小为20M -Xmx20M 避免堆自动扩展
		 * -Xmn10M 设置年轻代大小 -XX:+PrintGCDetails 打印日志信息 -XX:SurvivorRatio=8
		 * 设置Eden和Survivor大小比值 -XX:MaxTenuringThreshold 当年龄大于该值时,放入老年代
		 */
		// TODO Auto-generated method stub
		@SuppressWarnings("unused")
		byte[] allocation1, allocation2, allocation3, allocation4;
		allocation1 = new byte[_10MB / 4];
		allocation2 = new byte[4 * _10MB];
		allocation3 = new byte[4 * _10MB]; // 新生代执行MinorGC,将allocation1移入Survivor,将allocation2移入老年代
		allocation3 = null;
		allocation3 = new byte[4 * _10MB]; // 直接在新生代上分配空间
	}

打出日志

[GC (Allocation Failure) [DefNew
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age   1:    3175752 bytes,    3175752 total
: 48435K->3101K(92160K), 0.0144757 secs] 48435K->44061K(194560K), 0.0145074 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
- age   2:    3174784 bytes,    3174784 total
: 44061K->3100K(92160K), 0.0014645 secs] 85021K->44060K(194560K), 0.0014864 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 92160K, used 44879K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,  51% used [0x00000000f3800000, 0x00000000f60cce50, 0x00000000f8800000)
  from space 10240K,  30% used [0x00000000f8800000, 0x00000000f8b07180, 0x00000000f9200000)
  to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 tenured generation   total 102400K, used 40960K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
   the space 102400K,  40% used [0x00000000f9c00000, 0x00000000fc400010, 0x00000000fc400200, 0x0000000100000000)
 Metaspace       used 2698K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

可以看出,第二次GC后,新生代44061K->3100K,结束后打出的 from space 10240K, 30% used也可以证明这次allocation1 仍在survivor区里,没有被升级到老年代,这就是jvm根据对象年龄升代的判定机制

4.动态对象年龄判定

为了能更好地适应不同程序的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。
示例如下:
jvm参数

-verbose:gc 
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-Xms200m -Xmx200m -Xmn100m
-XX:+UseSerialGC
-XX:MaxTenuringThreshold=15
-XX:+PrintTenuringDistribution

示例代码

	public static void main(String[] args) {
		int _10MB = 10 * 1024 * 1024;

		@SuppressWarnings("unused")
		byte[] allocation1, allocation2, allocation3, allocation4;
		allocation1 = new byte[_10MB / 4];
		allocation2 = new byte[_10MB / 4];
		allocation3 = new byte[4 * _10MB];
		allocation4 = new byte[4 * _10MB]; // 新生代执行MinorGC,将allocation1移入Survivor,将allocation2移入老年代
		allocation4 = null;
		allocation4 = new byte[4 * _10MB]; // 直接在新生代上分配空间
	}

打印日志如下

[GC (Allocation Failure) [DefNew
Desired survivor size 5242880 bytes, new threshold 1 (max 15)
- age   1:    5797208 bytes,    5797208 total
: 50995K->5661K(92160K), 0.0158614 secs] 50995K->46621K(194560K), 0.0158953 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[GC (Allocation Failure) [DefNew
Desired survivor size 5242880 bytes, new threshold 15 (max 15)
: 46621K->0K(92160K), 0.0022996 secs] 87581K->46620K(194560K), 0.0023277 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 92160K, used 41779K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,  51% used [0x00000000f3800000, 0x00000000f60cce50, 0x00000000f8800000)
  from space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
  to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 tenured generation   total 102400K, used 46620K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
   the space 102400K,  45% used [0x00000000f9c00000, 0x00000000fc9871a0, 0x00000000fc987200, 0x0000000100000000)
 Metaspace       used 2698K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

可以看出,设置的年龄并没有到,allocation1 +allocation2 也只有5m不会占满survivor,但是第二次gc jvm还是清空了survivor区移到了老年代(46621K->0K),运行结果也能看出 from space 10240K, 0% used,survivor区中的对象被移除了

这是因为allocation1 +allocation2 超过了单个survivor区大小的一半,所以在第二次GC时通过jvm的动态年龄判定机制直接被移入老年代了

5.空间分配担保

在发生Minor GC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者HandlePromotionFailure设置不允许冒险,那这时也要改为进行一次Full GC。

新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在MinorGC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁。
看如下示例
jvm参数

-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-Xms200m -Xmx200m -Xmn100m
-XX:+UseSerialGC
	public static void main(String[] args) {
		int _10MB = 10 * 1024 * 1024;

		@SuppressWarnings("unused")
		byte[] allocation1, allocation2, allocation3, allocation4, allocation5, allocation6, allocation7;
		allocation1 = new byte[2 * _10MB];
		allocation2 = new byte[2 * _10MB];
		allocation3 = new byte[2 * _10MB];
		allocation1 = null;
		allocation4 = new byte[2 * _10MB];
		allocation5 = new byte[2 * _10MB];
		allocation6 = new byte[2 * _10MB];
		allocation4 = null;
		allocation5 = null;
		allocation6 = null;
		allocation7 = new byte[2 * _10MB];
		
	}

运行到 allocation6 = new byte[2 * _10MB];步骤时打印的gc日志如下:

[GC (Allocation Failure) [DefNew: 66355K->541K(92160K), 0.0127715 secs] 66355K->41501K(194560K), 0.0128080 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 
Heap
 def new generation   total 92160K, used 64378K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,  77% used [0x00000000f3800000, 0x00000000f7657548, 0x00000000f8800000)
  from space 10240K,   5% used [0x00000000f9200000, 0x00000000f92874a8, 0x00000000f9c00000)
  to   space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
 tenured generation   total 102400K, used 40960K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
   the space 102400K,  40% used [0x00000000f9c00000, 0x00000000fc400020, 0x00000000fc400200, 0x0000000100000000)
 Metaspace       used 2698K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

运行结束时打印日志如下:

[GC (Allocation Failure) [DefNew: 64716K->536K(92160K), 0.0135333 secs] 64716K->41496K(194560K), 0.0135662 secs] [Times: user=0.00 sys=0.02, real=0.01 secs] 
[GC (Allocation Failure) [DefNew: 63573K->536K(92160K), 0.0005871 secs] 104533K->41496K(194560K), 0.0006086 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 92160K, used 22654K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,  27% used [0x00000000f3800000, 0x00000000f4d99b20, 0x00000000f8800000)
  from space 10240K,   5% used [0x00000000f8800000, 0x00000000f8886008, 0x00000000f9200000)
  to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 tenured generation   total 102400K, used 40960K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
   the space 102400K,  40% used [0x00000000f9c00000, 0x00000000fc400020, 0x00000000fc400200, 0x0000000100000000)
 Metaspace       used 2662K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

明显看出,第二次GC前,新生代 used 64378K,老年代 total 102400K, used 40960K,其实是不足以担保空间分配的,但是jvm根据上次gc晋升的空间经验预估此次晋升会成功,就直接进行了Minor GC

如果不执行
allocation4 = null;
allocation5 = null;
allocation6 = null;
会输出如下日志:

[GC (Allocation Failure) [DefNew: 66355K->541K(92160K), 0.0131315 secs] 66355K->41501K(194560K), 0.0131727 secs] [Times: user=0.00 sys=0.00, real=0.01 secs] 
[GC (Allocation Failure) [DefNew (promotion failed) : 63558K->63557K(92160K), 0.0129612 secs][Tenured: 81920K->81920K(102400K), 0.0036135 secs] 104518K->102939K(194560K), [Metaspace: 2691K->2691K(1056768K)], 0.0166134 secs] [Times: user=0.02 sys=0.00, real=0.02 secs] 
Heap
 def new generation   total 92160K, used 42318K [0x00000000f3800000, 0x00000000f9c00000, 0x00000000f9c00000)
  eden space 81920K,  51% used [0x00000000f3800000, 0x00000000f6153b30, 0x00000000f8800000)
  from space 10240K,   0% used [0x00000000f8800000, 0x00000000f8800000, 0x00000000f9200000)
  to   space 10240K,   0% used [0x00000000f9200000, 0x00000000f9200000, 0x00000000f9c00000)
 tenured generation   total 102400K, used 81920K [0x00000000f9c00000, 0x0000000100000000, 0x0000000100000000)
   the space 102400K,  80% used [0x00000000f9c00000, 0x00000000fec00040, 0x00000000fec00200, 0x0000000100000000)
 Metaspace       used 2698K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 288K, capacity 386K, committed 512K, reserved 1048576K

可以看出第二次GC时jvm仍然认为可以安全晋升,实际上是失败了,所以只好又进行了一次fullGC

jdk1.6以后的版本不再提供HandlePromotionFailure参数设置,默认设置为允许冒险

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值