JVM实战思维脑图
过早优化是万恶之源——克努特优化原则,在软件开发过程中,我们应该避免过早优化,而是优先考虑代码的可读性、可维护性和可扩展性。在进行性能调优之前一定要问自己以下这三个问题:
为什么要JVM性能调优?
JVM性能调优的目的主要是减少应用程序的延迟,使其在用户请求下更快地响应,从而提高应用程序的吞吐量。
JVM调优的依据是什么?
通过监控工具和性能分析工具收集和分析应用程序的运行数据,例如内存使用情况、线程状态、GC(垃圾回收)情况等,分析监控数据和性能瓶颈,确定应用程序的性能瓶颈点和资源瓶颈点。
JVM调优的方向?
堆内存优化:调整堆内存的大小、分代垃圾回收算法和参数,以提高内存利用率、减少GC暂停时间和降低内存泄漏的风险。
GC调优:根据应用程序的内存使用模式和垃圾生成情况,选择适当的GC算法和参数设置,以减少GC暂停时间和提高吞吐量。
性能优化
性能优化包括:内存调优、GC调优、业务代码优化、SQL优化等等…这篇文章我们将带你由浅入深地去理解内存调优和GC调优。
JVM常用定位问题的命令
给一个系统定位问题的时候,恰当地使用jvm命令或分析工具可以提升我们分析数据,定位并解决问题的效率,下面将介绍几个排查问题时比较常用的命令。
- jps:可以列出正在运行的虚拟机进程。
- jinfo:实时查看和调整jvm各项参数。
- jstat:用于监视jvm各种运行状态信息,比如:类加载、内存、垃圾收集、即时编译等运行时数据。
- jmap:用于生成堆快照文件(一般称为heapdump 或 dump文件)。
- jstack:用于生成虚拟机当前时刻的线程快照。
内存泄露
概念:Java中如果不再使用一个对象,但是该对象依然在GC ROOT的引用链上,
这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏。
内存泄漏的常见场景
在java程序应用中,内存泄露往往由多方面的原因造成,下面我们就来分析一下内存泄露常见的场景:
equals()和hashCode()导致的内存泄漏
在定义新类时没有重写正确的equals()和hashCode()方法。在使用HashMap的场景下,
如果使用这个类对象作为key,HashMap在判断key是否已经存在时会使用这些方法,如
果重写方式不正确,会导致相同的数据被保存多份。
下面来看一个具体的实例:
public class Person {
private int id;
private String name;
private int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
Map的key是不能重复的,现在我们将重复的Person对象插入到Map当中。
public static void main(String[] args) {
HashMap<Person, Integer> personMap = new HashMap<>();
for (int i = 0; i < 100000; i++) {
Person person = new Person();
person.setId(1);
person.setName("zs");
person.setAge(1);
personMap.put(person, 1);
}
}
上述代码将相同Person对象作为key,存进Map中,理论上当有重复的key时,会进行覆盖。由于Person类没有重写hashcode和equals方法,因此Map进行Put操作时,其实每次都会存进新的对象,从而导致内存不断增长。
解决方案:
- 在定义新实体时,一定要重写equals()和hashCode()方法。
- 重写时一定要确定使用了唯一标识去区分不同的对象,比如用户的id等。
- hashmap使用时尽量使用编号id等数据作为key,不要将整个实体类对象作为key存放。
内部类引用外部类
- 非静态的内部类默认会持有外部类,尽管代码上不再使用外部类,所以如果有地方引
用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。
- 匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回
收调用者。
解决方案:
1、这个案例中,使用内部类的原因是可以直接获取到外部类中的成员变量值,简化开发。如果不想持有外部类
对象,应该使用静态内部类。
2、使用静态方法,可以避免匿名内部类持有调用者对象。
ThreadLocal的使用
ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定,从而实现线程安全的特性。
ThreadLocal的实现中内部维护着一个ThreadLocalMap,它是使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,在GC时ThreadLocal就会被回收,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。
如果线程一直不被回收就会造成内存泄漏,一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value。
解决方案:
- 线程方法执行完,一定要调用ThreadLocal中的remove方法清理对象。
- 将ThreadLocal视为需要在finally块中关闭的资源,以确保即使在发生异常的情况下也始终关闭该资源。
String的intern方法
JDK6中字符串常量池位于堆内存中的Perm Gen永久代中,如果不同字符串的intern方法被
大量调用,字符串常量池会不停的变大超过永久代内存上限之后就会产生内存溢出问题。
解决方案:
- 注意代码中的逻辑,尽量不要将随机生成的字符串加入字符串常量池 。
- 增大永久代空间的大小,根据实际的测试/估算结果进行设置-XX:MaxPermSize=256M。
静态字段保存对象
如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存
泄漏。
解决方案:
- 尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或
者将静态变量设置为null。
- 使用单例模式时,尽量使用懒加载,而不是立即加载。
- Spring的Bean中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。
资源没有正常关闭
连接和流这些资源会占用内存,如果使用完之后没有关闭,这部分内存不一定会出现内存泄漏,但是会
导致close方法不被执行。
解决方案:
- 为了防止出现这类的资源对象泄漏问题,必须在finally块中关闭不再使用的资源。
- 从 Java 7 开始,使用try-with-resources语法可以用于自动关闭资源。
内存溢出有哪几种产生的原因
1、持续的内存泄漏:内存泄漏持续发生,不可被回收同时不再使用的内存越来越多,
就像滚雪球雪球越滚越大,最终内存被消耗完无法分配更多的内存取使用,导致内存
溢出。
2、并发请求问题:用户通过发送请求向Java应用获取数据,正常情况下Java应用将
数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有
可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内
存的上限,导致内存溢出。
内存调优的步骤
发现问题
top命令
VisualVM
Arthas
arthas能做为我们做什么?以下这段话是从它的官网https://arthas.aliyun.com/doc/copy过来的
- 这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?
- 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?
- 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?
- 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!
- 是否有一个全局视角来查看系统的运行状况?
- 有什么办法可以监控到 JVM 的实时运行状态?
- 怎么快速定位应用的热点,生成火焰图?
- 怎样直接从 JVM 内查找某个类的实例?
诊断问题
当对内存溢出时,可以设置JVM参数将整个堆内存保存下来,生成内存快照文件。
如果想导出运行中系统的内存快照并进行分析,有以下两种方式:
- 通过JDK自带的jmap命令导出,格式为:
jmap -dump:live,format=b,file=文件路径和文件名 进程ID
- 通过arthas的heapdump命令导出,格式为:
heapdump --live 文件路径和文件名
修复问题
使用MAT或者VisualVM工具可以自动分析内存快照文件的内容,给出一个分析结果,并定位到由问题的类,然后我们只需根据分析结果找到对应代码进行优化即可。
测试验证
在测试环境验证问题是否解决,解决则发布上线
内存溢出实战
内存溢出在生产环境中偶尔会出现,比如一次性从数据库拉取过多的数据,程序代码中存在死循环等等…
我们需要通过以上内存调优的步骤学会如何定位内存溢出的问题,然后再通过MAT工具协助我们去分析问题的根本原因。
下面通过一个实战例子来加深理解内存溢出的定位与分析
模拟内存溢出
向List集合中添加1000个1M的字符数组,如果程序能正常运行,最后会打印success。
public static void main(String[] args) {
List<Byte[]> bytes = new ArrayList<Byte[]>();
for (int i = 0; i < 1000; i++) {
bytes.add(new Byte[1024 * 1024]);
}
System.out.println("success");
}
为了程序能够出现OOM,需要对JVM参数进行特殊配置
-Xms:堆的初始大小为512m
-Xmx:最大堆的大小也是512m
-XX:+HeapDumpOnOutOfMemoryError:出现内存溢出时会自动保存堆快照文件
运行结果:
当发生内存溢出时,自动帮我们保存了堆快照文件
将这个文件导入到MAT工具进行分析
从预览图上可以看有个Object[]占据了99.83%的堆内存空间,这个对象是非常可疑的。点击Details查看详情
通过上图分析可以发生内存溢出的代码位置和根本原因。
GC调优
GC调优指的是对垃圾回收(Garbage Collection)进行调优。GC调优的主要目标是避免由垃圾回收引起程序性能下降,其实本质就是减少GC时STW(stop the world)时间,因为在STW期间,用户线程是无法执行业务逻辑的,需要等GC完成后再恢复用户线程执行。
GC调优没有没有唯一的标准答案,如何调优与硬件、程序本身、使用情况均有关系。
GC调优的核心指标
吞吐量
吞吐量(Throughput)是指在一定时间内系统或过程所处理的工作量或数据量的数量。
保证高吞吐量的常规手段有两条:
- 优化业务执行性能,减少单次业务的执行时间
- 优化垃圾回收吞吐量
垃圾回收吞吐量
垃圾回收吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代
码时间 /(执行用户代码时间 + GC时间)。吞吐量数值越高,垃圾回收的效率就越高,允许更多的CPU时
间去处理用户的业务,相应的业务吞吐量也就越高。
延迟(Latency)
延迟指的是从用户发起一个请求到收到响应这其中经历的时间。
延迟 = GC延迟 + 业务执行时间,所以如果GC时间过长,会影响到用户的使用。
GC调优的步骤
发现问题
通过以下监控或者日志可以发现GC是否存在问题。
jstat工具
visualvm插件
Prometheus + Grafana
GC日志
诊断问题
我们需要通过以下的一些在线分析工具协助我们分析GC的情况。
GC Viewer
GCeasy
解决问题(修复问题)
JVM参数详解
堆参数:
-Xmx 和 –Xms,–Xms:初始化堆内存大小,-Xmx:最大堆内存大小。
-Xms和 -Xmx设置成一样大的好处:
元空间参数:
虚拟机栈参数:
其它参数:
测试验证
修复问题或者进行GC调优后需要在测试环境进行压测校验,通过观察GC日志验证问题是否解决。
总结
- 首先,进行优化性能优化之前,需要确认项目架构和业务代码还有没有优化空间,因为我们不能指望一个架构有缺陷或者存在"烂"代码,通过内存或GC优化就能令其达到一个质的飞跃。
- 其实,JVM的开发者们在JVM内部已经做了很多的优化来保证我们应用的稳定运行,我们不能为了调优而调优,不当的调优可能会适得其反。
- 最后,GC调优本来就是个非常复杂的工作,没有万能的调优策略可以满足所有的性能指标,每个业务系统都有着不同QPS和特殊业务处理,只能通过分析不同系统的GC日志做出合理的GC调优方案。GC优化必须建立在我们深入理解各种垃圾回收器的基础上,才能达到事半功倍的效果。