JAVA-JVM实践指南

JVM实践指南

前言

项目研发过程中,你或许并未直接使用到JVM,但实际上已经间接使用 JVM 很长时间了,在使用过程中,你可能或多或少地遇到以下问题:

  • 如果你遇到了OOM(内存溢出),你是否会束手无策?

  • 应用服务器出现高延迟问题,该如何排查定位?

  • 服务器出现性能不足时,是否只能依靠增加服务器?

此时,你或许需要一份 「JVM实践指南」

本文想聊一聊关于 JVM 的实践,在此之前,我们先回顾一下 JVM 相关知识。

为什么要学习JVM

  1. 能让我们更深入的理解Java,理解Java语言底层的执行过程,能让我们“知其然”也“知其所以然”,达到更高效编程的目的;

  2. 能让我们洞悉内存泄漏、内存溢出、GC频繁导致的高延迟等问题;

  3. 能让我们通过调整JVM相关参数提高Java应用的性能;

  4. 除了 Java 外,Groovy、Scala、Clojure,以及时下热门的 Kotlin,这些语言都可以运行在JVM之上,学习JVM可以了解这些语言的通用机制;

  5. 储备知识,现在用不上,不代表以后用不上;

什么是JVM,JVM有什么用

Java虚拟机(Java Virtual Machine 简称JVM)是执行字节码文件(.class)的抽象计算机,是Java语言的运行环境,JVM是Java字节码执行的引擎,能优化Java字节码,使之转化成效率更高的机器指令。

JVM在执行字节码(.class文件)时,负责将每一条要执行的字节码送给解释器,解释器再将其翻译成特定平台的机器指令并执行,实现跨平台运行。JVM屏蔽了各系统的差异性(Window、Linux系统等),让我们编写的代码可以更快速,更高效地到处执行。

JVM类型

JVM类型的不同,对应的性能也有差异,那JVM有哪些类型呢?目前市面上有三种主流的JVM,它们分别是:

  • SUN公司的JVM:HotSpot

  • IBM公司的JVM:J9

  • BEA公司的JVM :JRockit

通常公司基础镜像大部分使用的JVM为HotSpot,由于2009年Sun公司已被Oracle公司收购,因此商业版的JDK涉及商业收费问题,许多公司正逐渐使用 Open JDK,以此替换原有的Oracle的JDK,同时需了解JDK与JRE、JVM三者的关系即:JDK包含JRE,JRE包含JVM

JVM基于JDK8的内存模型

JVM内存模型是JVM的重要组成部分,我们按内存线程是否可共享维度,可大体划分为2个类型即:所有线程共享的区域:堆和方法区,每个线程独有的区域:虚方法栈、本地方法栈和程序计数器,下面我们简单介绍JVM各内存的主要职责。

堆内存

Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。在JDK8后,字符串常量池从永久代(方法区)中分离出来,存放于堆中。

方法区

方法区基于JDK8版本采用了元数据区,并且字符串常量池也移入到堆区进行管理。方法区存放着虚拟机加载的类信息、常量、静态变量以及即时编译器编译的方法代码等。

虚拟机栈

虚拟机栈也就是平时所说的栈内存,每个Java方法在被调用的时候都会创建一个栈帧(一个方法就对应一个栈帧),并入栈。一旦完成调用,则出栈。所有的的栈帧都出栈后,线程也就结束了,栈帧由四部分组成,分别是局部变量表、操作数栈、动态链接与方法出口。

本地方法栈

由本地方法栈(Native Method Stacks)与虚拟机栈所发挥的作用是非常的相似,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务,虚拟机规范中对本地方法栈中的方法使用的语言、使用方式与数据结构并没有强制规定。

程序计数器

程序计数器是当前线程正在执行的字节码的地址,程序计数器是线程隔离的,每一个线程在工作的时候都有一个独立的计数器。

JVM基于JDK8的垃圾回收过程

JVM垃圾回收重点主要是对堆内存的回收,JVM对堆空间还进行了划分,分年轻代和老年代,他们分别对应的默认的垃圾收集器为:Parallel Scavenge(年轻代)+ Serial Old(老年代),整个垃圾回收机制的过程为:

新生成的对象首先放到年轻代Eden区,当Eden空间满了,触发Minor GC,存活下来的对象将会移动到Survivor0区,Survivor0区满后触发执行Minor GC,Survivor0区存活对象移动到Suvivor1区,这样保证了一段时间内总有一个Survivor区为空。经过多次Minor GC仍然存活的对象移动到老年代。老年代存储长期存活的对象,占满时会触发Full GC,GC期间会停止所有线程等待GC完成,所以对响应要求高的应用尽量减少发生Full GC,避免响应超时。

什么情况下需要监控JVM运行情况及对其进行合理优化呢?

当部署的应用频繁出现以下情况:出现OutOfMemory 内存异常、系统吞吐量与响应性能不高或下降,如果检查发现源代码暂未发生异常,建议可从JVM层面进行分析。

