6

大聪明教你学Java | 深入浅出聊 JVM 调优

 2 years ago
source link: https://juejin.cn/post/7058466515892305934
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

大家对 JVM 调优肯定不陌生,而且我们肯定听身边的小伙伴说过:在面试的时候,有的面试官就会抓住 JVM 调优这一个点不放手,会问很多关于 JVM 调优的问题,给我都问懵了😵 虽然面试官会将其作为一个面试的重点,但是大部分小伙伴在实际的开发过程中都很少优化过 JVM,所以也就不太了解关于 JVM 调优的知识。今天就和大家深入浅出聊聊 JVM,把我所掌握的知识跟大家分享一下🙇‍

什么是 JVM

在说 JVM 调优之前,我们先来看看什么是 JVM👇。

JVM 是 Java Virtual Machine(Java虚拟机)的缩写,JVM 是一种用于计算设备的规范,它是一个虚构出来的计算机,是通过在实际的计算机上仿真模拟各种计算机功能来实现的。

引入Java语言虚拟机后,Java语言在不同平台上运行时不需要重新编译。Java语言使用Java虚拟机屏蔽了与具体平台相关的信息,使得Java语言编译程序只需生成在Java虚拟机上运行的目标代码(字节码),就可以在多种平台上不加修改地运行。 (以上内容来自百度)

Java 虚拟机本质上就是一个程序,当它在命令行上启动的时候,就开始执行保存在某字节码文件中的指令。Java 语言的可移植性正是建立在 Java 虚拟机的基础上。各位小伙伴应该都听过这么一句话:Java 是一个和平台无关的编程语言,之所以这么说,就是因为有 Java 虚拟机的存在,因为 Java 虚拟机知道底层硬件平台的指令长度和其他特性,同时 Java 源文件可以被编译成能被 Java 虚拟机执行的字节码文件(.class文件),所以无论是什么平台,只要平台上装有对应本平台的 Java 虚拟机,由 Java 源文件编译成的字节码文件(.class 文件)就可以在该平台上运行,这就是“一次编译,多次运行”

这么说可能有点不好理解,还是老规矩,给大家说说我的“猥琐理解”:比如我们现在需要养一个小鸭子,但是我们的饲养环境经常变化(今天在家里饲养,明天就可能去其他地方了),由于鸭子比较小,所以我们就需要给他准备保温箱,让它在保温箱里舒舒服服的生活。如果在家里饲养,那我们就用一个普通的保温箱;如果换了一个比较冷的地方,那我们就需要换一个更厚的保温箱。小鸭子就是我们编写的 Java 源文件,保温箱就是 Java 虚拟机,不同的环境就对应了不同的平台,无论我们想在什么样的环境中饲养小鸭子,只要换上了对应的保温箱,就能让小鸭子快块乐乐的成长。

那如果我们想提升一下小鸭子的生活质量该怎么办呢?其实办法也很简单,我们可以把保温箱改造一下,比如增加点空间、放上水槽食槽、放点棉花等等,这些办法都可以让保温箱“升个级”,升级保温箱的过程对应的就是 JVM 调优。下面就来聊聊今天的重点—— JVM 调优👇

JVM 调优

我们开发好的系统软件在上线前的测试或运行中可能会遇到一些问题,比如 CPU load 过高、请求有延迟,或者是每次收集垃圾时使用的时间越来越长、垃圾收集频率越来越高、每次垃圾收集清理掉的垃圾数据越来越少等等,这些都算是 JVM 的问题,这些问题或多或少的会影响到系统软件的执行速度,因此我们需要对 JVM 进行调优。我们调优的终极目标就是让应用程序使用最小的硬件消耗来承载更大的吞吐。JVM 调优主要是针对垃圾收集器的收集性能优化,令运行在虚拟机上的应用能够使用更少的内存以及延迟获取更大的吞吐量,说白了就是让程序在正常运行的前提下,获得更高的用户体验和运行效率。

JVM 调优步骤

有些小伙伴一想到 JVM 调优可能就觉得很麻烦,不知道从哪开始下手。其实 JVM 调优的步骤也很简单 👇

① 分析 GC 日志及 dump 文件,判断是否需要优化,确定问题根源所在 ② 确定 JVM 调优量化目标和 JVM调优参数(JVM 调优参数根据历史 JVM 参数来调整) ③ 依次调优内存、延迟、吞吐量等指标 ④ 对比观察调优前后的差异,同时需要不断的分析和调整参数,直到找到合适的 JVM 参数配置; ⑤ 找到最合适的参数,将这些参数应用到所有服务器,并进行后续跟踪。

