川石教育
全国咨询热线:136-9172-9932
  1. 首页 > 资讯与干货 > IT资讯

JVM性能调优,深入浅出看完就懂,自学必看!

作者:川石学院 日期:2021-06-04 16:31:11 点击数:

  本章给大家介绍JVM性能调优,JVM:Java Virtual Machine叫Java虚拟机,Java语言最大的特点就是可以跨平台操作,JAVA之所以可以跨平台操作,是因为JAVA将写好的目标代码装载在一个叫JAVA虚拟机的平台上,这样可以保证在不同平台上运行时,不需要再次编译代码。那么所以运行的代码其实是在JVM中,即代码不是直接运行在我们操作平台,所以JVM调优核心是如何让JAVA源代码在JVM中运行的效率更高。影响JVM运行的效率核心指标是内存的使用,所以我们通常说的JVM调优都是在谈论内存分配的问题。

JVM性能调优,深入浅出看完就懂,自学必看!(图1)

  一、 JVM内存模型

  JVM的内存模型是由JMM来定义的,是一种规范,主要定义JVM在计算机内存RAM中的工作方式。它屏蔽了各种硬件和操作系统的访问差异,不像C那么直接访问硬件内存,相对来说会更安全些。其主要是解决多线程通过共享内存进行通信时本地内存数据不一至、指令重排序、代码乱序等执行相关的问题。这样可以更好的保证并发时场景中的原子性、可见性和有序性。

  其实关于JVM内存模型是开发要理解的,那为什么我们做性能测试也要理解呢?是因为如果我们做性能测试监控JVM时,如果对JVM内存使用的原理不理解的话,那么我们就无法很好的去理解JVM分代、堆、非堆等使用的情况。就更无法理解JVM调优的相关参数了。

  JVM内存模型主要包括五大内存区域,如图10-21所示。  

JVM性能调优,深入浅出看完就懂,自学必看!(图2)

  图10-21 JVM内存模型

  1) 程序计数器

  程序计数器(Program Counter Register)是JVM中一块较小的内存区域,是当前线程执行的字节码行号指标器,记录下条执行JVM指令的地址。因为一个处理器在同一个时刻只会执行一条线程指令,但一个线程中有多个指令,为了在线程切换时可以恢复到正确的执行位置,会为每个线程设置一个独立的程序计数器,这样可以让不同线程之间的程序计数器互不影响,独立存储。所以每个程序计数器都是线程私有的。

  这个区域是唯一一个在JAVA虚拟机规范中不存在OutOfMemoryError内存溢出的区域,程序计数器是由虚拟机内部维护,不需要开发者进行操作。这个程序计数器对应于JVM参数为-Xss。

  2) Java栈(虚拟机栈)

  当启动一个新线程的时候,Java虚拟机都会为它分配一个Java栈,Java在运行时会以栈桢为单位来保存线程的运行状态。Java虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机对Java栈只执行两种操作:以栈为单位的压栈或出栈。

  当然线程请求的栈深度大于虚拟机所允许的尝试,那么会抛出StackOverflowError异常,如果虚拟机栈可以动态扩展,但在扩展时无法申请到足够的内存时,就会抛出OutOfMemoryError的错误。

  栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的java虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。

  栈桢结构图如图10-22所示。  

