本文主要内容:
Trace跟踪参数
堆的分配参数
栈的分配参数
JVM其他参数
既然学习JVM,阅读GC日志是处理Java虚拟机内存问题的基础技能,它只是一些人为确定的规则,没有太多技术含量。
既然如此,那么在IDE的控制台打印GC日志是必不可少的了。现在就告诉你怎么打印。
(1)如果你用的是Eclipse,打印GC日志的操作如下:
在上图的箭头处加上-XX:+PrintGCDetails这句话。于是,运行程序后,GC日志就可以打印出来了:
(2)如果你用的是IntelliJ IDEA,打印GC日志的操作如下:
在上图的箭头处加上-XX:+PrintGCDetails这句话。于是,运行程序后,GC日志就可以打印出来了:
当然了,光有-XX:+PrintGCDetails这一句参数肯定是不够的,下面我们详细介绍一下更多的参数配置。
一、Trace跟踪参数:
1、打印GC的简要信息:
-verbose:gc
-XX:+printGC
解释:可以打印GC的简要信息。比如:
[GC 4790K->374K(15872K), 0.0001606 secs]
[GC 4790K->374K(15872K), 0.0001474 secs]
[GC 4790K->374K(15872K), 0.0001563 secs]
[GC 4790K->374K(15872K), 0.0001682 secs]
上方日志的意思是说,GC之前,用了4M左右的内存,GC之后,用了374K内存,一共回收了将近4M。内存大小一共是16M左右。
2、打印GC的详细信息:
-XX:+PrintGCDetails
解释:打印GC详细信息。
-XX:+PrintGCTimeStamps
解释:打印CG发生的时间戳。
理解GC日志的含义:
例如下面这段日志:
[GC[DefNew: 4416K->0K(4928K), 0.0001897 secs] 4790K->374K(15872K), 0.0002232 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
上方日志的意思是说:这是一个新生代的GC。方括号内部的“4416K->0K(4928K)”含义是:“GC前该内存区域已使用容量->GC后该内存区域已使用容量(该内存区域总容量)”。而在方括号之外的“4790K->374K(15872K)”表示“GC前Java堆已使用容量->GC后Java堆已使用容量(Java堆总容量)”。
再往后看,“0.0001897 secs”表示该内存区域GC所占用的时间,单位是秒。
再比如下面这段GC日志:
上图中,我们先看一下用红框标注的“[0x27e80000, 0x28d80000, 0x28d80000)”的含义,它表示新生代在内存当中的位置:第一个参数是申请到的起始位置,第二个参数是申请到的终点位置,第三个参数表示最多能申请到的位置。上图中的例子表示新生代申请到了15M的空间,而这个15M是等于:(eden space的12288K)+(from space的1536K)+(to space的1536K)。
疑问:分配到的新生代有15M,但是可用的只有13824K,为什么会有这个差异呢?
3、指定GC log的位置:
-Xloggc:log/gc.log
解释:指定GC log的位置,以文件输出。帮助开发人员分析问题。
-XX:+PrintHeapAtGC
解释:每一次GC前和GC后,都打印堆信息。
例如:
上图中,红框部分正好是一次GC,红框部分的前面是GC之前的日志,红框部分的后面是GC之后的日志。
-XX:+TraceClassLoading
解释:监控类的加载。
例如:
[Loaded java.lang.Object from shared objects file]
[Loaded java.io.Serializable from shared objects file]
[Loaded java.lang.Comparable from shared objects file]
[Loaded java.lang.CharSequence from shared objects file]
[Loaded java.lang.String from shared objects file]
[Loaded java.lang.reflect.GenericDeclaration from shared objects file]
[Loaded java.lang.reflect.Type from shared objects file]
-XX:+PrintClassHistogram
解释:按下Ctrl+Break后,打印类的信息。
例如:
二、堆的分配参数:
JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位操作系统虽然寻址空间大小是4G(2^32),但具体操作系统会给一个限制(一般Windows系统有2GB内核空间,故用户空间限制在1.5G~2G,Linux系统有1GB内核空间,故用户空间是2G~3G);64为操作系统对内存无限制。所有线程共享数据区大小=新生代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m。所以java堆中增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为java堆的3/8。
1、-Xmx –Xms:指定java堆最大值(默认值是物理内存的1/4(<1GB))和初始java堆最小值(默认值是物理内存的1/64(<1GB))
默认(MinHeapFreeRatio参数可以调整)空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制.,默认(MaxHeapFreeRatio参数可以调整)空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。开发过程中,通常会将 -Xms 与 -Xmx两个参数的配置相同的值,其目的是为了能够在java垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源。
注意:此处设置的是Java堆大小,也就是新生代大小 + 年老代大小
举例、当参数设置为如下时:
-Xmx20m -Xms5m
然后我们在程序中运行如下代码:
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系统的最大空间
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系统的空闲空间
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间
运行效果:
保持参数不变,在程序中运行如下代码:(分配1M空间给数组)
byte[] b = new byte[1 * 1024 * 1024];System.out.println("分配了1M空间给数组");
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系统的最大空间
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系统的空闲空间
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M");
运行效果:
注:Java会尽可能将total mem的值维持在最小堆。
保持参数不变,在程序中运行如下代码:(分配10M空间给数组)
byte[] b = new byte[10 * 1024 * 1024];System.out.println("分配了10M空间给数组");
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系统的最大空间
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系统的空闲空间
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间
运行效果:
如上图红框所示:此时,total mem 为7M时已经不能满足需求了,于是total mem涨成了16.5M。
保持参数不变,在程序中运行如下代码:(进行一次GC的回收)
System.gc();
System.out.println("Xmx=" + Runtime.getRuntime().maxMemory() / 1024.0 / 1024 + "M"); //系统的最大空间
System.out.println("free mem=" + Runtime.getRuntime().freeMemory() / 1024.0 / 1024 + "M"); //系统的空闲空间
System.out.println("total mem=" + Runtime.getRuntime().totalMemory() / 1024.0 / 1024 + "M"); //当前可用的总空间
运行效果:
2、-Xmn、-XX:NewRatio、-XX:SurvivorRatio、-XXNewSize、-XX:MaxNewSize:
-Xmn
设置新生代大小,大小是:eden+ 2 survivor space
所有共享数据区大小=年轻代大小 + 年老代大小 + 持久代大小。一般永久代大小固定,所以增大年轻代后,将会减小年老代大小,此值对系统性能影响较大,Sun官方推荐配置为整个java堆的3/8
-XX:NewRatio
新生代(eden+2*Survivor)和老年代(不包含永久区)的比值
例如:-XX:NewRatio=4,表示新生代:老年代=1:4,即新生代占整个堆的1/5。在Xms=Xmx并且设置了Xmn的情况下,该参数不需要进行设置。
-XX:SurvivorRatio(幸存代)
设置两个Survivor区和eden的比值
例如:8,表示两个Survivor:eden=2:8,即一个Survivor占年轻代的1/10
-XX:NewSize
设置年轻代大小
-XX:MaxNewSize
设置年轻代最大值
现在运行如下这段代码:
public class JavaTest {
public static void main(String[] args) {
byte[] b = null;
for (int i = 0; i < 10; i++)
b = new byte[1 * 1024 * 1024];
}
}
我们通过设置不同的jvm参数,来看一下GC日志的区别。
(1)当参数设置为如下时:(设置新生代为1M,很小)
-Xmx20m -Xms20m -Xmn1m -XX:+PrintGCDetails
运行效果:
总结:
没有触发GC
由于新生代的内存比较小,所以全部分配在老年代。
(2)当参数设置为如下时:(设置新生代为15M,足够大)
-Xmx20m -Xms20m -Xmn15m -XX:+PrintGCDetails
运行效果:
上图显示:
没有触发GC
全部分配在eden(蓝框所示)
老年代没有使用(红框所示)
(3)当参数设置为如下时:(设置新生代为7M,不大不小)
-Xmx20m -Xms20m –Xmn7m -XX:+PrintGCDetails
运行效果:
总结:
进行了2次新生代GC
s0 s1 太小,需要老年代担保
(4)当参数设置为如下时:(设置新生代为7M,不大不小;同时,增加幸存代大小)
-Xmx20m -Xms20m -Xmn7m -XX:SurvivorRatio=2 -XX:+PrintGCDetails
运行效果:
总结:
进行了至少3次新生代GC
s0 s1 增大
(5)当参数设置为如下时:
-Xmx20m -Xms20m -XX:NewRatio=1
-XX:SurvivorRatio=2 -XX:+PrintGCDetails
运行效果:
(6)当参数设置为如下时: 和上面的(5)相比,适当减小幸存代大小,这样的话,能够减少GC的次数
-Xmx20m -Xms20m -XX:NewRatio=1
-XX:SurvivorRatio=3 -XX:+PrintGCDetails
3、-XX:+HeapDumpOnOutOfMemoryError、-XX:+HeapDumpPath
-XX:+HeapDumpOnOutOfMemoryError
OOM时导出堆到文件
根据这个文件,我们可以看到系统dump时发生了什么。
-XX:+HeapDumpPath
导出OOM的路径
例如我们设置如下的参数:
-Xmx20m -Xms5m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=d:/a.dump
上方意思是说,现在给堆内存最多分配20M的空间。如果发生了OOM异常,那就把dump信息导出到d:/a.dump文件中。
然后,我们执行如下代码:
Vector v = new Vector();
for (int i = 0; i < 25; i++)
v.add(new byte[1 * 1024 * 1024]);
上方代码中,需要利用25M的空间,很显然会发生OOM异常。现在我们运行程序,控制台打印如下:
现在我们去D盘看一下dump文件:
上图显示,一般来说,这个文件的大小和最大堆的大小保持一致。
我们可以用VisualVM打开这个dump文件。
注:关于VisualVM的使用,可以参考下面这篇博客:
使用 VisualVM 进行性能分析及调优:http://www.ibm.com/developerworks/cn/java/j-lo-visualvm/
或者使用Java自带的Java VisualVM工具也行:
上图中就是dump出来的文件,文件中可以看到,一共有19个byte已经被分配了。
4、-XX:OnOutOfMemoryError:
-XX:OnOutOfMemoryError
在OOM时,执行一个脚本。
可以在OOM时,发送邮件,甚至是重启程序。
例如我们设置如下的参数:
-XX:OnOutOfMemoryError=D:/tools/jdk1.7_40/bin/printstack.bat %p //p代表的是当前进程的pid
上方参数的意思是说,执行printstack.bat脚本,而这个脚本做的事情是:D:/tools/jdk1.7_40/bin/jstack -F %1 > D:/a.txt,即当程序OOM时,在D:/a.txt中将会生成线程的dump。
5、堆的分配参数总结:
根据实际事情调整新生代和幸存代的大小
官方推荐新生代占java堆的3/8
幸存代占新生代的1/10
在OOM时,记得Dump出堆,确保可以排查现场问题
6、永久区分配参数:
-XX:PermSize -XX:MaxPermSize
设置永久区的初始空间(默认为物理内存的1/64)和最大空间(默认为物理内存的1/4)。也就是说,jvm启动时,永久区一开始就占用了PermSize大小的空间,如果空间还不够,可以继续扩展,但是不能超过MaxPermSize,否则会OOM。
他们表示,一个系统可以容纳多少个类型
代码举例:
我们知道,使用CGLIB等库的时候,可能会产生大量的类,这些类,有可能撑爆永久区导致OOM。于是,我们运行下面这段代码:
for(int i=0;i<100000;i++){
CglibBean bean = new CglibBean("geym.jvm.ch3.perm.bean"+i,new HashMap());
}
上面这段代码会在永久区不断地产生新的类。于是,运行效果如下:
总结:
如果堆空间没有用完也抛出了OOM,有可能是永久区导致的。
堆空间实际占用非常少,但是永久区溢出 一样抛出OOM。
三、栈的分配参数:
1、-Xss:
设置每个线程栈空间的大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右
决定了函数调用的深度
每个线程都有独立的栈空间
局部变量、参数 分配在栈上
注:栈空间是每个线程私有的区域。栈里面的主要内容是栈帧,而栈帧存放的是局部变量表,局部变量表的内容是:局部变量、参数。
我们来看下面这段代码:(没有出口的递归调用)
public class TestStackDeep { private static int count = 0;
public static void recursion(long a, long b, long c) { long e = 1, f = 2, g = 3, h = 4, i = 5, k = 6, q = 7, x = 8, y = 9, z = 10; count++; recursion(a, b, c); }
public static void main(String args[]) { try { recursion(0L, 0L, 0L); } catch (Throwable e) { System.out.println("deep of calling = " + count); e.printStackTrace(); } }}
上方这段代码是没有出口的递归调用,肯定会出现OOM的。
如果设置栈大小为128k:
-Xss128K
运行效果如下:(方法被调用了294次)
如果设置栈大小为256k:(方法被调用748次)
意味着函数调用的次数太深,像这种递归调用就是个典型的例子。
2、-XXThreadStackSize:
设置线程栈的大小(0 means use default stack size)
一、JVM其他参数:
1、-XXThreadStackSize:
设置内存页的大小,不可设置过大,会影响Perm的大小
2、-XX:+UseFastAccessorMethods:
设置原始类型的快速优化
3、-XX:+DisableExplicitGC:
设置关闭System.gc()(这个参数需要严格的测试)
4、-XX:MaxTenuringThreshold
设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代. 对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概率。该参数只有在串行GC时才有效.
5、-XX:+AggressiveOpts
加快编译
6、-XX:+UseBiasedLocking
锁机制的性能改善
7、-Xnoclassgc
禁用垃圾回收
8、-XX:SoftRefLRUPolicyMSPerMB
设置每兆堆空闲空间中SoftReference的存活时间,默认值是1s 。(softly reachable objects will remain alive for some amount of time after the last time they were referenced. The default value is one second of lifetime per free megabyte in the heap)
9、-XX:PretenureSizeThreshold
设置对象超过多大时直接在旧生代分配,默认值是0。
10、-XX:TLABWasteTargetPercent
设置TLAB占eden区的百分比,默认值是1% 。
11、-XX:+CollectGen0First
设置FullGC时是否先YGC,默认值是false。