1 此文目的
本文不准备从盘古开天地开始讲述JVM的种种,相关的文章网上太多了,大多也无非转来转去,连图都差不多。笔者只整理个提纲挈领的学习路线指南,并对自己学习过程中遇到的坑和容易混淆和忽视的地方作个总结。
2 JVM内存模型
2.1 内存模型
内存区域划分有多个维度,相同区域在不同维度的名称并不一样。如下图所示
2.2 方法区和永久代
这两个概念,很多时候都被当做是同一个概念。实际上,“方法区”是java虚拟机规范中对存放类信息,字段,方法,常量,静态变量,接口和常量池的内存区域的定义,而“永久代”则是HotSpot VM在1.8版本以前对于方法区的具体实现。由于java虚拟机规范并没有对方法区的具体实现作限制,所以HotSpot VM和JRocket VM对于方法区的实现都是不一样的,JRocket中就没有永久代的概念。而在1.8及1.8以后的版本中,HotSpot VM用"元空间"--metaspace来代替永久代,实现方法区。 这个变化带来的就是VM参数的变化,所有的PermGen都被替换成了MetaSpace。并且metaSpace不再使用堆内存,而是使用系统内存。但是该发生的OOM一样会发生。原因也基本都是加载到内存中的 class 数量太多或者体积太大。
3.GC
3.1 GC算法
GC算法和GC收集器也是两个维度的概念。 GC算法包括清除算法(也叫标记清除算法),复制算法,标记-整理算法。 不同垃圾收集器针对不同的内存区域,采用不同的GC算法。 具体介绍,网上相关资料很多,可以参考这篇文章:blog.csdn.net/xiaoping091…
3.2 垃圾收集器
垃圾收集器经历了从串行收集器到并行收集器,再到并发收集器的进化过程。这三者的区别如下图所示
不同版本默认使用的垃圾收集器以及支持开发者定制的垃圾收集器都是不一样的 jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代) jdk1.9 默认垃圾收集器G1 与此同时,通过设置JVM参数也可以自己选择垃圾收集器。如要开启G1垃圾回收器,可以用-XX:+UseG1GC,支持G1垃圾回收器的JDK最低版本为JDK 7u4。在用户自己选择垃圾收集器的时候,要注意JDK版本的问题。 笔者用表格的形式列出了新生代和老年代的GC收集器的常见搭配方案:
3.3 Full GC触发条件
频繁FullGC导致的stop the world的现象,会大大影响系统的稳定性。尽管一代又一代的垃圾收集器的优化,使得stop the world的时间越来越短,但是在大型应用中,还是避之不及。 出发FullGC的情况有以下几种:
- System.gc()方法的调用
- 老年代不足
- 方法区不足
- concurrent mode failure concurrent mode failure是在执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足造成的(有时候“空间 不足”是CMS GC时当前的浮动垃圾过多导致暂时性的空间不足触发Full GC)。
- promotion failed minor gc时年轻代的存活区空间不足而晋升老年代,老年代又空间不足而触发full gc
- 统计得到的Minor GC晋升到旧生代的平均大小大于老年代的剩余空间 当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC)。
3.3.1 OOM的类型
通常情况下,JVM的GC机制能保证应用的正常运行,导致系统频繁FullGC的原因百分之九十都是内存溢出(OOM)。OOM分为以下几类:
- Java.lang.OutOfMemeoryError:Java heap space 堆空间的内存溢出,可能的原因是某个可达性分析认为不能被回收的对象随着时间推移变得越来越大,例如某个static类型的map对象,被不停地塞入键值对,也可能是大循环或者死循环不断创建对象,而对象分配内存的速度超过了GC清理内存的速度。
- Java.lang.OutOfMemeoryError:GC overhead limit exceeded 这种OOM异常是Hotspot VM 1.6定义的一个策略,通过统计GC时间来预测是否要OOM了,提前抛出异常,防止OOM发生。Sun 官方对此的定义是:“并行/并发回收器在GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存。用来避免内存过小造成应用不能正常工作。” 那么为什么会出现这种GC效率低下的现象呢?通常是因为老年代内存占有过多导致的频繁GC,这种情况下,可以增加-verbose:gc -XX:+PrintGCDetails来分析具体原因,也可以加-XX:+HeapDumpOnOutOfMemoryError,这样OOM时会自动做Heap Dump,第二种方法适用于所有OOM异常的排查。
- Java.lang.OutOfMemoryError: PermGen space(JAVA8引入了Metaspace区域)方法区内存溢出,通常是因为加载的类过多,可以先排除程序问题导致的重复类加载,或者加大方法区内存的分配
- Java.lang.OutOfMemoryError: unable to create new native thread 产生这种异常的原因是由于系统在不停地创建大量的线程,且不进行释放。
4. JVM调优
4.1 调优参数
正确设置JVM参数,可以尽可能多地避免系统资源浪费,尽可能详细地掌握系统运行情况,并且对可能出现的问题防患于未然。
Xms:堆初始空间
Xmx:堆最大空间
Xmn:年轻代大小
XX:MaxNewSize 新生代最大空间 建议设置为整个堆的1/3到1/4
XX:NewSize
XX:MaxTenuringThreshold survivor中到老年代中的年龄阈值
Xss:每个线程的栈大小
java -XX:+PrintCommandLineFlags -version 得到JDK建议的内存分配大小
tomcat设置catalina.sh:
export JAVA_OPTS="-server –Xms1024m -Xmx1024m -XX:+UseParallelOldGC -verbose:gc -Xloggc:../logs/gc.log
-XX:+PrintGCDetails -XX:+PrintGCTimeStamps"
-XX:+PrintCommandLineFlagsjvm参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断
4.2 JVM监控
-
1.本机环境下,推荐一款idea上的插件VisualVM Launcher,实际就是联动了JDK开发包中自带的jvisualvm.exe监控软件。也可以设置远程监控。具体使用方法,可以参考这篇文章https://blog.csdn.net/wngpenghao/article/details/82884874IDEA Java性能分析插件VisualVM Launcher 配置(JAVA VisualVM 与Jconsole配置相同)
-
2.Linux的相关命令: jstat命令可以对jvm从各维度进行统计,详细使用参考jstat命令查看jvm的GC情况
-
3.VM参数设置时,指定打印出gc日志 -Xloggc:../logs/gc.log -XX:+PrintGCDetails -XX:+PrintGCTimeStamps 详细的参数设置以及gc日志该如何阅读,可以参考java之GC日志该怎么看
4.3 JVM异常排查
- 保存dump 当使用监控软件或者命令查看发现JVM异常时,应第一时间保存下dump现场。 命令是jmap -dump:format=b,file=文件名[pid] pid是服务进程
如果是使用jvisualvm就更方便了,直接点击如图所示的按钮即可:
4.4 实战例子
由于实际工作中,能接触到JVM机会的机会并不多,所以笔者整理了一些经典实例
Metaspace溢出排查过程
分享一次 Java 内存泄漏的排查
一次生产的 JVM 优化案例
JVM成长之路,记录一次内存溢出导致频繁FGC的问题排查及解决
非常详细的jvm调优实例,性能瓶颈定位