零, 在 IDE 的后台打印 GC 日志:
既然学习 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, 为什么会有这个差异呢? 等我们在后面的文章中学习到了 GC 算法之后就明白了.
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 后, 打印类的信息.
例如:
二, 堆的分配参数:
1,-Xmx -Xms: 指定最大堆和最小堆
举例, 当参数设置为如下时:
-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"); // 当前可用的总空间
运行效果:
问题 1: -Xmx(最大堆空间)和 -Xms(最小堆空间)应该保持一个什么关系, 可以让系统的性能尽可能的好呢?
问题 2: 如果你要做一个 Java 的桌面产品, 需要绑定 JRE, 但是 JRE 又很大, 你如何做一下 JRE 的瘦身呢?
- 2,-Xmn,-XX:NewRatio,-XX:SurvivorRatio:
- -Xmn
设置新生代大小
-XX:NewRatio
新生代 (eden+2*s) 和老年代 (不包含永久区) 的比值
例如: 4, 表示新生代: 老年代 = 1:4, 即新生代占整个堆的 1/5
-XX:SurvivorRatio(幸存代)
设置两个 Survivor 区和 eden 的比值
例如: 8, 表示两个 Survivor:eden=2:8, 即一个 Survivor 占年轻代的 1/10
现在运行如下这段代码:
- 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, 堆的分配参数总结:
根据实际事情调整新生代和幸存代的大小
官方推荐新生代占堆的 3/8
幸存代占新生代的 1/10
在 OOM 时, 记得 Dump 出堆, 确保可以排查现场问题
6, 永久区分配参数:
-XX:PermSize -XX:MaxPermSize
设置永久区的初始空间和最大空间. 也就是说, 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:
设置栈空间的大小. 通常只有几百 K
决定了函数调用的深度
每个线程都有独立的栈空间
局部变量, 参数 分配在栈上
注: 栈空间是每个线程私有的区域. 栈里面的主要内容是栈帧, 而栈帧存放的是局部变量表, 局部变量表的内容是: 局部变量, 参数.
我们来看下面这段代码:(没有出口的递归调用)
- 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 次)
意味着函数调用的次数太深, 像这种递归调用就是个典型的例子.
参考资料:
《深入 JVM 内核原理诊断与优化》视频学习
http://www.cnblogs.com/smyhvae
来源: http://www.bubuko.com/infodetail-3301525.html