JVM提供了丰富的监控命令,能帮助我们直观得观察JVM的运行情况,首先我们需要先获取部署的Java应用的PID编号,可以使用命令:pgrep -lf java ,执行后如下图所示,获取到的PID的编号即为:1

有了PID我们继续使用命令:jmap -heap PID,这里的PID需替换为获取到的应用PID。执行命令后下图所示:

通过jmap 命令的结果可以清楚的看到当前堆内存的配置,内存使用占比情况。

此外还可以借助jstat(JVM Statistics Monitoring Tool)命令,它是用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据,它没有GUI图形界面,只提供了存文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的重要工具,执行命令:jstat -gcutil 1 后效果如下:

JVM还提供了许多其他诸如:jstack、jinfo等命令均可以结合查看JVM的实时运行情况。

有了JVM运行状况的数据,我们才可以有的放矢,所以请在优化之前,必须有监控数据作为支撑。当通过分析JVM数据后发现,Heap内存(老年代)持续上涨达到设置的最大内存值、Full GC 次数频繁、GC 停顿时间过长(超过1秒) 等情况,此时可以适当对JVM参数调整,并持续观察调整过参数的服务器性能与其他未调整的服务器对比,观察性能是否有所改善,通过不断调整,观察,调整,以达到参数处于一个较优的状态。但JVM优化仅仅是一个手段,并不一定所有问题都可以通过JVM进行优化解决,因此,在进行JVM合理优化时,建议遵循以下几个原则:

  • 上线之前,应考虑将应用的JVM参数设置到较优状态;

  • 应减少创建对象的数量;

  • 应减少使用全局变量和大对象;

优先架构优化和代码优化,JVM优化是不得已的手段。

如何利用JVM定位和解决实际问题

假如发现应用突然内存使用耗尽,需进行问题定位,应该如何处理?

内存溢出一般分两种场景,分别是内存使用过度和内存泄露,使用过度指在短时间内急剧耗尽大量内存导致内存溢出,内存泄露指长时间内,内存逐渐耗尽导致内存溢出,时间可能是几个小时或几天(通过开启详细垃圾回收可以清晰地观察到内存的分配情况),Java系统的内存溢出大部分都是由于内存使用过度导致,内存泄露比较少见。所以我们这里给出一个较通用的定位方法。

  1. 首先查找出Java程序的PID命令为:pgrep -lf java

  2. 使用jmap命令生成Dump文件:jmap -dump:format=b,file=dump.hprof pid,执行后生成得文件会存放在当前路径下,如下图所示:

    format 指定输出格式,file 指定文件名

    有了Dump文件,如何进一步定位问题呢?因为生成的文件并非的doc、txt等文件类型,我们需借助分析工具打开文件内容进一步分析,可借助以下几种工具打开该文件进行分析:

    • Visual VM
    • IBM HeapAnalyzer
    • JDK 自带的Hprof工具
    • MAT(推荐使用)
  3. 将Dump文件导入到MAT工具中进行分析

    MAT是MemoryAnalyzerTool的简称,它是一款功能强大的Java堆内存分析器,可以用于查找内存泄漏以及查看内存消耗情况

    用MAT打开hprof文件后一般会进入如下的overview界面,该界面会以饼图的方式显示当前消耗内存最多的几类对象,可以使我们对当前内存消耗有一个直观的印象。

  4. 点击切换视图,可以看到内存占用百分之八十是因为这个线程,继续点开发现是一个超大的字符串”AikesAikesAikes”

    此时我们大概发现了内存溢出的直接原因,接下来要寻找出现这个问题的代码在哪里,再返回到最初的大饼图,点击最下面的details

    然后继续点击See stacktrace 堆叠追踪

    得出下图可以看到完整的堆栈信息,红框标记的便是定位到的代码位置,至此该问题定位完毕。最后需对该段代码进行详细分析为何会造成生成大对象,造成占用大量的内存空间,从而导致内存溢出问题。

总结

JVM 虽平时较少直接使用,但我们的应用却时时刻刻都依赖着它,熟悉它可以帮助我们在遇到各种内存溢出,内存泄漏等问题时指引我们定位问题,同时熟悉它还能引导我们写出更高质量的代码,比如:避免创建大对象,循环创建对象等等。因此学习JVM以及GC是有必要的。

由于篇幅有限,更多的JVM底层知识和细节未能一一列出,后续有机会我们再继续探讨不同JDK版本下JVM的差异性,推出了哪些新的垃圾收集器,性能上做了哪些提升,有哪些优点等等。

参考链接:

1、JVM调优命令大全:https://blog.csdn.net/weixin_44688973/article/details/125793959

2、JVM垃圾回收器介绍和对比:https://blog.csdn.net/weixin_44816664/article/details/128830854

3、Java9新特性:https://blog.csdn.net/qq_34755766/article/details/82906877


JAVA-JVM实践指南
https://www.magese.com/2023/02/09/JAVA-JVM实践指南/
作者
Magese
发布于
2023年2月9日
许可协议