0. 准备
0.1 三个属性(吞吐量、延迟、内存)中选择其中两个进行jvm调优,称之为GC调优3选2.
吞吐量:用户代码时间 / (用户代码执行时间 + 垃圾回收时间)。是评价垃圾收集器能力的重要指标之一,是不考虑垃圾收集引起的停顿时间或内存消耗,垃圾收集器能支撑应用程序达到的最高性能指标。吞吐量越高算法越好。
吞吐量优先的垃圾回收器:-XX:+UseParallelGC -XX:+UseParallelOldGC,即常规的(PS/PO)
响应时间优先的垃圾回收器:CMS、G1
0.2 常用性能调优工具
1. MAT
- 提示可能的内存泄露的点
2. jvisualvm
3. jconsole
4. Arthas
5. show-busy-java-threads
0.3 线上排查问题的一般流程
0.3.1 CPU占用过高排查流程
1. 利用 top 命令可以查出占 CPU 最高的的进程pid: top ; shift + p。(如果pid为 9876)
2. 然后查看该进程下占用最高的线程id【top -Hp 9876】
3. 假设占用率最高的线程 ID 为 6900,将其转换为 16 进制形式 (因为 java native 线程以 16 进制形式输出) 【printf '%x\n' 6900】
4. 利用 jstack 打印出 java 线程调用栈信息【jstack 9876 | grep '0x1af4' -A 50 --color】,这样就可以更好定位问题
0.3.2 内存占用过高排查流程
1. 利用 top 命令可以查出占 CPU 最高的的进程pid: top ; shift + m
2. 查看JVM堆内存分配情况:jmap -heap pid
3. 查看占用内存比较多的对象 jmap -histo pid | head -n 100
4. 查看占用内存比较多的存活对象 jmap -histo:live pid | head -n 100
1. JVM参数类型
- 1.1 标配参数
- -version
- -help
- java -showversion
- 1.2 X参数
- -Xint:解释执行
- -Xcomp:第一次使用就编译成本地代码
- -Xmixed:混合模式
- 1.3 XX参数
- 1.3.1 Boolean类型
- 公式:-XX:+ 或者-某个属性 + 表示开启,-表示关闭
- Case:-XX:-PrintGCDetails:表示关闭了GC详情输出
- 1.3.2 key-value类型
- 公式:-XX:属性key=属性value
- 不满意初始值,可以通过下列命令调整
- case:如何:-XX:MetaspaceSize=21807104:查看Java元空间的值
- eg: -Xms:-XX:initialHeapSize 初始堆空间 -Xmx:-XX:MaxHeapSize 堆最大值 -Xss:-XX:ThreadStackSize 栈空间
2. 查看jvm参数默认值
2.1 jps 和 jinfo
jps:查看java的后台进程 jinfo:查看正在运行的java程序
2.1.1 找到进程:jps -l
2.1.2 找到参数初始值:
jinfo -flag 具体参数 java进程编号
jinfo - flags java进程编号
2.2 PrintFlagsInitial 和 PrintFlagsFinal 和 PrintCommandLineFlags
- java -XX:+PrintFlagsInitial -version 初始值
- java -XX:+PrintFlagsFinal -version 最终值
- java -XX:+PrintCommandLineFlags -version 打印出JVM的默认的简单初始化参数(带垃圾回收器)
3. 常用jvm参数
- -Xms:初始化堆内存,默认为物理内存的1/64,等价于 -XX:initialHeapSize
- -Xmx:最大堆内存,默认为物理内存的1/4,等价于-XX:MaxHeapSize
- -Xss:设计单个线程栈的大小,一般默认为512K~1024K,等价于 -XX:ThreadStackSize
- 使用 jinfo -flag ThreadStackSize 会发现 -XX:ThreadStackSize = 0
- 这个值的大小是取决于平台的
- Linux/x64:1024KB
- OS X:1024KB
- Oracle Solaris:1024KB
- Windows:取决于虚拟内存的大小
- -Xmn:设置年轻代大小
- -XX:MetaspaceSize:设置元空间大小
- 元空间的本质和永久代类似,都是对JVM规范中方法区的实现,不过元空间与永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存,因此,默认情况下,元空间的大小仅受本地内存限制。
- -Xms10m -Xmx10m -XX:MetaspaceSize=1024m -XX:+PrintFlagsFinal
- 但是默认的元空间大小:只有20多M
- 为了防止在频繁的实例化对象的时候,让元空间出现OOM,因此可以把元空间设置的大一些
- -XX:PrintGCDetails:输出详细GC收集日志信息
- GC
- Full GC
+ gc耗时
4. OOM
### **什么情况下,会抛出OOM呢?**
- JVM98%的时间都花费在内存回收
- 每次回收的内存小于2%
满足这两个条件将触发OutOfMemoryException,这将会留给系统一个微小的间隙以做一些Down之前的操作,比如手动打印Heap Dump。并不是内存被耗空的时候才抛出
****
### **系统OOM之前的一些现象**
- 每次垃圾回收的时间越来越长,由之前的10ms延长到50ms左右,FullGC的时间也有之前的0.5s延长到4、5s
- FullGC的次数越来越多,最频繁时隔不到1分钟就进行一次FullGC
- 老年代的内存越来越大并且每次FullGC后,老年代只有少量的内存被释放掉
- java.lang.StackOverflowError 栈溢出
- java.lang.OutOfMemoryError:java heap space 堆溢出
- java.lang.OutOfMemoryError:GC overhead limit exceeeded
- java.lang.OutOfMemoryError:Direct buffer memory
- java.lang.OutOfMemoryError:unable to create new native thread
- java.lang.OutOfMemoryError:Metaspace
LearningNotes: Java学习笔记,主要来源于B站上视频的学习,同时会记录平时一些学习和项目中遇到的问题,同步更新在蘑菇博客,如果对我的博客网站感兴趣的话,欢迎关注我的 蘑菇博客项目 笔记主要涵盖:Java,Spring,SpringCloud,计算机网络,操作系统,数据结构,Vue等 如果笔记对您有帮助的话,欢迎star支持,谢谢~ - Gitee.com
4.1 StackoverFlowError
堆栈溢出,我们有最简单的一个递归调用,就会造成堆栈溢出,也就是深度的方法调用
栈一般是512K,不断的深度调用,直到栈被撑破
4.2 OutOfMemoryError
创建了很多对象,导致堆空间不够存储
4.3 GC overhead limit exceeded
GC回收时间过长时会抛出OutOfMemoryError,过长的定义是,超过了98%的时间用来做GC,并且回收了不到2%的堆内存
连续多次GC都只回收了不到2%的极端情况下,才会抛出。假设不抛出GC overhead limit 错误会造成什么情况呢?
那就是GC清理的这点内存很快会再次被填满,迫使GC再次执行,这样就形成了恶性循环,CPU的使用率一直都是100%,而GC却没有任何成果。
4.4 Direct buffer memory
Netty + NIO:这是由于NIO引起的
写NIO程序的时候经常会使用ByteBuffer来读取或写入数据,这是一种基于通道(Channel) 与 缓冲区(Buffer)的I/O方式,它可以使用Native 函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在Java堆和Native堆中来回复制数据。
ByteBuffer.allocate(capability):第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢
ByteBuffer.allocteDirect(capability):第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存的拷贝,所以速度相对较快
但如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,这时候怼内存充足,但本地内存可能已经使用光了,再次尝试分配本地内存就会出现OutOfMemoryError,那么程序就奔溃了。
一句话说:本地内存不足,但是堆内存充足的时候,就会出现这个问题
4.5 unable to create new native thread
不能够创建更多的新的线程了,也就是说创建线程的上限达到了
在高并发场景的时候,会应用到
高并发请求服务器时,经常会出现如下异常java.lang.OutOfMemoryError:unable to create new native thread
,准确说该native thread异常与对应的平台有关
导致原因:
- 应用创建了太多线程,一个应用进程创建多个线程,超过系统承载极限
- 服务器并不允许你的应用程序创建这么多线程,linux系统默认运行单个进程可以创建的线程为1024个,如果应用创建超过这个数量,就会报
java.lang.OutOfMemoryError:unable to create new native thread
解决方法:
- 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
- 对于有的应用,确实需要创建很多线程,远超过linux系统默认1024个线程限制,可以通过修改linux服务器配置,扩大linux默认限制
4.6 Metaspace
元空间内存不足,Matespace元空间应用的是本地内存
-XX:MetaspaceSize
的处理化大小为20M
元空间是什么
元空间就是我们的方法区,存放的是类模板,类信息,常量池等
Metaspace是方法区HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟内存中,而是使用本地内存,也即在java8中,class metadata(the virtual machines internal presentation of Java class),被存储在叫做Matespace的native memory
永久代(java8后背元空间Metaspace取代了)存放了以下信息:
- 虚拟机加载的类信息
- 常量池
- 静态变量
- 即时编译后的代码
5. jvm调优实战 OOM
5.0 准备
5.0.1 查看jvm参数默认值
5.0.2 找到出问题的类(dump/top+jmap)
利用dump文件找到哪个类占内存:
nohup java -jar -server -Xms8g -Xmx8g -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/heap.hprof ./test.jar >/dev/null 2>&1 &
XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath
利用工具MemoryAnalyzer进行分析。通过饼状图查看哪个类占内存。通过柱状图查看这类的相关引用。
5.1 jvm系统参数
增大,并设置为相同值,防止内存抖动
-Xms:初始堆大小
-Xmx:最大堆大小
5.2 CMS垃圾回收器参数
5.2.1 Folating garbage问题
-XX:CMSInitiating Occupancy Fraction 92%
降低这个值,让CMS保持老年代足够的空间
-XX:CMSInitiatingOccupancyFraction=70 是指设定CMS在对内存占用率达到70%的时候开始GC(因为CMS会有浮动垃圾,所以一般都较早启动GC);
5.2.2 Memory Fragmentation问题
-XX:+UseCMSCompactAtFullCollection开关参数,用于在“享受”完Full GC服务之后额外免费赠送一个碎片整理的过程,内存整理的过程无法并发的,空间碎片问题没有了,但停顿时间不得不变长了
-XX:CMSFullGCsBeforeCompaction 默认为0,指定多少次FGC后进行一次压缩
5.3 大对象,数组小数组。类别提取抽象类,行为定义接口。少定义静态static资源。分页查询。
6. 垃圾回收器
6.1 CMS:Concurrent Mark Sweep
优点:停顿低。缺点:并发执行CPU压力高。采用标记清除算法会导致碎片。
6.2 G1
6.3 比较
6.3.1 传统GC (G1之前)
- 单CPU或者小内存,单机程序
- -XX:+UseSerialGC
- 多CPU,需要最大的吞吐量,如后台计算型应用
- -XX:+UseParallelGC(这两个相互激活)
- -XX:+UseParallelOldGC
- 多CPU,追求低停顿时间,需要快速响应如互联网应用
- -XX:+UseConcMarkSweepGC
- -XX:+ParNewGC
参数 | 新生代垃圾收集器 | 新生代算法 | 老年代垃圾收集器 | 老年代算法 |
-XX:+UseSerialGC | SerialGC | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParNewGC | ParNew | 复制 | SerialOldGC | 标记整理 |
-XX:+UseParallelGC | Parallel [Scavenge] | 复制 | Parallel Old | 标记整理 |
-XX:+UseConcMarkSweepGC | ParNew | 复制 | CMS + Serial Old的收集器组合,Serial Old作为CMS出错的后备收集器 | 标记清除 |
-XX:+UseG1GC | G1整体上采用标记整理算法 | 局部复制 |
6.3.2 G1 & CMS
6.3.3 小总结
7. 垃圾清除算法
7.1 标记-清除(Mark-Sweep)算法
分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
它的主要不足有两个:
- 效率问题,标记和清除两个过程的效率都不高。
- 空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
7.2 复制算法
为了解决效率问题,一种称为复制(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。
复制算法的代价:是将内存缩小为了原来的一半,减少了实际可用的内存。现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
7.3 标记-整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。根据老年代的特点,有人提出了另外一种标记-整理(Mark-Compact)算法,标记过程仍然与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
7.4 分代收集算法
当前商业虚拟机的垃圾收集都采用分代收集(Generational Collection)算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用标记—清理或者标记—整理算法来进行回收。
8. 垃圾判断算法
8.1 引用计数法:循环引用问题
给对象中添加一个引用计数器, 每当有一个地方引用它时, 计数器值就加1; 当引用失效时, 计数器值就减1; 任何时刻计数器为0的对象就是不可能再被使用的。
问题:循环引用
8.2 跟可达算法
这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点, 从这些节点开始向下搜索, 搜索所走过的路径称为引用链( Reference Chain) , 当一个对象到GC Roots没有任何引用链相连( 用图论的话来说, 就是从GC Roots到这个对象不可达) 时, 则此对象是不可的。
即使在可达性分析算法中不可达的对象, 也并非是“非死不可”的, 这时候它们暂时处于“缓刑”阶段, 要真正宣告一个对象死亡, 至少要经历两次标记过程: 如果对象在进行可达性分析后发现没有与GC Roots相连接的引用链, 那它将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize( ) 方法。 当对象没有覆盖finalize( ) 方法, 或者finalize( ) 方法已经被虚拟机调用过, 虚拟机将这两种情况都视为“没有必要执行”。