(四)学习JVM —— 内存分配与回收策略

(一)学习JVM ——运行时数据区域

(二)学习JVM —— 垃圾回收机制

(三)学习JVM —— 垃圾回收器

(四)学习JVM —— 内存分配与回收策略

内存分配与回收

对象的回收已经通过介绍回收算法与虚拟机,大致学习了一次。

对象的内存分配,往大方向讲,就是在堆上分配对象,主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按县城优先在TLAB上分配。

JVM在内存新生代Eden Space中开辟了一小块线程私有的区域,称作TLAB(Thread-local allocation buffer)。默认设定为占用Eden Space的1%。在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享也适合被快速GC,所以对于小对象通常JVM会优先分配在TLAB上,并且TLAB上的分配由于是线程私有所以没有锁开销。因此在实践中分配多个小对象的效率通常比分配一个大对象的效率要高。
也就是说,Java中每个线程都会有自己的缓冲区称作TLAB(Thread-local allocation buffer),每个TLAB都只有一个线程可以操作,TLAB结合bump-the-pointer技术可以实现快速的对象分配,而不需要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只需要在自己的缓冲区分配即可。

少数情况下,也可能会直接分配在老年代中,分配规则不是百分百固定的,其细节取决于使用的是哪一种垃圾收集器组合,还有虚拟机中的参数设置。

理解GC日志

先看看GC日志是什么格式,在后面的例子中会对日志进行分析。

33.125: [GC [DefNew: 3324K->152K(3712K), 0.0025925 secs] 3324K->152K(11904K), 0.0031680 secs]
100.667: [Full GC [Tenured: 0K->210K(1024K), 0.01149142 secs] 4603K->210K(19456K), [Perm: 2999K->2999K(21248K)], 0.0150007 secs] [Times: user=0.01 sys=0.00 real=0.02 secs]

最前面的数字33.125和100.667,代表GC发生的时间,这个数字的含义是从虚拟机启动以来经过的秒数。

GC日志开头的"[GC"和"[Full GC"说明了这次垃圾回收的停顿类型,如果有Full代表发生了STW。

接下来"[DefNew"、"[Tenured"和"[Perm"表示GC发生的区域,DefNew=Default New Generation,代表新生代,如果用ParNew回收器,新生代叫"[ParNew"=Parallel New Generation,如果采用Parallel Scavenge回收器,新生代叫"PSYoungGen",老年代同理。

后面方括号里的3324K->152K(3712K)的意思是“GC前该内存区域已使用的容量”->"GC后该内存区域已使用的容量(该内存区域总容量)"。

方括号外面的3324K->152K(11904K)表示"GC前堆已使用的容量"->"GC后堆已使用的容量(堆总容量)"

再往后的"0.0031680 secs"表示本次GC所占用的时间,单位是秒。

有的回收器会带有"[Times: user=0.01 sys=0.00 real=0.02 secs]",这种输入与Linux的time命令输出一致,分别代表用户态消耗CPU时间,内核态消耗CPU时间,操作从开始到结束经过的墙钟时间(Wall Clock Time)。

对象在Eden分配

大多数情况下,对象在新生代Eden区分配。当Eden区没有足够空间时进行分配时,虚拟机将会发起一次Minor GC。

新生代GC (Minor GC)

指发生在新生代的垃圾回收动作,因为Java对象大多都具备朝生夕灭的特性,所以MinorGC非常频繁,一般回收速度也比较快。

虚拟机提供了-XX:+PrintGCDetails参数,告诉我们在发生垃圾回收行为时,打印内存回收日志,并在线程退出的时候输出当前内存各区域的分配情况,具体测试看下面代码,和GC日志:

package test;