我们把上面的步骤总结一下其实也就是分三步(跟把大象装冰箱一样😂):首先就是分析日志,确认是否需要调优;其次是确认调优目标及调优参数;最后一步就是反复测试并进行追踪。这次再看 JVM 调优是不是就没那么棘手啦😀

上面的操作步骤中,某些步骤是需要多次不断迭代完成的(比如调整参数、追踪调优后的幸能)。这里有一点是需要注意的,我们调优的时候一般是从满足程序的内存使用需求开始的,接下来是满足程序的时间延迟的要求,最后才是满足程序吞吐量的要求,我们要遵循这个步骤来不断优化,每一个步骤都是进行下一步的基础,绝对不能反其道而行之🈲。

JVM 相关参数

在上面提到的步骤中,最重要的步骤就是对 JVM 参数的调整,所以就需要我们对 JVM 参数有一定的了解,下面我就先说说关于 JVM 参数的相关内容👇

-XX 参数被称为不稳定参数,此类参数的设置很容易引起 JVM 性能上的差异,这也就使 JVM 存在极大的不稳定性。如果此类参数设置合理将大大提高JVM的性能及稳定性。可以将这类不稳定参数理解成一把双刃剑,用好了可以威力大增,用不好可能就会出现伤敌一百自损一万的局面。不稳定参数主要分为三类,分别是布尔类型参数值、数字类型参数值和字符串类型参数值。布尔类型参数值分为两种:-XX:+ 和 -XX:-,“+” 表示启用该选项;“-” 表示禁用该选项;数字类型参数的格式是:-XX: ,给选项设置一个数字类型值,在数值后面可增加单位,例如:“m” 或 “M” 表示兆字节、“k” 或 “K” 表示千字节;字符串类型参数的格式和数字类型参数的格式是相同的,只是后面所跟的数值不同,字符串类型参数设置的是一个字符串类型值,通常用于指定一个文件、路径或一系列命令列表。例如:-XX:HeapDumpPath=xxx/xxx/xxx(文件路径)。

下面咱们看几个具体的例子 👇

-Xms2g -Xmx2g -Xmn512M -Xss128K -XX:PermSize=128M -XX:MaxPermSize=128M -XX:NewRatio=4 -XX:SurivorRatio=4 -XX:MaxTenuringThreshold=1

  • -Xms2g:JVM启动初始化堆大小为2g,Xms的默认是物理内存的1/64但小于1G。

  • -Xmx2g:JVM最大的堆大小为2g,Xmx默认是物理内存的1/4但小于1G;将-Xms和-Xmx的值配置为一样,可以避免每次垃圾回收完成后对JVM堆大小进行重新的调整。

  • -Xmn512M:堆中的新生代大小为512M

  • -Xss128K:每个线程的堆栈大小为128K

  • -XX:PermSize=128M:JVM持久代的初始化大小为128M

  • -XX:MaxPermSize=128M:JVM持久代的最大大小为128M

  • -XX:NewRatio=4:JVM堆的新生代和老年代的大小比例为1:4

  • -XX:SurvivorRatio=4:新生代Surivor区(新生代有2个Surivor区)和Eden区的比例为2:4

  • -XX:MaxTenuringThreshold=1:新生代的对象经过几次垃圾回收后(如果还存活),进入老年代。如果该参数设置为0,这表示新生代的对象在垃圾回收后,不进入survivor区,直接进入老年代

-XX:+UseParallelGC -XX:ParallelGCThread=4 -XX:+UseParallelOldGC -XX:MaxGCPauseMillis=100 -XX:+UseAdaptiveSizePolicy

  • -XX:+UseParallelGC:使用并行的垃圾收集器,但仅针对新生代有效,老年代仍然使用串行收集器
  • -XX:ParallelGCThread=4:设置并行垃圾回收器的线程为4个,该设置最好与处理器的数目相同
  • -XX:+UseParalleOldGC:配置老年代使用并行垃圾收集器,JDK1.6支持老年代使用并行收集器
  • -XX:MaxGCPauseMillis=100:设置每次新生代每次收集器垃圾回收的最长时间,如果无法满足该时间,JVM会自动调整新生代区的大小,以满足该值
  • -XX:+UseAdaptiveSizePolicy:设置此值后,JVM会自动调整新生代大小以及相应的surivor区的比例,以达到设置的最低响应时间或者收集频率等

