Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。对象的内存分配,往大方向上讲,就是在堆上分配,对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓存,将按线程优先在TLAB上分配。少数情况下,也可能直接分配在老年代中。
一、对象优先在Eden分配
大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。
测试代码段
package GC;
public class TestAllocation {
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];
}
}
参数设置
-Xms20m
-Xmx20m
-verbose:gc
-Xloggc:E:\eclipse\adt-bundle-windows-x86_64-20131030\gcRecorder\gc.log
-Xmn10M
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
gc.log的内容
Java HotSpot(TM) 64-Bit Server VM (25.91-b14) for windows-amd64 JRE (1.8.0_91-b14), built on Apr 1 2016 00:58:32 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4080628k(826660k free), swap 8159380k(4248300k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
Heap
PSYoungGen total 9216K, used 7291K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
eden space 8192K, 89% used [0x00000000ff600000,0x00000000ffd1ef60,0x00000000ffe00000)
from space 1024K, 0% used [0x00000000fff00000,0x00000000fff00000,0x0000000100000000)
to space 1024K, 0% used [0x00000000ffe00000,0x00000000ffe00000,0x00000000fff00000)
ParOldGen total 10240K, used 4096K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
object space 10240K, 40% used [0x00000000fec00000,0x00000000ff000010,0x00000000ff600000)
Metaspace used 2639K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
我们会发现GC日志内容和书中所给的样例不太一样。这时由于我的虚拟机在没有指定收集器组合的情况下,默认使用"Parallel Scavenge"和"ParNew"收集器。收集器不同,处理的策略自然也不同。但内存构成是一样的。如下图
如果想要使用书上样例所说的“Serial/Serial Old”收集器,那么我们就必须手动去添加参数进行设置。
-XX:+UseSerialGC,虚拟机运行在Client模式下的默认值,Serial+Serial Old。
-XX:+UseParNewGC,ParNew+Serial Old,在JDK1.8被废弃,在JDK1.7还可以使用。
-XX:+UseConcMarkSweepGC,ParNew+CMS+Serial Old。
-XX:+UseParallelGC,虚拟机运行在Server模式下的默认值,Parallel Scavenge+Serial Old(PS Mark Sweep)。
-XX:+UseParallelOldGC,Parallel Scavenge+Parallel Old。
-XX:+UseG1GC,G1+G1。
我们把第一行添加到JVM的参数列表中。
运行后日志如下:
Java HotSpot(TM) 64-Bit Server VM (25.91-b14) for windows-amd64 JRE (1.8.0_91-b14), built on Apr 1 2016 00:58:32 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4080628k(260676k free), swap 8159380k(3351908k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
0.096: [GC (Allocation Failure) 0.096: [DefNew: 7127K->526K(9216K), 0.0047051 secs] 7127K->6670K(19456K), 0.0048400 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
def new generation total 9216K, used 4704K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 51% used [0x00000000fec00000, 0x00000000ff014930, 0x00000000ff400000)
from space 1024K, 51% used [0x00000000ff500000, 0x00000000ff5839c8, 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 2639K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
这里的JVM中多了“Allocation Failure”。它表示向young generation(eden)给新对象申请空间时young generation(eden)剩余的合适空间不够所需的大小导致的minor gc。
程序执行过程如下:
一开始的三个对象allocation1、allocation2、allocation3首先被添加到新生代(Eden)中。当要继续添加allocation时,发现Eden已经被占用6M了,剩余的2M不足以分配allocation4所需要的4M内存。因此发生了Minor GC。而在进行Minor GC时,虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(1MB),所以只好通过分配担保机制提前转移到老年代去。因此,新生代已使用的内存由7127K降为526K(这个应该是存在from space的数据,因为这个区域在GC之后还是占51%,说明没有被清除)。
这次GC结束后,4MB的对象顺利分配在Eden中,结果是Eden占用4MB,Survivor空闲,老年代被占用6MB。
补充:Minor GC和Full GC的区别
1.Minor GC:指发生在新生代的垃圾收集动作,因为Java对象大多具备朝生夕死的特性,所以Minor GC非常频繁,一般回收速度也比较快。
2.Major GC/Full GC:指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC。Major GC的速度一般会比Minor GC慢上10倍。
二、大对象直接分配到老年区
所谓的大对象是指需要大量连续内存空间的Java对象,典型的就是那种很长的字符串和数组。经常出现大对象容易导致内存还有不少空间时就提前出发垃圾收集以获取足够的连续空间来“安置”它们。(写程序的时候应当避免短命大对象)。
设置参数:-XX:PretenureSizeThreshold=x。x是一个表示大小的数值,令大于这个设置值的对象直接在老年代分配。这样就避免在Eden区和两个Survivor区之间发生大量的内存复制。
测试代码
package GC;
public class testPretenureSizeThreshold {
private static final int _1M = 1024*1024;
public static void main(String[] args) {
byte[] allocation1;
allocation1 = new byte[4*_1M];
}
}
参数设置:-XX:PretenureSizeThreshold=3145728(3145728就是3M,但是这里不能直接写3M)
运行后的gc.log如下
Java HotSpot(TM) 64-Bit Server VM (25.91-b14) for windows-amd64 JRE (1.8.0_91-b14), built on Apr 1 2016 00:58:32 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4080628k(572040k free), swap 8159380k(3352032k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:NewSize=10485760 -XX:PretenureSizeThreshold=3145728 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
Heap
def new generation total 9216K, used 1147K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)
eden space 8192K, 14% used [0x00000000fec00000, 0x00000000fed1eef0, 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 2639K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
我们会发祥the space 中被占据了40%的空间,也就是4MB。说明这个byte对象直接被分配到了老年区。
三、长期存活的对象将进入老年代
虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出现并经过第一次Minor GC后仍然存活,并且能够被Survivor容纳的话,将被移动到Survivor中,并且年龄设为1。对象在Survivor区每熬过一次Minor GC,nianling就增加1岁,当它的年龄增加到一定程度(默认为15岁)就会进入老年代中。对象晋升老年代的年龄阈值可以通过参数-XX:MaxTenuringThreshold设置。
测试代码:
package GC;
public class testTenuringThreshold {
private static final int _1M = 1024*1024;
public static void main(String[] args) {
byte[] allocation1,allocation2,allocation3;
allocation1 = new byte[_1M/4];
allocation2 = new byte[4*_1M];
allocation3 = new byte[4*_1M];
allocation3=null;
allocation3 = new byte[4*_1M];
}
}
参数设置:-XX:MaxTenuringThreshold=1
运行后gc.log
Java HotSpot(TM) 64-Bit Server VM (25.91-b14) for windows-amd64 JRE (1.8.0_91-b14), built on Apr 1 2016 00:58:32 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4080628k(705932k free), swap 8159380k(3567832k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:InitialTenuringThreshold=1 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=1 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
0.088: [GC (Allocation Failure) 0.088: [DefNew: 5335K->782K(9216K), 0.0057450 secs] 5335K->4878K(19456K), 0.0058814 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
0.095: [GC (Allocation Failure) 0.095: [DefNew: 4878K->0K(9216K), 0.0010286 secs] 8974K->4877K(19456K), 0.0010502 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 4877K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffac36b8, 0x00000000ffac3800, 0x0000000100000000)
Metaspace used 2639K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
参数设置:-XX:MaxTenuringThreshold=15
Java HotSpot(TM) 64-Bit Server VM (25.91-b14) for windows-amd64 JRE (1.8.0_91-b14), built on Apr 1 2016 00:58:32 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 4080628k(588196k free), swap 8159380k(3392672k free)
CommandLine flags: -XX:InitialHeapSize=20971520 -XX:MaxHeapSize=20971520 -XX:MaxNewSize=10485760 -XX:MaxTenuringThreshold=15 -XX:NewSize=10485760 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseSerialGC
0.106: [GC (Allocation Failure) 0.106: [DefNew: 5335K->782K(9216K), 0.0048209 secs] 5335K->4878K(19456K), 0.0049518 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
0.112: [GC (Allocation Failure) 0.112: [DefNew: 4878K->0K(9216K), 0.0017173 secs] 8974K->4877K(19456K), 0.0017617 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 4877K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)
the space 10240K, 47% used [0x00000000ff600000, 0x00000000ffac36b8, 0x00000000ffac3800, 0x0000000100000000)
Metaspace used 2639K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 285K, capacity 386K, committed 512K, reserved 1048576K
结果有误,allocation1还是被拿到老年代中了,而且年龄为1的对象内存总和也并没有超过Survivor区域的一半。是不是在新版本的JDK中对这方面内容做了一些改变?
四、动态对象年龄判定。
虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,如果在Survivor区中相同年龄(设年龄为age)的对象的所有大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就可以直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
五、空间分配担保
在进行Minor GC前虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看HandlePromotionFailure设置值是否允许担保失败。若HandlePromotionFailure=true,那么虚拟机会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小。如果大于,虚拟机将尝试进行一次Minor GC,尽管这次Minor GC是有风险的。(这里的风险就是万一担保失败,虚拟机要重新发起一次Full GC。相当于绕了一个大圈子,浪费了时间)如果小于,或者HandlePromotionFailure=false,那这时虚拟机会放弃Minor GC,转而进行一次Full GC。在大部分情况下我们会将HandlePromotionFailure打开,为了避免Full GC过于频繁冒一点风险也是可以接受的。整个执行过程图如下
不过在JDK6update24之后的版本中规则已经发生了改变。HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Full GC,否则将进行Minor GC。
补充:为何要有两个Survivor区?——简单说就是为了避免内存空间出现碎片化,具体请看https://blog.csdn.net/towads/article/details/79784249
在进行Minor GC的时候会对Eden space和from space两块区域进行垃圾回收,把存活对象放到to space中。两块Survivor区域轮流交换from space和to space的角色。
博客内容来自《深入理解Java虚拟机》