public class Test1 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM参数:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8
	 * @param args
	 */
	public static void main(String[] args) {

		byte[] c1, c2, c3, c4;
		c1 = new byte[2 * _1MB];
		c2 = new byte[2 * _1MB];
		c3 = new byte[2 * _1MB];
		c4 = new byte[4 * _1MB]; // 出现 Minor GC

	}

}
[GC (Allocation Failure) [DefNew: 7292K->556K(9216K), 0.0021306 secs] 7292K->6700K(19456K), 0.0021570 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4734K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,  54% used [0x00000000ff500000, 0x00000000ff58b018, 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 2719K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

main方法中,尝试分配3个2M对象和1个4MB对象,根据设置,eden 8M,两个survivor分别是1M,新生代的可用空间是eden + 1个survivor = 9M。

在分配c4的时候会发生一次Minor GC,这个GC的结果是(日志第一行),新生代7292K->556K(9216K),内存占用几乎没有减少(因为c1、c2、c3都是存活的对象)。这次发生的原因是,分配c4的时候,发展Eden已经占用了6MB,剩余的空间不够分配4MB内存,因此发生Minor GC。GC期间,发现c1、c2、c3对象都是2MB,无法放入Survivor空间(因为Survivor只有1MB),所以只好通过分配担保将这3个2MB的对象转移到老年代。

GC结束后,c4顺利分配在Eden中,因此程序线程结束后,结果为"eden space ... 51%",老年代"the space 10240K ... 60%"

大对象在老年代分配

所以大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串以及数组。大对象堆虚拟机的分配来说是一个坏消息(更坏的消息就是遇到一群朝生夕阳灭的短命大对象),经常出现大对象容易导致内存还有不少空间,就提前出发垃圾回收来获取足够的连续空间分配它们。

虚拟机提供了一个-XX:PretenureSizeThreshold参数,令大于这个值的对象直接在老年代分配,避免Eden和Survivor发生大量的内存复制,具体测试看下面代码,和GC日志:

package test;

public class Test2 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM参数:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:PretenureSizeThreshold=3145728
	 */
	public static void main(String[] args) {

		byte[] c1;
		c1 = new byte[4 * _1MB];

	}

}
Heap
 def new generation   total 9216K, used 1312K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  16% used [0x00000000fec00000, 0x00000000fed481e8, 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 2718K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

代码中参数-XX:PretenureSizeThreshold=3145728是说大于3MB的对象直接分配到老年代(3145728=3*1024*1024),看日志中 "the space 1024K, 40% used"说明对象c1直接分配到了老年代。

长期存活的对象晋升到老年代

虚拟机采用分代收集的思想管理内存,在内存回收时就必须要能识别那些对象该分配在新生代,哪些对象该分配在老年代。虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过一次MinorGC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor并将年龄设置为1岁。对象在Survivor区每熬过一次Minor GC,就增加1岁,当它的年龄增加到一定程度(默认15岁),就晋升到老年代。对象晋升老年代的阀值,可以通过参数-XX:MaxTenuringThreshold设置。

测试将该参数设置为1,并观察代码与GC日志:

package test;

public class Test3 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM参数:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
	 */
	public static void main(String[] args) {

		byte[] c1, c2, c3;
		c1 = new byte[_1MB / 4];
		c2 = new byte[4 * _1MB];
		c3 = new byte[4 * _1MB]; // Minor GC
		c3 = null;
		c3 = new byte[4 * _1MB]; // Minor GC

	}

}
[GC (Allocation Failure) [DefNew: 5500K->811K(9216K), 0.0016828 secs] 5500K->4908K(19456K), 0.0017091 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 4908K->0K(9216K), 0.0005337 secs] 9004K->4906K(19456K), 0.0005447 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4906K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffacabc8, 0x00000000ffacac00, 0x0000000100000000)
 Metaspace       used 2719K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

一共发生了2次GC。分配c1只需要256KB,在第一次GC时,Survivor可以容纳c1,当第二次GC时,熬过1此GC的Survivor中的c1晋升到了老年代,所以结果为eden 51% used(存放c3),老年代10240K 47% used(存放c1和c2)。

动态对象年龄判定

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

具体看代码与GC日志:

package test;

public class Test4 {

	private static final int _1MB = 1024 * 1024;

	/**
	 * VM参数:-XX:+UseSerialGC -Xms20m -Xmx20m -Xmn10m -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15
	 */
	public static void main(String[] args) {

		byte[] c1, c2, c3;
		c1 = new byte[_1MB / 4];
		c2 = new byte[4 * _1MB];
		c3 = new byte[4 * _1MB]; // Minor GC
		c3 = null;
		c3 = new byte[4 * _1MB]; // Minor GC

	}

}
[GC (Allocation Failure) [DefNew: 5500K->811K(9216K), 0.0017106 secs] 5500K->4908K(19456K), 0.0017381 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[GC (Allocation Failure) [DefNew: 4908K->0K(9216K), 0.0005325 secs] 9004K->4906K(19456K), 0.0005422 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 def new generation   total 9216K, used 4178K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
  eden space 8192K,  51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
  from space 1024K,   0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)
  to   space 1024K,   0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)
 tenured generation   total 10240K, used 4906K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
   the space 10240K,  47% used [0x00000000ff600000, 0x00000000ffacabc8, 0x00000000ffacac00, 0x0000000100000000)
 Metaspace       used 2719K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 302K, capacity 386K, committed 512K, reserved 1048576K

代码中MaxTenuringThreshold参数已经设置成了15,但是发现在经过第二次GC后,Survivor中的c1依旧晋升到了老年代,这就是因为,c1与c2加起来大于Survivor空间的一般(大于512KB),并且它们是同年代的对象,满足同年对象达到Survivor空间的一般这种规则。如果注释到最后一行c3,就会发现结果不一样。

空间分配担保

在发生MinorGC之前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。

只要老年代的连续空间大于新生代对象总大小,或历次晋升的平均大小就会进行MinorGC,否则进行FullGC。

取平均晋升大小的值进行比较其实是一种动态概率的手段,也就是说,如果某次MinorGC内存后的对象突增,远远高于平均值的话,那就只好在重新发起一次Full GC。

(一)学习JVM ——运行时数据区域

(二)学习JVM —— 垃圾回收机制

(三)学习JVM —— 垃圾回收器

(四)学习JVM —— 内存分配与回收策略

猜你喜欢

转载自my.oschina.net/u/2450666/blog/1616414