标签:
本文是一次内部分享中总结了jvm gc的分类和一些实例, 内容是introduction级别的,供初学人士参考.
成文仓促,难免有些错误,如果有大牛发现,请留言,我一定及时更正,谢谢!
JVM内存布局主要包含下面几个部分:
Hotspot虚拟机使用分代收集算法,将Java Heap根据对象的存活周期分为多个区域:新生代、老生代和永生代。
新生代和老生代位于Java heap中,是垃圾收集器主要处理的内存区域。
永生代则基本上等价于Method Area,也就是说其中包含的数据在jvm进程存活期间会一直存在,一般不会发生变化。
java堆内存的布局如下图所示:
使用jstat可以查看某个java进程的内存状况:
1
2
3
|
chendeMacBook-Air:~ eleforest$ jstat -gc 16136 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 0.0 8192.0 2867.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 |
其中各个指标介绍如下:(单位为KB)
常用的垃圾收集器包括下面几个
下面将会用一段简单的程序演示jvm在配置使用不同的收集器情况下,GC行为的不同点,通过GC的行为能够了解到不同收集器的收集策略和行为。代码非常简单:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
//jvm basic args:-Xmx20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 public class Main { public static void main(String[] args) throws Exception { byte [] alloc1,alloc2,alloc3,alloc4; alloc1 = new byte [ 2 * 1024 * 1024 ]; Thread.sleep( 2000 ); alloc2 = new byte [ 2 * 1024 * 1024 ]; Thread.sleep( 2000 ); alloc3 = new byte [ 2 * 1024 * 1024 ]; Thread.sleep( 2000 ); alloc4 = new byte [ 2 * 1024 * 1024 ]; Thread.sleep( 2000 ); } } |
其中上例中的jvm参数解释如下:
Xmx | 最大堆容量,包含了新生代和老生代的堆容量 |
Xms | 最小堆容量,此时配置与Xmx一样,避免了申请空间时的堆扩展 |
Xmn | 新生代容量,包含eden,survivor1,survivor2三个区域 |
PrintGCDetails | 让jvm在每次发生gc的时候打印日志,利于分析gc的原因和状况 |
SurvivorRatio | 新生代中eden的比例,如果设置为8,意味着新生代中eden占据80%的空间,两个survivor分别占据10% |
测试环境为mac os 10.8,jdk版本如下:
1
2
3
4
|
chendeMacBook-Air:~ eleforest$ java -version java version "1.7.0_09" Java(TM) SE Runtime Environment (build 1.7.0_09-b05) Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode) |
直接运行上述代码,用jstat观察gc情况如下:
1
2
3
4
5
6
7
8
9
10
11
|
chendeMacBook-Air:~ eleforest$ jstat -gc 21729 1000 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 0.0 8192.0 819.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 2867.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 2867.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 4915.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 4915.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 6963.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 6963.9 10240.0 0.0 21248.0 2637.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 292.9 8192.0 2375.9 10240.0 6144.0 21248.0 2640.3 1 0.007 0 0.000 0.007 1024.0 1024.0 0.0 292.9 8192.0 2375.9 10240.0 6144.0 21248.0 2640.3 1 0.007 0 0.000 0.007 |
由上述的结果可见,程序启动时,Eden使用了819.9K的空间(我现在还不知道819k是什么东西的开销),S1、S2、老生代均没有占用,永生代则使用了2.6MB空间,其中包含了包含被虚拟机加载的类、常量、静态变量等数据。
随后连续三次申请了2MB的空间,这些数据都被放到了Eden区域,这就是jvm内存分配的第一个原则:对象优先在Eden分配,这个原则只在Eden空间足够,且申请的内存小于jvm参数PretenureSizeThreshold设置值时生效(根据采用的收集器不同,还会有很多不同情况)
注意看第四次申请2MB空间,此时由于Eden空间无法容纳新的数组,因此发生了一次Minor GC,具体的GC log如下所示:
1
2
3
4
5
6
7
8
9
10
11
|
[GC [DefNew: 6963K->292K(9216K), 0.0065350 secs] 6963K->6436K(19456K), 0.0065940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs] Heap def new generation total 9216K, used 2832K [0x0000000112230000, 0x0000000112c30000, 0x0000000112c30000) eden space 8192K, 31% used [0x0000000112230000, 0x00000001124aaf60, 0x0000000112a30000) from space 1024K, 28% used [0x0000000112b30000, 0x0000000112b793b0, 0x0000000112c30000) to space 1024K, 0% used [0x0000000112a30000, 0x0000000112a30000, 0x0000000112b30000) tenured generation total 10240K, used 6144K [0x0000000112c30000, 0x0000000113630000, 0x0000000113630000) the space 10240K, 60% used [0x0000000112c30000, 0x0000000113230030, 0x0000000113230200, 0x0000000113630000) compacting perm gen total 21248K, used 2647K [0x0000000113630000, 0x0000000114af0000, 0x0000000118830000) the space 21248K, 12% used [0x0000000113630000, 0x00000001138c5ec0, 0x00000001138c6000, 0x0000000114af0000) No shared spaces configured. |
其中第一行中的"DefNew"代表使用的收集器是Serial收集器,这次Minor GC使用copy算法,做了下面几件事情:
比较有意思的是,在我的机器上重新再跑一次示例程序,发生了不一致的gc行为:
1
2
3
4
5
6
7
8
9
10
11
|
[GC [PSYoungGen: 6963K->384K(9216K)] 6963K->6528K(19456K), 0.0052500 secs] [Times: user=0.01 sys=0.00, real=0.01 secs] [Full GC [PSYoungGen: 384K->0K(9216K)] [ParOldGen: 6144K->6436K(10240K)] 6528K->6436K(19456K) [PSPermGen: 2637K->2635K(21248K)], 0.0157270 secs] [Times: user=0.04 sys=0.00, real=0.02 secs] HeapA PSYoungGen total 9216K, used 2539K [0x00000001106d0000, 0x00000001110d0000, 0x00000001110d0000) eden space 8192K, 31% used [0x00000001106d0000,0x000000011094af60,0x0000000110ed0000) from space 1024K, 0% used [0x0000000110ed0000,0x0000000110ed0000,0x0000000110fd0000) to space 1024K, 0% used [0x0000000110fd0000,0x0000000110fd0000,0x00000001110d0000) ParOldGen total 10240K, used 6436K [0x000000010fcd0000, 0x00000001106d0000, 0x00000001106d0000) object space 10240K, 62% used [0x000000010fcd0000,0x0000000110319278,0x00000001106d0000) PSPermGen total 21248K, used 2645K [0x000000010aad0000, 0x000000010bf90000, 0x000000010fcd0000) object space 21248K, 12% used [0x000000010aad0000,0x000000010ad65688,0x000000010bf90000) |
GC log第一行的PSYoungGen意味着这次运行中jvm自动选择了Parallel Scavenge收集器,GC行为发生了变化,同样的内存请求,PS收集器除了一次Minor GC以外,还发生了一次Full GC。PS收集器的实现与serial不一致,其行为模式还需要进一步研究.
比较吊诡的是jvm的自动选择行为,我阅读了openjdk的源码,版本为:openjdk-7-fcs-src-b147-27_jun_2011
其中关于jvm自动选择gc的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
if (os::is_server_class_machine() && !force_client_mode ) { // If no other collector is requested explicitly, // let the VM select the collector based on // machine class and automatic selection policy. if (!UseSerialGC && !UseConcMarkSweepGC && !UseG1GC && !UseParNewGC && !DumpSharedSpaces && FLAG_IS_DEFAULT(UseParallelGC)) { if (should_auto_select_low_pause_collector()) { //如果需要低时延收集器,选择cms FLAG_SET_ERGO( bool , UseConcMarkSweepGC, true ); } else { //否则缺省使用ps收集器 FLAG_SET_ERGO( bool , UseParallelGC, true ); } no_shared_spaces(); } } |
如上所示,jvm在没有明确设置gc时会采用parallel scavenge作为缺省收集器。因此我机器上jvm自动选择gc的行为还需要进一步研究。
调整jvm的参数,添加-XX:+UseParNewGC,告诉jvm选择使用ParNew收集器,此时执行的结果与示例1中使用serial收集器的行为完全一样。这里不再赘述
调整jvm参数为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
-Xmx20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC< /pre > 此时启动示例程序,我们会看到如下的结果: <pre class= "brush:shell" >chendeMacBook-Air:~ eleforest$ jstat -gc 21729 1000 S0C S1C S0U S1U EC EU OC OU PC PU YGC YGCT FGC FGCT GCT 1024.0 1024.0 0.0 0.0 8192.0 820.2 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 2868.2 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 2868.2 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 4916.3 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 4916.3 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 6964.3 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 0.0 8192.0 6964.3 8192.0 0.0 21248.0 2638.2 0 0.000 0 0.000 0.000 1024.0 1024.0 0.0 320.1 8192.0 2375.9 8192.0 6146.1 21248.0 2641.2 1 0.009 2 0.001 0.010 1024.0 1024.0 0.0 320.1 8192.0 2375.9 8192.0 6146.1 21248.0 2641.2 1 0.009 2 0.001 0.010 1024.0 1024.0 0.0 320.1 8192.0 2375.9 8192.0 6146.1 21248.0 2641.2 1 0.009 4 0.002 0.011 1024.0 1024.0 0.0 320.1 8192.0 2375.9 8192.0 6146.1 21248.0 2641.2 1 0.009 4 0.002 0.011 1024.0 1024.0 0.0 320.1 8192.0 2375.9 8192.0 6146.1 21248.0 2641.2 1 0.009 6 0.003 0.012 |
也就是说到第四个2MB申请,老生代里使用6MB的数据之后,jvm还进行了6次full gc,这是由于cms特殊性导致的:cms为了保证进行gc时应用的低时延,要求在老生代中剩余充足的空间以备应用使用。这个特性可以用下列参数进行调整和限制
1
|
-XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly |
其中CMSInitiatingOccupancyFraction的缺省为68%。在我们的示例中,OU已经超过了这个限制,jvm试图去清理老生代,因此发生了多次full gc。
通过修改CMSInitiatingOccupancyFraction为80或者更高值,再次执行示例程序后不会再发生fullGC。
为了使应用平顺,CMS收集器的使用需要小心的调整堆空间的大小,太小的老生代可能会起到相反的效果,过高的CMSInitiatingOccupancyFraction也会导致回收数据时使应用无法正常工作。
以上便是我在这篇博客中想要分享的内容,做一些记录,也分享出来。
但是如分享中所说的,还有以下问题还没有搞清楚:
标签:
原文地址:http://www.cnblogs.com/hushaojun/p/4966749.html