JVM调优详解(一次java性能优化实战)

8,208 阅读11分钟

前言:

本文章主要讲述什么是JVM虚拟机,JVM的内存组成,垃圾回收算法,不同的垃圾回收器,以及性能调优的参数,若对JVM组成已有充足的认识,可直接查看第四章节JVM调优具体参数讲解

概要:

众所周知,JVM(Java Virtual Machine)是一种虚拟机,它是Java编程语言的运行环境,用于在不同的硬件平台上执行Java字节码,它隔离了操作系统的底层实现细节,使得Java程序只需编译成JVM识别的字节码,JVM再将字节码解释成操作系统能够识别的指令,实现了“一次编译,到处运行”。

JVM参数调优是对单个Java进程启动过程各类内存以及线程的分配使用,根据项目需要对各类参数的合理设置,需要对JVM内存模型以及垃圾回收机制有一定的理解;需知JVM参数众多,很多JVM默认的启用参数已足以满足项目需要,不要为了调优而大幅修改所有参数。

一、JVM内存模型

简短的说,JVM内存模型主要包含Java堆、Java虚拟机栈、本地方法栈、方法区、程序计数器。

image.png

  • Java堆为所有线程共享的内存区域,几乎所有的对象实例都是在这里创建,也是垃圾(对象)回收的主要区域,也被称为GC堆,垃圾收集时会根据对象的存活时间进行分代,不同的垃圾回收器会使用不同的垃圾回收算法,一般会分为年轻代/老年代,默认比例为1:2,年轻代为对象刚创建时存储的位置,若经过15次垃圾回收后还存活,则会被移到老年代。再细分年轻代被分为Eden区,From Survivor区,To Survivor区,默认比例为8:1:1。若堆内存占满无法释放时会抛出OutOfMemoryError错误。

      一般在启动命令中会指定Java堆内存使用的最大内存空间,如:
      java -Xms4g -Xmx4g -jar demo.jar
      上述命令会设置JVM启动时占有的最大堆内存为4GB,若使用完毕会导致堆内存溢出
    
  • 虚拟机栈线程私有的内存空间,每个线程独享一段内存,当线程执行进入新的方法时称为入栈,会记录方法内部的局部变量,方法出入参信息,方法执行结束会出栈,释放已使用的内存空间,若未指定默认的栈内存空间为256KB,若栈内存使用完毕会抛出StackOverFlowError。

      java -Xss256k -jar demo.jar
    
  • 本地方法栈基本与虚拟机栈功能相似,区别是本地方法栈执行的是由native修饰的本地方法。

  • 方法区,Java8之后才有的内存区域,在之前被称为永久代,与堆内存功能相似,主要用于存储在运行中基本不会释放的类信息,静态变量等,该内存与堆内存独立设置参数。

      java -XX:MetaspaceSize=256m -jar demo.jar
    
  • 程序计数器是线程私有的较小的内存区域,主要用于记录当前线程执行的字节码的行号,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

二、垃圾回收算法

主要有标记-清除算法复制算法(一般为年轻代使用)、标记-清除-整理算法(一般为老年代使用)。

如何判断对象是否能够存活
  • 引用计数法:每个对象在对象头里存储被引用的次数,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,无法解决对象相互循环引用的问题。
  • 可达性分析(Reachability Analysis):从GC Roots开始向下搜索,搜索所走过的路径称为引用链。当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,不可达对象。

当前流行的垃圾回收器都采用可达性分析作为判断垃圾回收的条件。

1. 标记-清除算法

image.png

该算法清除的效率不高,且清除后存在大量的内存碎片,故现在不再使用。

2. 复制算法

image.png

将内存区域分为两部分,将存活的对象复制到另一区域,并将剩余内存空间全部清理。该算法在存活对象较少的情况使用效率比较高,故用于年轻代。

上面说到年轻代被分为Eden区,From Survivor区,To Survivor区,默认比例为8:1:1。默认创建对象会在年轻代的Eden区,垃圾回收时会将还在存活的对象从Eden区拷贝到其中一个Survivor区,然后将剩余的Eden区清理干净,即完成一次Minor GC

image.png

3. 标记-整理算法

image.png

