浅谈 JVM 整体架构与调优参数
本文分享自华为云社区《【性能优化】JVM整体架构与调优参数说明》,作者: 冰 河。
JVM 的分类
这里,我们先来说说什么是 VM 吧,VM 的中文含义为:虚拟机,指的是使用软件的方式模拟具有完整硬件系统功能、运行在一个完全隔离环境中的完整计算机系统,是物理机的软件实现。
常用的虚拟机有:VMWare、Virtual Box,Java Virtual Machine(JVM,Java 虚拟机)。
这里,我们重点聊的就是 JVM,Java 虚拟机。看下图。
这张图看起来还是比较简单的,JVM 运行于操作系统之上,操作系统是运行在计算机硬件上的。
关于 JVM,其实有很多大厂开发了不同版本的 JVM,比较知名的有:Sun HotSpot VM、BEA JRockit VM、IBM J9 VM、 Azul VM、 Apache Harmony、 Google Dalvik VM、 Microsoft JVM 等等。
现在使用的比较多的 JDK8 版本就是 Sun HotSpot VM 与 BEA JRockit VM 合并之后开发出的 JDK 版本。
JVM 的构成
JVM 主要由三个子系统构成,分别为:类加载器子系统、运行时数据区(内存结构)和字节码执行引擎。
为了更好的理解 JVM,我们来看一下 JVM 的全貌图。
当我们开发 Java 程序时,首先会编写.java 文件,之后,会将.java 文件编译成.class 文件。
JVM 中,会通过类装载子系统将.class 文件的内容装载到 JVM 的运行时数据区,而 JVM 的运行时数据区又会分为:方法区、堆、栈、本地方法栈和程序计数器 几个部分。
在装载 class 文件的内容时,会将 class 文件的内容拆分为几个部分,分别装载到 JVM 运行时数据区的几个部分。其中,值得注意的是:程序计数器的作用是:记录程序执行的下一条指令的地址。
方法区也叫作元空间,主要包含了:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应的 Class 实例的引用等信息。
在 JVM 中,程序的执行是通过执行引擎进行的,执行引擎会调用本地方法的接口来执行本地方法库,进而完成整个程序逻辑的执行。
我们常说的垃圾收集器是包含在执行引擎中的,在程序的运行过程中,执行引擎会开启垃圾收集器,并在后台运行,垃圾收集器会不断监控程序运行过程中产生的内存垃圾信息,并根据相应的策略对垃圾信息进行清理。
这里,大家需要注意的是:栈、本地方法栈和程序计数器是每个线程运行时独占的,而方法区和堆是所有线程共享的。所以,栈、本地方法栈和程序计数器不会涉及线程安全问题,而方法区和堆会涉及线程安全问题。
方法区(元空间)
很多小伙伴一看到方法区三个字,脑海中的第一印象可能是存储方法的地方吧。
实则不然,方法区的另一个名字叫作元空间,相信不少小伙伴或多或少的听说过元空间。这个区域是 JDK1.8 中划分出来的。主要包含:运行时常量池、类型信息、字段信息、方法信息、类加载器的引用、对应的 Class 实例的引用等信息。方法区中的信息能够被多个线程共享。
例如,在程序中声明的常量、静态变量和有关于类的信息等的引用,都会存放在方法区,而这些引用所指向的具体对象 一般都会在堆中开辟单独的空间进行存储,也可能会在直接内存中进行存储。
堆
堆中主要存储的是实际创建的对象,也就是会存储通过 new 关键字创建的对象,堆中的对象能够被多个线程共享。堆中的数据不需要事先明确生存期,可以动态的分配内存,不再使用的数据和对象由 JVM 中的 GC 机制自动回收。对 JVM 的性能调优一般就是对堆内存的调优。
Java 中基本类型的包装类:Byte、Short、Integer、Long、Float、Double、Boolean、Character 类型的数据是存储在堆中的。
堆一般会被分成年轻代和老年代。而年轻代又会被进一步分为 1 个 Eden 区和 2 个 Survivor 区。在内存分配上,如果保持默认配置的话,年轻代和老年代的内存大小比例为 1 : 2,年轻代中的 1 个 Eden 区和 2 个 Survivor 区的内存大小比例为:8 : 1 : 1。
栈
栈一般又叫作线程栈或虚拟机栈,一般存储的是局部变量。在 Java 中,每个线程都会有一个单独的栈区,每个栈中的元素都是私有的,不会被其他的栈所访问。栈中的数据大小和生存期都是确定的,存取速度比较快。
在 Java 中,所有的基本数据类型(byte、short、int、long、float、double、boolean、char)和引用变量(对象引用)都是在栈中的。一般情况下,线程退出或者方法退出时,栈中的数据会被自动清除。
程序在执行过程中,会在栈中为不同的方法创建不同的栈帧,在栈帧中又包含了:局部变量表、操作数栈、动态链接和方法出口。
关于局部变量表、操作数栈、动态链接和方法出口的具体作用,会在《架构师进阶系列》中的后续文章中详细阐述。
栈中一般会存储对象的引用,这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。
注意: 这里说的是这些引用所指向的具体对象一般都会在堆中开辟单独的地址空间进行存储,也有可能存储在直接内存中。
因为在 JVM 中,如果开启了逃逸分析和标量替换,则可能不会再在堆上创建对象,可能会将对象直接分配到栈上,也可能不再创建对象,而是进一步分解对象中的成员变量,将其直接在栈上分配空间并赋值。
本地方法栈
本地方法栈相对来说比较简单,就是保存 native 方法进入区域的地址。
例如,在 Java 中创建线程,调用 Thread 对象的 start()方法时,会通过本地方法 start0()调用操作系统创建线程的方法。此时,本地方法栈就会保存 start0()方法进入区域的内存地址。
程序计数器
程序计数器也叫作 PC 计数器,只要存储的是下一条将要执行的命令的地址。
JVM 调优参数
在 JVM 中,主要是对堆(新生代)、方法区和栈进行性能调优。各个区域的调优参数如下所示。
堆:-Xms、-Xmx
新生代:-Xmn
方法区(元空间):-XX:MetaspaceSize、-XX:MaxMetaspaceSize
栈(线程):-Xss
为了更加直观的表述,我们可以将 JVM 的内存区域和对应的调优参数总结成下图所示。
在设置 JVM 启动参数时,需要特别注意方法区(元空间)的参数设置。
关于方法区(元空间)的 JVM 参数主要有两个:-XX:MetaspaceSize 和-XX:MaxMetaspaceSize。
-XX:MetaspaceSize: 指的是方法区(元空间)触发 Full GC 的初始内存大小(方法区没有固定的初始内存大小),以字节为单位,默认为 21M。达到设置的值时,会触发 Full GC,同时垃圾收集器会对这个值进行修改。
如果在发生 Full GC 时,回收了大量内存空间,则垃圾收集器会适当降低此值的大小;如果在发生 Full GC 时,释放的空间比较少,则在不超过设置的-XX:MetaspaceSize 值或者在没设置-XX:MetaspaceSize 的值时不超过 21M,适当提高此值。
-XX:MaxMetaspaceSize: 指的是方法区(元空间)的最大值,默认值为-1,不受堆内存大小限制,此时,只会受限于本地内存大小。
最后需要注意的是: 调整方法区(元空间)的大小会发生 Full GC,这种操作的代价是非常昂贵的。如果发现应用在启动的时候发生了 Full GC,则很有可能是方法区(元空间)的大小被动态调整了。
所以,为了尽量不让 JVM 动态调整方法区(元空间)的大小造成频繁的 Full GC,一般将-XX:MetaspaceSize 和-XX:MaxMetaspaceSize 设置成一样的值。例如,物理内存 8G,可以将这两个值设置为 256M
最后,我们一起看下在物理内存 8G 的情况下,启动应用程序时,可以设置的 JVM 参数。当然,我这里给出的是一些经验值,实际部署到生产环境时,需要经过压测找到最佳的参数值。
启动 SpringBoot
启动 Tomcat(Linux)
在 Tomcat bin 目录下 catalina.sh 文件里配置。
启动 Tomcat(Windows)
在 Tomcat bin 目录下 catalina.bat 文件里配置。
总结
今天,我们一起学习了 JVM 的整体架构和调优参数,主要包括:JVM 的总体结构、JVM 的分类、JVM 的构成和调优参数。你学会了吗?欢迎在文末留言说出你的想法,如果你有更好的见解,也可以在文末留言和大家交流。
版权声明: 本文为 InfoQ 作者【华为云开发者联盟】的原创文章。
原文链接:【http://xie.infoq.cn/article/6559b2eb8ce589833272c8066】。文章转载请联系作者。
评论