JVM性能调优,深入浅出看完就懂,自学必看!(图3)

  图10-22 栈桢站结构图

  局部变量表(Local Variable Table)是一组变量值的存储空间,用于存放方法参数和方法内部定义的局部变量,并且在Java编译为Class文件时,可以计算出该方法所需要分配的局部变量表的最大容量。局部变量表会将一些基本数据类型,如boolean、byte、char、short、int、float、long、double等存放在里面。

  变量槽是局部变量表容量的最小单位,每个变量槽存储32位长度的内存空间,例如boolean、byte、char、short、int、float、reference。但是对于64位长度的数据类型,如long和double。那么虚拟机会以高位对齐的方式分配两个连接的存储空间,就相当于把long和double数据类型读写分割成为两次32位读写。

  在Java Class文件中会存放很多符号引用,字节码中的方法调用指令就是以常量池中指向方法的符号引用作为参数。这些符号引用有一部分会在类加载阶段或第一次使用时转化为直接引用,这种转化过程称之为静态解析。另一部分将在每一次运行期转化为直接引用,这种称之为动态连接(Dynamic Linking)。

  方法执行完成后,该方法必须退出,所谓的方法退出就相当于当前栈帧出栈。方法退出一般有两种方法:

  一是:使用方法返回指令,执行引擎遇到方法返回的字节码指令,然后会将值传递给上层的方法调用者,这是正常的一种退方式。

  二是:异常的退出方式,如果在方法执行过程中遇到了异常情况,并且如果没有即时处理这个异常,就会导致该方法退出。

  一般来说,该方法正常退出时,调用者计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。但无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。

  当该方法退出时可能执行的操作会恢复上层方法的局部变量表和操作数栈,如果有返回值,则把它压入调用者栈帧的操作数栈中,调整计数器的值以指向方法调用指令后面的一条指令。

  1) 本地方法栈

  本地方法栈(Native Method Stacks)即管理Native方法的地方,Navtive方法是Java 通过JNI直接调用本地C/C++库,也就是Native方法相当于一个接口,一个C/C++暴露给Java的接口,Java会通过这个接口去调用到C/C++方法。当线程调用Java方法时,虚拟机会创建一个栈帧,并将栈帧压入到Java虚拟机栈中。但当线程调用的是Native方法时,虚拟机会保持Java虚拟机栈不变,也不会向Java虚拟机栈中压入新的栈帧,虚拟机只是简单地连接并直接调用指定的Native方法即可。

  本地方法栈(Native Method Stacks)与 Java 虚拟机栈所发挥的作用是十分相似的,但还是有一些区别。主要区别有以下几个方面:

  Ø 不管是Java虚拟机栈还是本地方法栈,线程都是私有的。

  Ø 压栈时都是先进后出(LIFO)的方式。

  Ø 对于Java虚拟机会存储栈桢来支持Java方法的调用、执行和退出。

  Ø 对于本地方法栈主要是支撑Native方法的调用、执行和退出。

  Ø 都可能出现OutOfMemoryError异常和StackOverflowError异常。

  Ø 有一些虚拟机(如HotSpot)将Java虚拟机栈和本地方法栈合并实现。

  2) 堆

  堆是应该是我们在分析JVM中谈到最多的内容,它就是我们通常说的新生代、老年老代、持久代所保存的地方,这也是面试时最经常被问到的,如何设置新生代、老年代、持久代的值?新生代、年老代、持久代就是保存在堆中。

  堆是Java虚拟机管理内存最大的一块内存区域,堆存放的对象是线程共享的,所以多线程的时候也需要同步机制。堆是垃圾收集器管理的主要区域,因此也被称为“GC”堆

  堆从内存回收的角度来说,可以分为新生代和老年代,堆大小可以通过-Xmx和-Xms来设置,当内存空间不中时会提示OutOfMemoryError错误。

  3) 方法区

  方法区又称为非堆,是所有线程共享的内存区域。用于存储被虚拟机加载的类信息、常量、静态变量、静态代码块、即时编译器编译后的代码数据等。主要目标是针对数量池的回收和对类型的卸载。

  在JDK8之前,HotSpot是通过“永久代”来实现方法区的,其他虚拟机(如JRockit、J9VM)不存在永久代这个概念。方法区可以和Java堆一样被 HotSpot的垃圾收集器所管理,不需要单独处理。

  二、堆与栈

  在上一章节介绍JVM内存模型时,发现JVM内存分为堆和栈两种,那么为什么需要将内存分为堆和栈两种呢?之所以分成两类是为了JVM在调用内存时更好的对内存进行管理。

  在JAVA虚拟机中使用的数据又分为两类:一是基础数据;二是引用数据。基础数据是引用数据本身,引用数据是引用数据对象。基础数据通常包括:byte、short、int、long、char、float、double、Boolean、returnAddres。引用类型包括:接口、类、数组。

  栈是运行单位,所以的运行对象,都在是栈中,当程序运行时JVM会为每个线程一个栈大小。每个线程栈是不通用的,因为每个任务都有一个独立的线程来执行。堆是存储单位,所以有需要使用的数据都在堆中,堆是可以共享的。也就是堆是处理的数据的地方,栈是用来处理的逻辑的地方。 之所以分堆与栈,这样的好处是可以将业务逻辑与数据进行分离,同时也可以提高数据的共享程度。

  从软件设计的角度来看,栈代表了处理逻辑,而堆代表了数据,这样将数据与逻辑分离可以让处理逻辑更为清晰。这种隔离、模块化的思想在软件设计的方方面面都有体现。

  堆与栈的分离,使得堆中的内容可以被多个栈共享,但栈不管理 Java栈还是方法栈其线程都是私有的,是无法共享的,所以这样就可以让数据被多个线程共享进行操作。这种共享有很多好处,一方面提供了一种有效的数据交互方式(如内存共享),另一方面,节省了内存空间。

  栈因为运行时的需要进行址段的划分。由于栈只能向上增长,因此会限制住栈存储内容的能力。而堆不同,堆的大小可以根据需要动态增长。因此,堆与栈的分离,使得动态增长成为可能,相应栈中只需要记录堆中的一个地址即可。

  三、PermGen与Metaspace区别

  在java 8之前JVM第三代都是持久代PermGen,在java 8和之后的版本都是Metaspace元空间。

  持久代PermGen space的全称是Permanent Generation space,是指内存的永久保存区域,那么为什么会出现内存溢出呢?这是因为存放Class的信息在被加载时会放入到持久代PermGen space区域,当如果出现很多Class的话,那么就会可能出现PermGen space错误。

  JVM类型也很多种,比如 Oralce-Sun Hotspot、ralce JRockit、IBM J9、Taobao JVM等等。当然用的最多的还是Hotspot。需要注意的是,PermGen space是Oracle-Sun Hotspot才有,JRockit以及J9是没有这个区域。一般现在讨论的多的是Hotspot的JVM,所以通常会说持久代。

  持久代中包含了虚拟机中所有通过反射获取到的数据,如类和方法对象,不同的Java虚拟机之间可能会进行类的共享操作,因此持久代又分为只读区和读写区。关于JVM运行时会使用到多少持久代的空间取决于应该程序用到了多少类。除此之外,Java SE库中的类和方法也都存储在这里。当JVM对类的操作完成后,发现不再需要使用这个类时,就会将这个类释放出来,释放的空间需要使用Full GC进行回收。

  那么持久代是如何来设置呢?在JVM中可以通过MaxPermSize参数来设置,默认值为64M,Java堆中分配的区域尽量是连续的,如果非连续的堆空间,那要定位出持久代到新对象的引用是非常复杂的也是很耗时的。在堆中有一种记忆集叫卡表,可以记录某个内存代在普通对象指针的修改情况。当持久代都使用了后,系统就会抛出OutOfMemoryError的异常信息,当然解决的办法就是清理了不用的类或者增加MaxPermSize的值。

  那么在现在的JVM中为何将原来的持久代取消了呢?因为原来的持久代有以下一些缺点:

  1) 以前的版本中PermGen会存储一些字符串,PermGen内存的大小是通过-xx:PermSize这个参数来设置的,但是由于字符串池的大小经常是变化的,导致设置-xx:PermSize这个参数变的困难,这样很容易出现OOM提示的错误 ,java.lang.OutOfMemoryError: PermGen space。

  2) 以前将方法主要存储在PermGen,现在将方法都移动Metaspace,Metaspace不在JVM中,而是在本地的内存。

  3) 减少经常使用Full GC的频率。

  根据上面的各种原因,永久代最终被移除,永久代移除后,原来永久代中的方法区移至Metaspace元空间中,字符串常量移至Java Heap堆中。

  Metaspace元空间由两大部分组成:Klass Metaspace和NoKlass Metaspace。

  1) Klass Metaspace

  Klass Metaspace是用来存放klass的,就是class文件在JVM中运行时的数据结构,这部分内存空间默认放在Compressed Class Pointer Space中,是一个连续的内存区域块,紧接着Heap堆,在JVM中可以-XX:CompressedClassSpaceSize来控制这块内存大小,默认值为1G。

  Compressed Class Pointer Space不是必须存在的,如果设置了-XX:-UseCompressedClassPointers或者设置的-Xmx值大于32G,那么这块内存就不会存在,这种情况下klass就会存在NoKlass Metaspace中。

  2) NoKlass Metaspace

  NoKlass Metaspace专门来存klass相关的其它内容,如method、constantPool等,它可以是多个不连续的内存组成。这块内存是必须的,不能不存在,并且是在本地内存中进行分配。

  Klass Metaspace和NoKlass Metaspace两个部分的内存空间是所有类加载器都可以共享的,当然这些加载器都需要分配内存,为了更好的管理这些类加载器,每个类加载器都有一个SpaceManger空间管理来管理这些类加载器如何分配内存大小。分配的内存都是来自于实内存,如果Klass Metaspace用完了,那么就会提醒OutOfMemoryError异常,但一般的情况下是不会出现这种情况的,NoKlass Metaspace是由一小块一小块内存累加起来的。

  元空间和持久代在使用内存上是很类似的,都是对JVM规范中方法区的实现,但是他们分配内存是不同的,持久代内存是在虚拟机中,但是元空间是本地内存,所以正常情况下元空间的大小不受限制,如果说受限制那只是受本地内存限制,并且元空间一般是不可能会出现OutOfMemoryError异常的。设置元空间大小一般可以通过以下几个参数来实现:

  1) -XX:MetaspaceSize

  -XX:MetaspaceSize,初始空间大小,达到该值就会触发垃圾收集进行类型卸载,同时GC会对该值进行调整:如果释放了大量的空间,就适当降低该值;如果释放了很少的空间,那么就会提高该元空间的值,但不管怎么提高或增加元空间的值,都不能超过MaxMetaspaceSize所设置的值。

  2) -XX:MaxMetaspaceSize

  -XX:MaxMetaspaceSize表示元空间可以达到的最大值,默认是没有限制的,取决于机器的内存,限制类的元数据使用的内存大小,以免出现虚拟内存切换以及本地内存分配失败。如果怀疑有类加载器出现泄露,应当设置这个参数; 元空间的初始大小是21M,这是GC的初始的高水位线,超过这个大小会进行Full GC来进行类的回收。 如果启动后GC过于频繁,请将该值设置得大一些,可以设置成和持久代一样的大小,这个GC可以不用那么频繁的执行。

  3) -XX:MinMetaspaceFreeRatio

  -XX:MinMetaspaceFreeRatio表示GC之后,最小的Metaspace剩余空间容量的百分比,目的是控制减少为分配空间所导致的垃圾收集。MinMetaspaceFreeRatio和下面的MaxMetaspaceFreeRatio,主要是影响触发metaspaceGC的阈值。默认值为40,表示每次GC完之后,如果metaspace内存的空闲比例小于MinMetaspaceFreeRatio%,那么将尝试做扩容,增大触发metaspaceGC的阈值。不过这个增量至少是MinMetaspaceExpansion才会做,不然不会增加这个阈值。这个参数主要是为了避免触发metaspaceGC的阈值和GC之后committed的内存的量比较接近,于是将这个阈值进行扩大。

  注:这里不用GC之后used的量来算,主要是担心可能出现committed的量超过了触发metaspaceGC的阈值,这种情况一旦发生会很危险,会不断做GC。

  4) -XX:MaxMetaspaceFreeRatio

  -XX:MaxMetaspaceFreeRatio表示GC之后,最大的Metaspace剩余空间容量的百分比,目的是控制减少为释放空间所导致的垃圾收集。默认值为70,这个参数和上面的参数基本是相反的,是为了避免触发metaspaceGC的阈值过大,而想对这个值进行缩小。这个参数在GC之后committed的内存比较小的时候并且离触发metaspaceGC的阈值比较远的时候才进行调整。

  5) -verbose

  -verbose通过这个参数可以获取类型加载和卸载的信息。

  那么元空间这些内存是怎么来管理和分配或者说回收的呢?元空间的内存管理是由元空间虚拟机来管理,通常说的一个元空间是指一个类加载器的存储区域,当然所有元空间合在一起就称之为元空间,以前对于类的元数据需要不同的垃圾回收器来进行处理,但现在只需要执行虚拟机的C++代码即可以完成,并且类和其元数据的生命周期与类加载器是相同的,如果类加载器还是存活的话,那么类的元数据也是存活的,这个时候是不会被回收。当一个类加载器被垃圾回收器标记为不再存活时,其对应的元空间就会被回收。

  元空间虚拟机负责元空间的分配,其采用的形式为组块分配,组块的大小因类加载器的类型而异,在元空间虚拟机中存在一个全局的空闲组块列表,当一个类加载器需要一个组块时,它就会从这个全局的组块列表中获取,并不断的维持一个属于自己的组块列表,当类加载器不再存活时,这个组块也就会被释放,并返回给全局组块列表,类加载器拥有的组块会被分成很多个块,每个块存储一个单元的元信息,组块中的每个块是线性分配的,组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式连接,一旦某个虚拟内存映射区域清空,这部分内存就会返回给操作系统。

  如果需求监控Metaspace元空间的信息,可以使用JDK自带的一些工具来展示Metaspace的详细信息:

  针对Metaspace,JDK自带的一些工具做了修改来展示Metaspace的信息:

  jmap -clstats :打印类加载器的统计信息(取代了在JDK8之前打印类加载器信息的permstat)。

  jstat -gc :Metaspace的信息会被打印出来。

  jcmd GC.class_stats:这是一个新的诊断命令,可以使用户连接到存活的JVM,转储Java类元数据的详细统计。

  本章关于“java性能调优”的内容就学习完了,大家喜欢的话记得每天来这里和小编一起学习涨薪技能哦。(笔芯)

  附:川石信息全国校区最新开班时间,课程资料获取13691729932(微信同号)。  

JVM性能调优,深入浅出看完就懂,自学必看!(图4)


相关文章
  • 亚马逊运营成功转行软件测试,薪资13K表示很满意!2021-06-04 16:31:11
  • 西安川石的兰朋友喊你来当他的学弟学妹啦!2021-06-04 16:31:11
  • 国外的月亮也不一定比国内测试猿的年薪美~2021-06-04 16:31:11
  • 建筑工程专业朱同学成功转行为软件测试人!2021-06-04 16:31:11
  • 财务管理专业转行软件测试月薪甩会计几条街!2021-06-04 16:31:11
  • 只有技术沉淀才能成功上岸,深圳就业薪资13K!2021-06-04 16:31:11
  • 薪资11K!实现自我价值,从掌握一门IT技术开始...2021-06-04 16:31:11
  • 文科生转行软件测试照样拿下高薪15K!2021-06-04 16:31:11
  • 恭喜罗同学喜提19.5K,成功入行软件测试!2021-06-04 16:31:11
  • 毕业1年,迷茫的他最终选择转行软件测试2021-06-04 16:31:11