因老年代大多为存活时间较久的对象,故垃圾回收时一般回收对象较少,故使用标记-整理算法,将需回收的对象标记,并将剩余对象移动到一端,减少内存碎片的残留,老年代垃圾回收称为Full GC

三、垃圾回收器

1.Serial/Serial Old

  Serial/Serial Old收集器是最基本最古老的收集器,它是一个单线程收集器,并且在它进行垃圾收集时,必须暂停所有用户线程。目前基本没有在使用。

2.ParNew

  ParNew收集器是Serial收集器的多线程版本,使用多个线程进行垃圾收集。

3.Parallel Scavenge

  Parallel Scavenge收集器是一个新生代的多线程收集器(并行收集器),它在回收期间不需要暂停其他用户线程,其采用的是Copying算法,该收集器与前两个收集器有所不同,它主要是为了达到一个可控的吞吐量。

4.Parallel Old

  Parallel Old是Parallel Scavenge收集器的老年代版本(并行收集器),使用多线程和Mark-Compact算法。

5.CMS

  CMS(Current Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它是一种并发收集器,采用的是Mark-Sweep算法。(老年代收集器)

6.G1

  G1收集器是当今收集器技术发展最前沿的成果,它是一款面向服务端应用的收集器,它能充分利用多CPU、多核环境。因此它是一款并行与并发收集器,并且它能建立可预测的停顿时间模型。

image.png

在JDK1.8版本中,默认使用Parallel Scavenge作为新生代垃圾收集器,Parallel Old作为老年代垃圾回收器,一般用于CPU密集型程序使用。

也可以在启动命令中指定 -XX:+UseParNewGC -XX:+UseConcMarkSweepGC,将新生代改为ParNew,老年代改为CMS垃圾收集器,用于IO密集型程序使用。

在系统资源较为充足时,至少2核4g以上时,可以在启动命令指定 -XX:+UseG1GC,将垃圾回收器修改为G1垃圾回收器,堆内存会被分为2048个小块,不再细分为年轻代或老年代,内存回收为将一个块的存活对象拷贝到另一个空白块中,局部看是复制算法,整体看是标记整理算法。

具体G1垃圾回收器的讲解可以参考这篇文章,G1垃圾收集器全视角解析

四、JVM参数调优

JVM参数众多,很多JVM默认的启用参数已足以满足项目需要,不要为了调优而大幅修改所有参数,一般在启动命令中指定堆内存,垃圾回收器,元数据区以及栈内存空间即足够使用。

需根据当前系统资源以及进程类型,进程需使用资源数量分析。

1. 系统1核2G,程序为CPU密集型(即后台大量任务计算)
java -Xms1500M -Xmx1500M -Xss256k -XX:MetaspaceSize=256m -jar demo.jar

JDK1.8版本中,默认使用Parallel Scavenge作为新生代垃圾收集器,Parallel Old作为老年代垃圾回收器,无需手动指定。
2. 系统1核2G,程序为IO密集型(即主要为网络IO和磁盘IO,多为C端程序)
java -Xms1500M -Xmx1500M -Xss256k -XX:MetaspaceSize=256m -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -jar demo.jar

新生代改为ParNew,老年代改为CMS垃圾收集器,用于IO密集型程序使用。
3. 系统2核4G(或更多),程序为IO密集型(即主要为网络IO和磁盘IO,多为C端程序)
java -Xms1500M -Xmx1500M -XX:MetaspaceSize=256m -XX:+UseG1GC -jar demo.jar

系统资源充足时,推荐使用G1垃圾收集器,在JDK9之后默认使用G1垃圾收集。
打印GC日志
-XX:+PrintGCDetails	 输出形式:[GC [DefNew: 8614K->781K(9088K), 0.0123035 secs] 118250K->113543K(130112K), 0.0124633 secs]
-XX:+PrintGCTimeStamps  打印Gc时间戳
-Xloggc:logs/Gc.log	把相关日志信息记录到logs/GcLog.log文件以便分析

在启动命令中添加以上指令可打印GC执行日志,但会损耗部分性能,一般用于测试环境调试用,不要用于生产环境。

-XX:+HeapDumpOnOutOfMemoryError 出现堆内存溢出时,自动导出堆内存 dump 快照
-XX:HeapDumpPath=logs 设置导出的堆内存快照的存放地址为logs

这两个命令用于在堆内存溢出时打印溢出快照,可用于定位代码错误原因,可以手动开启。

五、JVM监控的命令:

jps命令,查看服务器正在运行的jvm进程号

19008 Jps
3140 RemoteMavenServer36
3604
18108 demo.jar

jmap –heap 18108 -heap 打印heap的概要信息,GC使用的算法,heap的配置及wise heap的使用情况.

using thread-local object allocation.
Concurrent Mark-Sweep GC   ##同步并行垃圾回收,也可能是这样:Parallel GC with 4 thread(s)
Heap Configuration:  ##堆初始化配置情况
   MinHeapFreeRatio = 40 ##最小堆使用比例:对应jvm启动参数-XX:MinHeapFreeRatio设置JVM堆最小空闲比率(default 40)
   MaxHeapFreeRatio = 70 ##最大堆可用比例:对应jvm启动参数 -XX:MaxHeapFreeRatio设置JVM堆最大空闲比率(default 70)
   MaxHeapSize      = 2147483648 (2048.0MB) ##最大堆空间大小:对应jvm启动参数-XX:MaxHeapSize=设置JVM堆的最大大小
   NewSize          = 268435456 (256.0MB) ##新生代分配大小:对应jvm启动参数-XX:NewSize=设置JVM堆的‘新生代’的默认大小
   MaxNewSize       = 268435456 (256.0MB) ##最大可新生代分配大小:对应jvm启动参数-XX:MaxNewSize=设置JVM堆的‘新生代’的最大大小
   OldSize          = 5439488 (5.1875MB) ##老生代大小:对应jvm启动参数-XX:OldSize=<value>:设置JVM堆的‘老生代’的大小
   NewRatio         = 2  ##新生代比例:对应jvm启动参数-XX:NewRatio=:‘新生代’和‘老生代’的大小比率
   SurvivorRatio    = 8 ##新生代与suvivor的比例:对应jvm启动参数-XX:SurvivorRatio=设置年轻代中Eden区与Survivor区的大小比值
   MetaspaceSize    = 268435456 (256.0MB) ##元数据空间大小,对应jvm启动参数-XX:MetaspaceSize=256m设置元数据区的初始大小

Heap Usage: ##堆使用情况:堆内存分步
New Generation (Eden + 1 Survivor Space):  ##新生代(伊甸区 + survior空间)
capacity = 241631232 (230.4375MB)  ##伊甸区容量
used     = 77776272 (74.17323303222656MB) ##已经使用大小
free     = 163854960 (156.26426696777344MB) ##剩余容量
32.188004570534986% used ##使用比例

Eden Space:  ##Eden区内存分布
   capacity = 214827008 (204.875MB) ##Eden区总容量
   used     = 74442288 (70.99369812011719MB) ##Eden区已使用
   free     = 140384720 (133.8813018798828MB) ##Eden区剩余容量
   34.65220164496263% used ##Eden区使用比率

From Space: ##survior1区
   capacity = 26804224 (25.5625MB) ##survior1区容量
   used     = 3333984 (3.179534912109375MB) ##surviror1区已使用情况
   free     = 23470240 (22.382965087890625MB) ##surviror1区剩余容量
   12.43827838477995% used ##survior1区使用比例

To Space: ##survior2 区
   capacity = 26804224 (25.5625MB) ##survior2区容量
   used     = 0 (0.0MB) ##survior2区已使用情况
   free     = 26804224 (25.5625MB) ##survior2区剩余容量
   0.0% used ## survior2区使用比例
concurrent mark-sweep generation: ##老年代
  capacity = 2147483648 (2048.0MB)
  used     = 42028568 (40.081565856933594MB)
  free     = 2105455080 (2007.9184341430664MB)
  1.9571077078580856% used
  

jstat -gc 4751 3000 10 可以查看进程号为4751的进程GC详情,每3000毫秒打印一次,打印10次

image.png

也可使用第三方工具Arthas来查看JVM运行状态

六、总结:

本文主要对JVM虚拟机以及垃圾回收机制,性能调优做了一个较为全面的讲解,希望能对读者有所帮助,学无止境。

作者:龙猫帝
原文链接:juejin.cn/post/695808…
版权所有,欢迎保留原文链接进行转载:)