(一)学习JVM ——运行时数据区域 java
(四)学习JVM —— 内存分配与回收策略安全
对象的回收已经经过介绍回收算法与虚拟机,大体学习了一次。bash
对象的内存分配,往大方向讲,就是在堆上分配对象,主要分配在新生代的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技术能够实现快速的对象分配,而不须要任何的锁进行同步,也就是说,在对象分配的时候不用锁住整个堆,而只须要在本身的缓冲区分配便可。测试
少数状况下,也可能会直接分配在老年代中,分配规则不是百分百固定的,其细节取决于使用的是哪种垃圾收集器组合,还有虚拟机中的参数设置。spa
先看看GC日志是什么格式,在后面的例子中会对日志进行分析。.net
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区没有足够空间时进行分配时,虚拟机将会发起一次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),内存占用几乎没有减小(由于c一、c二、c3都是存活的对象)。此次发生的缘由是,分配c4的时候,发展Eden已经占用了6MB,剩余的空间不够分配4MB内存,所以发生Minor GC。GC期间,发现c一、c二、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 —— 内存分配与回收策略