1. 概述
内存泄漏:一块内存被占用后未释放,程序后续不在使用这块内存内容,导致这块内存一直被占用。
内存溢出:OOM,一个程序或进程分配的内存(内存或堆栈)不够使用,导致无法继续运行。大多数般情况下,内存泄漏都会导致OOM。
JVM调优就是通过调整一堆参数,来合理配置堆栈大小、垃圾回收器,使得程序运行的稳定、高效。
2. 垃圾回收(Garbage Collection)
2.1. 垃圾识别
我们将无用的引用对象称之为垃圾,那么GC如何区分有用的和无用的对象/数据引用呢?
两种方式:
1. 引用计数(Reference Count)
引用计数算法模型.png
环状垃圾.png
优点:简单,古老
缺点:对于环状的引用垃圾无法识别
2. 根可达(Root Searching)
根可达算法模型.png
什么是根root,JVMS(The Java Virtual Machine Specification)中定义如下:
- JVM stacks - 虚拟机栈中引用的对象
- native method stacks - 本地方法栈中引用的对象 JNI
- run-time constant pool - 常量池中常量引用的对象
- static references in method area, class - 类或方法中静态属性的引用对象
- 等等
JVM使用的就是“根可达”算法
2.2. 垃圾回收算法
以“根可达”算法派生的三种基础算法:
- 标记清除(Mark-Sweep)
从每个root出发,依次标记有引用关系的对象,没有标记的就是垃圾,最后将没有标记的对象清除。
优点:简单,速度快
缺点:随时间推移,内存碎片化严重,有可能大的对象找不到一块合适内存分配,易触发FGC。
Mark-Sweep.png
- 标记拷贝(Mark-Copy)
内存一份为二,每次只激活使用其中一块。回收时,将当前块中标记的有用对象快速拷贝到未使用的块中,完成后未使用块激活,使用块标记为未激活;下次清理时两块再次切换。JMM中的 Survivor 就是使用这个,在S0/S1之间来回切换。
优点:速度极快,没有碎片化产生
确定:内存利用率不高,至少有一半内存空闲
Mark-Copy.png
- 标记整理(Mark-Compact)
综合 1 2 优点,先标记出有用的块,然后将存活的对象整理到内存空间一端,使其形成连续的已使用空间,最后把已使用空间之外的全部区域清理掉。
缺点:效率较低
Mark-Compact.png
以上三种垃圾回收算法的不通运用于组合,产生了十种垃圾回收器
2.3. JVM内存模型
介绍垃圾回收器回收器之前先回顾下JVM内存模型:包含类加载器,执行器(解释执行,JIT编译执行),运行时内存布局。
注意:JVM内存模型与Java内存模型(JMM)是两个概念,JMM规范Java虚拟机与计算机内存如何协同工作,规定一个线程如何和何时可以看到其他线程修改过的共享变量值,以及在必须时如何同步共享变量。即程序如何保证操作的原子性、可见性、有序性。
JVM内存布局.png
JVM中对象创建分配管理流程.png
TLAB(Thread Local Allocation Buffer)在Eden中开辟了一小块线程私有的区域内存,为线程独享。默认设定为占用Eden Space的1%。
在Java程序中很多对象都是小对象且用过即丢,它们不存在线程共享,适合被快速GC(栈中弹出)
对象分配内存,为保证分配原子性,需要对区域加锁、失败重试(CAS)等方式。
2.4. 垃圾回收器(Garbage Collector)
十种垃圾回收器.png
JDK1.8 默认使用 ParallelScavenge + ParallelOld,生产上小于8G一般使用ParNew+CMS,8G及以上推荐使用G1
前六种组合方式:
- Serial+SerialOld
- ParallelScavenge+ParallelOld
- ParNew+CMS
2.4.1. Serial回收器
Serial是比较早期使用的垃圾收集器,工作于年轻代中,工作时 STW(stop the world),使用 Mark-Copy 算法的单线程垃圾收集器。应用于Young区域(YGC)
2.4.2. Serial Old回收器
与Serial类似,只不过它工作于老年代,也具有 STW 性质,使用Mark-Sweep标记清除或Mark-Compact标记整理算法的单线程垃圾收集器。
Serial+SerialOld往往组合使用,但在随着内存逐渐增大后,由于使用的是单线程进行GC,会导致STW的时间过于长,给用户的表现就是卡顿,卡死的状态。为此,诞生了并行的垃圾收集器
2.4.3. Parallel Scavenge回收器
一个具有 STW 性质,使用 Mark-Copy 算法的多线程垃圾收集器。工作于年轻代。
2.4.4. Parallel Old回收器
一个具有 STW 性质,使用 Mark-Compact 算法的多线程垃圾收集器。工作于老年代。
ParallelScavenge+ParallelOld往往组合使用,jdk1.8的默认。但并不是线程越多就能使STW的时间缩短,因为受限于CPU核数,若多于CPU核数,则会很多时间浪费在线程上下文切换上。
2.4.5. ParNew回收器
一个具有 STW 性质,使用 Mark-Copy 算法的多线程垃圾收集器。工作于年轻代。由于Parallel Scavenge与CMS配合不好,因此诞生了专门用于配合CMS垃圾收集器的ParNew垃圾收集器。
2.4.6. CMS回收器
CMS(Concurrent Mark Sweep)是一个并发收集器,STW停顿时间较短,使用 Mark-Sweep 算法。GC线程与工作线程可同时运行。
CMS回收器.png
CMS有四个阶段:
- 初始标记(Initial Mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要短暂停顿。
- 并发标记(Concurrent Mark):进行GC Roots Scan的过程,它在整个回收过程中耗时最长,不需要停顿,可与工作线程同时运行。
- 重新标记(Remark):修正并发标记中漏标的对象,需要STW。
- 并发清除(Concurrent Sweep):清理,无需停顿。
注:并发标记过程中会存在错标问题(本来是垃圾,但是标记完后,系统又产生了对它的引用)与漏标问题(本来不是垃圾,但在标记完后,不再有该对象的引用,即已经是垃圾,称之为浮动垃圾)。漏标问题可以在下一次GC就可以解决。而错标问题,则是CMS、G1、ZGC等最新的垃圾收集器所关注并要解决的问题,而这类垃圾收集器的区别就在于怎么解决错标问题。CMS和G1采用的都是三色标记法,而ZGC采用的是颜色指针法。
并发标记算法:三色扫描算法
三色标记完后,因为并发,可能存在漏标或错标的,需要进一步修正,
CMS使用三色标记 + Incremental Update修正
2.4.6.1. 三色标记
三色标记算法中的三色(白 灰 黑)是逻辑上的概念。如下图所示,
三色标记模型.png
A自己已经访问过了,同时它的成员变量也已经都访问过了(即标记完成),则此时A为黑色;
B自己已经标记过了,但是成员变量尚未标记,此时B为灰色;
D是没有标记过得节点,此时为白色。
2.4.7. G1回收器
G1(Garbage-First Garbage Collector),与CMS相比,G1具有压缩功能,避免碎片问题,G1的暂停时间更加可控。G1是1.7推出,1.8成熟。
G1模型.png
G1将堆分成若干大小相同的小块,即region,每个region可被标记为Edge、Survivor、Old、Humongous四种类型(Humongous是特殊的Old,可以跨越连续的region,用来存特大对象),未标记代表空闲区域。回收时,G1采用 Mark-Copy 优先回收垃圾最多的region,因此不会产生碎片,并停顿时间可预测。jdk11已将G1作为默认GC。
G1有五个阶段:
- 初始标记(Initial Mark):仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要短暂停顿。
- 根区域扫描(Root Region Scan):不会STW,并发标记从上一阶段扫描被引用的老年代对象。
- 并发标记(Concurrent Mark):与CMS类似。
- 重新标记(Remark):需要STW,修正并发标记中漏标的对象,完成最终标记。
- 并发清除(Concurrent Sweep):清理,无需停顿。
G1使用三色标记 + SATB修正(为了配合G1的RSet,效率更高)
2.4.8. 以上回收器对比
| 回收器 | 串行/并行 | 分代 | 算法 | 适用内存 | 目标 | 适应场景 |
|---|---|---|---|---|---|---|
| Serial | 串行 | Y | Mark-Copy | 几十M | 速度优先 | 单CPU环境下的Client模式 |
| Serial Old | 串行 | O | Mark-Sweep / Mark-Compact | 几十M | 速度优先 | 单CPU环境下的Client模式,CMS降级预案 |
| Parallel Scavenge | 并行 | Y | Mark-Copy | 几个G | 吞吐量优先 | 在后台运算,没有太多交互任务 |
| Parallel Old | 并行 | O | Mark-Compact | 几个G | 吞吐量优先 | 在后台运算,没有太多交互任务 |
| ParNew | 并行 | Y | Mark-Copy | 几个G | 速度优先 | 多CPU,Server模式,同CMS搭配 |
| CMS | 并发 | O | Mark-Sweep | 20G | 速度优先 | 多用与网站,B/S模型的服务器 |
| G1 | 并发 | YO | Mark-Copy | 上百G | 速度优先 | 大内存,低延迟,高吞吐的服务器 |
| ZGC | 并发 | YO | - | 4T - 16T | 速度优先 | - |
并发:串行+并行
Y:年轻代 O:老年代
3. 参数调整
- JVM的命令行参数参考:https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
- HotSpot参数分类
标准: - 开头,所有的HotSpot都支持,如
java -version非标准:-X 开头,特定版本HotSpot支持特定命令
java -Xmixed高级:-XX 开头,也是不稳定的,下个版本可能取消
java -XX:+UseG1GC参数类型:
- boolean类型:
+表示true添加,-表示false取消; - 非boolean类型(key=value):使用
-XX:xx=value形式设置
- boolean类型:
java -XX:+PrintFlagsWithComments //只有debug版本能用
假如运行了 java HelloGC 程序:
-
java -XX:+PrintCommandLineFlags HelloGC打印命令行参数(可以看默认垃圾回收器) -
java -Xmn10M -Xms40M -Xmx60M -XX:+PrintCommandLineFlags -XX:+PrintGC HelloGC
PrintGCDetails PrintGCTimeStamps PrintGCCauses java -XX:+UseConcMarkSweepGC -XX:+PrintCommandLineFlags HelloGC-
java -XX:+PrintFlagsInitial默认参数值 -
java -XX:+PrintFlagsFinal -version最终参数值(= 没有修改过 := 人为修改过)
下面介绍部分参数,以jdk1.8为例。
3.1. GC参数
推荐网站:https://perfma.com/
- -XX:+UseSerialGC = Serial New (DefNew) + Serial Old
- 小型程序。默认情况下不会是这种选项,HotSpot会根据计算及配置和JDK版本自动选择收集器
- -XX:+UseParNewGC = ParNew + SerialOld
- 这个组合已经很少用(在某些版本中已经废弃)
- -XX:+UseConcMarkSweepGC = ParNew + CMS + Serial Old
- -XX:+UseParallelGC = Parallel Scavenge + Parallel Old (1.8默认) 【PS + SerialOld】
- -XX:+UseParallelOldGC = Parallel Scavenge + Parallel Old
- -XX:+UseG1GC = G1
设定日志参数
-Xloggc:/opt/xxx/logs/xxx-xxx-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogFiles=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCause
3.2. 堆内存参数
一般生产上,会把最小内存和最大内存设置一样,避免JVM堆栈弹性收缩扩展,影响服务性能。
JVM内存参数设置.png
4. 问题排查
内存溢出OOM,CPU飙高100%
一般四步:
- 获取进程号PID
- 根据PID找到线程
- 找到线程堆栈
- 根据堆栈信息,结合代码和应用场景等,来详细分析排查原因(最难最耗时的环节,经验很重要)
4.1. 工具介绍
4.1.1. 命令工具
JDK自带了一些工具命令
-
jps:查看当前机器jvm进程 -
jinfo:查实时参数信息,并可修改 -
jmap:生成推存储快照,该命令一执行,JVM就暂停,生产一般不用 -
jstat:查询JVM性能统计信息 -
jstack: 查询进程/线程的堆栈信息。
4.1.2. 可视化工具
- jconsole : jdk自带,一般用于本地开发环境测试。
- jvisualvm : jdk自带,远程连接
- jhat : jdk自带,将dump文件以html形式展示
- jprofiler :(收费)
- arthas : 阿里出的,在线排查工具,通过命令操作。需要安装到服务器,一般生产上运维可以这么搞。
- Memory Analyer Tools(MAT):eclipse的内存分析工具
4.2. OOM
OOM不用看就是java程序,jps查看进程号
收集dump文件(一般生产都会配置 -XX:+HeapDumpOnOutOfMemoryError 出现OOM时自动打印dump文件)
分析dump,找到实例数最多,占用内存过多的对象
从这些实例对象入手,分析原因
4.3. CPU飙高
CPU飙高一般都是程序运行出现 while(true) 或类似while(true)的情况。比如死锁,链表上出现环遍历等。
定位思路:
查进程:看下是哪个应用导致在大量消耗CPU资源,top + jps 查看进程号
查线程:定位是哪个线程
查堆栈:根据堆栈信息,综合分析,猜测可能产生的原因
查源码:根据堆栈信息反查源码,分析哪些可疑代码在哪些场景下可能出现死循环












网友评论