JVM常见的两个性能问题
JVM调优的目的在于提升性能,减少内存溢出OOM(OutOfMemoryError)和Full GC(Full Garbage Collection)的次数,提升程序吞吐量。
什么是OOM(OutOfMemoryError)?
OOM(OutOfMemoryError)是Java虚拟机(JVM)在内存不足时抛出的一种错误。当 JVM 无法分配新对象或无法扩展其内存时,会抛出此错误。OOM通常表示应用程序的内存使用超出了 JVM 的内存限制,或者存在内存泄漏等问题。
-
堆内存不足:当堆内存无法满足新对象的分配需求时,会抛出java.lang.OutOfMemoryError: Java heap space错误。
-
栈内存不足:当线程栈的深度超过 JVM 允许的最大深度时,会抛出java.lang.StackOverflowError错误;如果栈无法扩展到足够的内存,则会抛出java.lang.OutOfMemoryError: unable to create new native thread错误。
-
本地方法栈不足:与栈内存不足类似,本地方法栈不足也会导致java.lang.OutOfMemoryError错误。
-
方法区(元空间)不足:当方法区(元空间)无法分配新的类元数据时,会抛出java.lang.OutOfMemoryError: Metaspace错误。
-
直接内存不足:当直接内存无法分配新的缓冲区时,会抛出java.lang.OutOfMemoryError: Direct buffer memory错误。
什么是Full GC(Full Garbage Collection)?
Full GC是一种垃圾回收操作,是Java虚拟机(JVM)在垃圾回收时,不仅回收年轻代的对象,还回收老年代和方法区的对象。Full GC通常比Minor GC(年轻代垃圾回收)更耗时,因为它需要扫描和回收整个堆内存。
- 触发时机:
-
- 老年代空间不足:当老年代的可用内存不足以容纳新生代晋升的对象时,会触发Full GC。
- 方法区空间不足:当方法区(元空间)的可用内存不足时,会触发Full GC。
- 调用System.gc():显式调用System.gc()方法会建议JVM执行Full GC,但不一定会执行。
- Minor GC后进入老年代的平均大小大于老年代的可用内存:如果Minor GC后进入老年代的对象总和大于老年代的可用内存,会触发Full GC。
- 对象大小大于Survivor区可用内存:当对象从Eden区或Survivor区复制到另一个Survivor区时,如果对象大小大于目标Survivor区的可用内存,会将对象直接晋升到老年代,如果老年代的可用内存不足,则触发 Full GC。
- Full GC的影响:
-
-
STW(Stop The World):Full GC会暂停所有用户线程,导致应用程序卡顿,影响用户体验。
-
性能影响:Full GC 的执行时间通常较长,会显著影响应用程序的性能。
-
如何进行JVM的性能优化?
总的来说从以下几个方面干预:
- 优化内存分配:根据应用的需求,设置合适的堆内存大小,避免内存不足或浪费。通过减少对象的创建和销毁,使用对象池等方式,来减少内存的分配和回收。
-
优化垃圾回收:根据应用的特点,选择合适的垃圾回收器和调整参数,选择合适的垃圾回收器,如Serial收集器、ParNew收集器、CMS收集器和G1收集器等。
-
优化代码:编写高效、简洁的代码,减少不必要的计算和操作。
-
调整垃圾回收参数:通过调整垃圾回收的参数,如新生代和老年代的比例、回收的频率等,来优化垃圾回收的性能。
-
避免内存泄漏:及时发现和修复内存泄漏的问题,减少不必要的内存占用。
-
使用性能分析工具:如JConsole、VisualVM等工具,对JVM的性能进行分析和监控,及时发现问题并进行优化。
具体调优方式:
1)垃圾回收调优
- 选择合适的垃圾回收器:
- 如果侧重响应时间,可选用CMS或G1。
- 如果侧重吞吐量,可选用Serial或Parallel Scavenge。
- 设置堆内存大小:
- 初始堆内存(-Xms):设置为物理内存的1/4到1/2。
- 最大堆内存(-Xmx):与初始堆内存尽量保持一致,避免运行时频繁调整堆大小。
- 调整新生代和老年代比例:
- 默认情况下:新生代占1/3,老年代占2/3。
- 可使用选项 -XX:NewRatio=2 将老年代与新生代的比设置为2:1,即新生代占1/3。
- 日志分析:
-
通过GC日志(使用参数-Xloggc: 指定日志文件路径,-XX:+PrintGCDateStamps 打印时间戳等)分析GC情况,判断是否需要调整。
-
2)其他调优手段
- 线程池调优:合理设置线程池大小,避免线程过多导致上下文切换频繁或线程不足导致任务被阻塞。
- 缓存调优:如使用Map缓存时,设定合理的缓存过期时间和最大缓存容量。
- 并行与并发调优:对于多线程场景,合理使用锁机制(如减小锁粒度、使用乐观锁等)。
3)常用监控工具
-
JConsole:提供对虚拟机各个方面的信息查看和某些方面的操作。
-
JProfiler:商业的分析工具,可以查看线程、内存、CPU等信息。
-
VisualVM:java自带的工具,支持对已启动的Java程序进行性能分析和故障排查。
4)线上处理方法
- 开启日志:合理设置JVM参数(模拟报错就设置小一点),项目启动打印日志
- -Xmx200m -Xms200m -XX:+PrintGCDetails -XX:+UseConcMarkSweepGC -XX:+HeapDumpOnOutOfMemoryError -XX:+HeapDumpBeforeFullGC -XX:+HeapDumpAfterFullGC -XX:+HeapDumpPath=地址
- 内存监控:预警监控(JVM-Micrometer)
- 拿到日志:溢出日志生成dump文件(.hprof)
- 进行分析:分析工具MAT或者visulVM打开,Actions-Dominator Tree位置找到内存溢出对象或线程[命名的重要性]
- 代码优化:根据工具指出的问题,定位跟踪问题代码,进行优化验证
5)本地处理方法
- 开启日志:合理设置JVM参数(模拟报错就设置小一点),项目启动打印日志
- 内存监控:预警监控(VisualVM)
- 拿到日志:溢出日志生成dump文件(.hprof)
- 进行分析:分析工具MAT或者visulVM打开,找到内存溢出对象或线程[命名的重要性]
- 代码优化:根据工具指出的问题,定位跟踪问题代码,进行优化验证
关注公众号:咖啡Beans
在这里,我们专注于软件技术的交流与成长,分享开发心得与笔记,涵盖编程、AI、资讯、面试等多个领域。无论是前沿科技的探索,还是实用技巧的总结,我们都致力于为大家呈现有价值的内容。期待与你共同进步,开启技术之旅。