【JVM详解JVM优化】聊聊JVM优化
简介:
前面两期文章讲了JVM内存模型:【JVM详解&JVM优化】JVM内存模型-CSDN博客
以及JVM垃圾回收机制:【JVM详解&JVM优化】JVM垃圾回收机制-CSDN博客
在本篇文章中,我们将深入探讨Java虚拟机(JVM)的优化策略,以提升Java应用的性能和效率。JVM是Java程序运行的核心,其内部机制和参数设置直接影响应用的表现。文章将首先概述JVM的基本结构和内存管理,帮助读者理解优化的必要性。
接下来,我们将探讨多种JVM优化技术,包括堆内存的合理配置、垃圾收集器的选择与调优,以及如何通过JVM参数调整(如-Xms
、-Xmx
、-XX:+UseG1GC
等)来提高性能。此外,文章还将涵盖常见的性能监测工具和技术,如Java VisualVM和JProfiler,帮助开发者实时分析和诊断应用性能瓶颈。
最后,本文将分享一些实际调优经验,展示如何在真实环境中应用这些优化策略,以确保Java应用的高效性和稳定性。无论你是初学者还是经验丰富的开发者,本篇文章都将为你提供实用的见解和建议,助你在JVM优化的道路上更进一步。
一、常见的优化策略
1. 调整 JVM 参数
-
堆大小: 根据应用需求设置合适的堆内存大小(
-Xms
和-Xmx
)。-Xms512m -Xmx2048m
-
年轻代与老年代比例: 调整年轻代和老年代的比例,可以使用
-XX:NewRatio
参数。例如,设置年轻代为老年代的 1:2:-XX:NewRatio=2
-
GC 策略: 选择适合应用场景的垃圾回收策略,如 G1、CMS 或 ZGC:
-XX:+UseG1GC
常用JVM参数参考
-
jdk1.7 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
-
jdk1.8 默认垃圾收集器Parallel Scavenge(新生代)+Parallel Old(老年代)
-
jdk1.9 默认垃圾收集器G1
-XX:+PrintCommandLineFlags jvm参数可查看默认设置收集器类型
-XX:+PrintGCDetails亦可通过打印的GC日志的新生代、老年代名称判断
2. 监控和分析
- JVM 监控工具: 使用 JVisualVM、JConsole 等工具监控内存使用情况、CPU 性能和线程活动。
- Heap Dump 分析: 分析堆转储文件,查找内存泄漏和对象创建情况。
- 通过观察 GC 频率和停顿时间,来进行 JVM 内存空间调整,使其达到最合理的状态。调整过程记得小步快跑,避免内存剧烈波动影响线上服务。
3. 优化代码
- 减少对象创建: 尽量复用对象,使用对象池等技术,降低 GC 压力。
- 使用基本数据类型: 尽量使用基本数据类型而非包装类型,减少内存消耗。
- 优化循环和递归: 避免不必要的循环和递归调用,优化算法复杂度。
4. JIT 编译优化
- 方法内联: 让 JIT 编译器内联小方法,以减少方法调用的开销。
- 编译级别: 调整 JIT 编译器的编译级别,可以使用
-XX:CompileThreshold
来设置编译阈值。
5. 配置类加载
- 类加载器: 自定义类加载器,优化类的加载和卸载过程。
- 避免重复加载: 确保类不被重复加载,避免因类加载导致的内存浪费。
6. 线程优化
- 线程池: 使用线程池管理线程,避免频繁创建和销毁线程。
- 锁优化: 尽量减少锁的粒度,使用读写锁等减少线程竞争。
7. 垃圾回收调优
- 调节 GC 频率: 使用
-XX:MaxGCPauseMillis
和-XX:GCTimeRatio
等参数调节 GC 行为。 - 避免 Full GC: 优化堆的使用,避免频繁的 Full GC,确保大对象存放在老年代。
8. 使用现代 JVM 特性
- 逃逸分析: 利用逃逸分析优化对象的分配,减少不必要的堆分配。
- JVM 热点特性: 利用 JVM 热点特性,进行持续的性能调优。
逃逸分析(Escape Analysis)是目前Java虚拟机中比较前沿的优化技术。
这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。(不要在轻易回答面试官,所有对象都是在堆上创建了)
逃逸分析的基本原理是:
分析对象动态作用域,当一个对象在方法里面被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他方法中,这种称为方法逃逸;
甚至还有可能被外部线程访问到,譬如赋值给可以在其他线程中访问的实例变量,这种称为线程逃逸;
从不逃逸、方法逃逸到线程逃逸,称为对象由低到高的不同逃逸程度。
JVM中通过如下参数可以指定是否开启逃逸分析:
-XX:+DoEscapeAnalysis :表示开启逃逸分析。
-XX:-DoEscapeAnalysis :表示关闭逃逸分析。
开启逃逸分析,编译器可以对代码进行如下优化:
1、同步消除:如果一个对象被逃逸分析发现只能被一个线程所访问,那对于这个对象的操作可以不同步。
2、栈上分配:如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
3、标量替换:如果一个对象被逃逸分析发现不会被外部方法访问,并且这个对象可以拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个比这个方法使用的成员变量来代替。将对象拆分后,可以让对象的成员变量在栈上分配和读写。
通过综合运用这些策略,可以显著提高 JVM 的性能和响应速度,确保 Java 应用程序在生产环境中的高效运行。根据具体应用场景,选择合适的优化策略进行调整。
二、调优经验
JVM的性能优化可以分为代码层面和非代码层面。
在代码层面,大家可以结合字节码指令进行优化,比如一个循环语句,可以将循环不相关的代码提取到循环体之外,这样在字节码层面就不需要重复执行这些代码了。
在非代码层面,一般情况可以从内存、gc以及cpu占用率等方面进行优化。
注意,JVM调优是一个漫长和复杂的过程,而在很多情况下,JVM是不需要优化的,因为JVM本身已经做了很多的内部优化操作。
Java整个堆大小设置,Xmx 和 Xms设置为老年代存活对象的3-4倍,即FullGC之后的老年代内存占用的3-4倍,Xmx和Xms的大小设置为一样,避免GC后对内存的重新分配。而Full GC之后的老年代内存大小,我们可以通过前面在Visual VM中添加的插件Visual GC查看。先手动进行一次GC,然后查看老年代的内存占用。
新生代Xmn的设置为老年代存活对象的1-1.5倍。
老年代的内存大小设置为老年代存活对象的2-3倍。
JVM配置方面,一般情况可以先用默认配置(基本的一些初始参数可以保证一般的应用跑的比较稳定了),在测试中根据系统运行状况(会话并发情况、会话时间等),结合gc日志、内存监控、使用的垃圾收集器等进行合理的调整,当老年代内存过小时可能引起频繁Full GC,当内存过大时Full GC时间会特别长。
那么JVM的配置比如新生代、老年代应该配置多大最合适呢?答案是不一定,调优就是找答案的过程,物理内存一定的情况下,新生代设置越大,老年代就越小,Full GC频率就越高,但Full GC时间越短;相反新生代设置越小,老年代就越大,Full GC频率就越低,但每次Full GC消耗的时间越大。建议如下:
-Xms和-Xmx的值设置成相等,堆大小默认为-Xms指定的大小,默认空闲堆内存小于40%时,JVM会扩大堆到-Xmx指定的大小;空闲堆内存大于70%时,JVM会减小堆到-Xms指定的大小。如果在Full GC后满足不了内存需求会动态调整,这个阶段比较耗费资源。
新生代尽量设置大一些,让对象在新生代多存活一段时间,每次Minor GC 都要尽可能多的收集垃圾对象,防止或延迟对象进入老年代的机会,以减少应用程序发生Full GC的频率。
老年代如果使用CMS收集器,新生代可以不用太大,因为CMS的并行收集速度也很快,收集过程比较耗时的并发标记和并发清除阶段都可以与用户线程并发执行。
方法区大小的设置,1.6之前的需要考虑系统运行时动态增加的常量、静态变量等,1.7只要差不多能装下启动时和后期动态加载的类信息就行。
代码实现方面,性能出现问题比如程序等待、内存泄漏除了JVM配置可能存在问题,代码实现上也有很大关系:
避免创建过大的对象及数组:过大的对象或数组在新生代没有足够空间容纳时会直接进入老年代,如果是短命的大对象,会提前出发Full GC。
避免同时加载大量数据,如一次从数据库中取出大量数据,或者一次从Excel中读取大量记录,可以分批读取,用完尽快清空引用。
当集合中有对象的引用,这些对象使用完之后要尽快把集合中的引用清空,这些无用对象尽快回收避免进入老年代。
可以在合适的场景(如实现缓存)采用软引用、弱引用,比如用软引用来为ObjectA分配实例:SoftReference objectA=new SoftReference(); 在发生内存溢出前,会将objectA列入回收范围进行二次回收,如果这次回收还没有足够内存,才会抛出内存溢出的异常。
避免产生死循环,产生死循环后,循环体内可能重复产生大量实例,导致内存空间被迅速占满。
尽量避免长时间等待外部资源(数据库、网络、设备资源等)的情况,缩小对象的生命周期,避免进入老年代,如果不能及时返回结果可以适当采用异步处理的方式等。
三、结语
🔥如果文章对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏、✍️评论,支持一下小老弟,蟹蟹大咖们~