-XX:UseConcMarkSweepGC -XX:+UseParNewGC -XX:CMSFullGCsBeforeCompaction=5 -XX:+UseCMSCompactAtFullCollection

  • -XX:UseConcMarkSweepGC:设置JVM堆的老年代使用CMS并发收集器,设置该参数后,-XX:NewRatio参数失效,但-Xmn参数依然有效
  • -XX:UseParNewGC:设置新生代使用并发收集器,在JDK1.5以上,JVM会根据系统自动设置
  • -XX:CMSFullGCsBeforeCompaction=5:设置5才CMSGC后对堆空间进行压缩、整理
  • -XX:+UseCMSCompactAtFullCollection:打开对老年代的压缩,可能会影响性能,但可以消除堆碎片

JVM 调优参数

JVM 涉及到的参数有很多,但是在调优的过程中并不是所有参数都需要修改,关于如何对 JVM 进行调优,我在这里为大家整理出了以下几点调优建议 (主要是小弟经验有限,会的不多😂)👇

  1. 针对 JVM 堆的设置,一般可以通过 -Xms 和 -Xmx 限定其最小值和最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,我们通常把最小值和最大值设置为相同的值。
  2. 年轻代和年老代的内存将根据默认的比例(本着Full GC尽量少的原则,让年老代尽量缓存常用对象,所以二者的默认比例是 1:2)分配堆内存, 可以通过调整二者之间的比率 NewRadio 来调整二者之间的大小。除此之外也可以通过其他方式来进行设置,比如年轻代,通过 -XX:newSize -XX:MaxNewSize 来设置其绝对大小。同样,我们为了防止年轻代的堆收缩,通常会把-XX:newSize -XX:MaxNewSize 设置为同样大小。
  3. 年轻代和年老代的内存大小是相关牵制的,二者就像是一个跷跷板的关系(一个内存变高就会导致另外一个内存变低),所以在设置二者的内存大小时就需要格外谨慎了,我们可以通过观察应用一段时间,看应用在峰值时年老代会占多少内存,在保证不影响 Full GC 的前提下,可以根据实际情况加大年轻代内存,比如可以把比例控制在1:1。有一点需要注意,我们在调节内存大小时,要保证年老代有足够的增长空间。
  4. 在配置比较高的服务器上(比如多核、大内存),我们可以为年老代选择并行收集算法: -XX:+UseParallelOldGC 。
  5. 在应用运行时,每个线程默认会开启 1M 的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默空间就太大了,所以在设置线程堆栈空间时一般设置成 256K 就足够了。
  6. 如果应用在运行时报 java.lang.OutOfMemoryError: Direct buffer memory 的异常,我们可以上调 -XX:MaxDirectMemorySize 的值,增大直接内存。
  7. 如果还是不知道该对哪个参数进行调整的话,我们可以选择使用分析工具(例如 GCViewer)来帮助我们,使用调优工具可以非常直观地分析出待调优点。(这招可以称之为绝招了,哈哈哈哈😄)

关于 JVM 调优的一些拙见

最后再说说我对 JVM 调优的一些拙见:其实一般的项目也不需要对 JVM 进行优化,即便是一些高并发或者高请求量的服务或者软件,对 JVM 的调优也不是那么重要的。因为 JVM 本身就是为这种低延时高并发大吞吐的服务设计和优化的,我们真的很少很少需要去改变什么,所以即便是遇到了需要调优的情况,我们也应该注重于对应用本身进行调优(如果贸然的对 JVM 进行调优,或许引发的问题更大)。说的再直接一点,就算将一些项目的 JVM 优化后的参数删除掉,或许他们也还是可以运行的很好(面对一些极端情况或许会有问题)。

而且也不知道为什么,好多公司在面试 JAVA 程序员的时候就喜欢抓着 JVM 不放,问一大堆关于 JVM 的问题,其实我感觉这就有点舍本逐末了。而且不知道各位小伙伴有没有遇到过这种同事,他们解决问题喜欢一杆子捅到底层,遇到问题竟然会先提出是 JVM 存在 BUG 导致的异常,遇到高延迟的情况就会说是 GC 算法有问题,他们甚至会去考虑替换 GC 算法(也不知道是哪来的自信😂),说实话就算是把 JVM 的油水榨干了,也不如把业务逻辑优化那么一丢丢来得更实在。

本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇‍

希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●'◡'●)

如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。

爱你所爱 行你所行 听从你心 无问东西


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK