侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 297 篇文章
  • 累计创建 134 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

java虚拟机JVM内存分配及回收机制、虚拟机调优

孔子说JAVA
2021-09-10 / 0 评论 / 1 点赞 / 158 阅读 / 13,684 字 / 正在检测是否收录...

Java 虚拟机JVM(Java virtualmachine)是中、高级开发人员必须修炼的知识,学习门槛及成本较高。因为在开发环境一般较少涉及JVM的问题,所以很多人觉得是否了解无所谓,只要能编写出可执行的代码就可以了。其实在生产环境中的很多问题都是由 JVM 引发的故障问题,比如 OutOfMemoryError(OOM) 内存溢出问题,虚拟机参数不合理导致频繁的垃圾回收影响系统性能等,开发环境中tomcat容器因JVM参数配置不当导致的无法正常启动都现象。了解出现这些问题背后的原理,将让我们解决问题事半功倍,不再为无法找到问题原因而苦恼。

  • 可以把JVM理解成为一个应用程序,在服务器上是一个进程。JVM通过解释字节码文件达到执行java程序的目的,也就是通过Class Loader来加载class文件,并且按照Java API来执行加载的程序。一个Java程序就是一个main函数,每个Java程序启动分配一个JVM实例,即启动一个JVM。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行,因此实现Java平台无关性。

1、运行时数据存储区域

Java 虚拟机在执行 Java 程序的过程中会把它管理的内存划分为若干个不同的数据区域,主要包括:堆、栈、方法区、程序计数器等。

  • 堆、方法区是所有线程共享的内存区域;
  • 而java栈、本地方法栈和程序员计数器是运行是线程私有的内存区域。
  • JVM 的优化问题主要在线程共享的数据区中。

image-1649318207291

1.1 线程私有的内存区域

1.1.1 程序计数器(Program Counter Register)

是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的行号指示器,字节码解释器工作时通过改变计数器的值来选择下一条所需执行的字节码指令。

  • 为了确保线程切换后(上下文切换)能恢复到正确的执行位置,每个线程都有一个独立的程序计数器,各个线程的计数器互不影响,独立存储。也就是说程序计数器是线程私有的内存。
  • 如果线程执行 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果执行的是 Native 方法,计数器值为Undefined。
  • 程序计数器不会发生内存溢出(OutOfMemoryError即OOM)问题。

1.1.2 虚拟机栈(JVM Stacks)

与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,它的生命周期与线程相同。

  • 虚拟机栈描述的是Java方法执行的内存模型:每个方法被执行的时候都会同时创建一个栈帧(Stack Frame)用于存储局部变量表、操作栈、动态链接、方法出口等信息。
  • 每一个方法被调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 局部变量表:存放编译期可知的基本数据类型(boolean、byte、char、short、int、float、long、double)的局部变量(包括参数)、对象的引用reference类型(String、数组、对象等)和 returnAddress类型(指向一条字节码指令的地址)。

1.1.3 本地方法栈(Native Method Stacks)

本地方法栈与虚拟机栈所发挥的作用是非常相似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。

1.2 所有线程共享的内存区域

1.2.1 JAVA堆(Heap)

JAVA堆(Heap)是Java虚拟机所管理的内存中最大的一块,是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存。

  • JVM主要管理两种类型的内存:堆和非堆。在JVM中堆之外的内存又称为非堆内存(Non-heap memory)。简单来说堆就是Java代码可及的内存,是留给开发人员使用的;非堆就是JVM留给自己用的, 所以方法区、JVM内部处理或优化所需的内存(如JIT编译后的代码缓存)、每个类结构(如运行时常数池、字段和方法数据)以及方法和构造方法的代码都在非堆内存中。

1.2.2 方法区(Method Area)

方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的Class类信息、final常量、static静态变量、即时编译器编译后的代码等数据。

  • 在方法区还有个运行时常量池:存放编译生成的各种字面量和符号引用,运行期间也可能将新的常量放入池中。
  • 常量池避免了频繁的创建和销毁对象而影响系统性能,其实现了对象的共享。
  • jdk1.6中字符串常量池存放在永久代中,当使用intern()方法时,查询字符串常量池是否存在当前字符串,若不存在则将当前字符串复制到字符串常量池中,并返回字符串常量池中的引用。
  • jdk1.7中字符串常量池存放在堆中,当使用intern()方法时,先查询字符串常量池是否存在当前字符串,若字符串常量池中不存在则再从堆中查询,然后存储并返回相关引用;若都不存在则将当前字符串复制到字符串常量池中,并返回字符串常量池中的引用。
String s = new String("1") + new String("2");
System.out.println(s.intern() == s);

// jdk1.6中输出结果为 false
// 解释:s.intern()查询字符串常量池中是否存在“12”后,将“12”复制到字符串常量池中并返回字符串常量池的引用。而s存放在在堆中,返回false。

// jdk1.7中输出结果为 true
// 解释:s.intern()先查询字符串常量池中是否存在“12”后,再从堆中查询“12”是否存在并存储堆中的引用并返回。因此s.intern()与s指向的是同一个引用,返回true。

2、JAVA内存分配

JVM实质上分为三大块,年轻代(YoungGen),年老代(Old Memory),及持久代(perm, 也叫永久代,在Java8中持久代被取消,换成了元数据meta space)。

  • 垃圾回收GC,分为2种,一是Minor GC,可以可以称为YGC,即年轻代GC,Eden区还有一种垃圾回收称为Major GC,又称为FullGC。

2.1 JAVA堆内存分配

堆是JVM内存占用最大,管理最复杂的一个区域。唯一的途径就是存放对象实例。所有的对象实例以及数组都是在堆上进行分配。

  • JDK 1.7以后,字符串常量从永久代中剥离出来,存放在堆中。
  • JDK 1.8以前,堆内存分为新生代/年轻代(Young Generation),老年代(Old Generation)和永久代三个区域。JDK 1.8之后永久代换成了meta space元空间(元数据区域),主要是存放方法中的操作临时对象等.
  • JDK1.8之前是占用JVM内存,JDK1.8之后直接使用物理内存。(对象的堆内存由称为垃圾回收器的自动内存管理系统回收)

年轻代又分为伊甸园(Eden)和幸存区(Survivor区),幸存区又分为From Survivor空间(S0)和 To Survivor空间(S1)。

  • 年轻代存储“新生对象”,我们新创建(new)的对象先分配到年轻代Eden区中。当年轻内存占满后,会触发Minor GC垃圾回收,经过几次minor GC垃圾回收还存活的对象就会被放到S0区域,再经过几次minor GC垃圾回收还存活的对象会放到S1区。最后达到minor GC的阈值就放到老年代。
  • 老年代存储长期存活的对象和大对象,只进行major GC垃圾回收。年轻代中存储的对象,经过多次GC后仍然存活的对象会移动到老年代中进行存储。老年代空间占满后,会触发Full GC。
  • 注:Full GC是清理整个堆空间,包括年轻代和老年代。如果Full GC之后,堆中仍然无法存储对象,就会抛出OutOfMemoryError异常。

image-1649318572378

老年代约战2/3的堆空间,年轻代约战1/3的堆空间,eden区约战8/10 的年轻代,survivor0(S0)约战1/10 的年轻代,survivor1(S1)约战1/10的年轻代。

JVM初始分配的内存由-Xms指定,默认是物理内存的1/64;JVM最大分配的内存由-Xmx指 定,默认是物理内存的1/4。默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制;空余堆内存大于70%时,JVM会减少堆直到 -Xms的最小限制。因此服务器 一般设置-Xms、-Xmx相等 以避免在每次GC 后调整堆的大小。

  • 注:如果-Xmx 不指定或者指定偏小,应用可能会导致java.lang.OutOfMemory错误,此错误来自JVM,不是Throwable的,无法用try…catch捕捉。
// JVM初始内存和最大分配内存设置
-Xms128M 
-Xmx512M 

2.2 JAVA非堆内存分配

JDK1.7版本及以前 ,JVM使用-XX:PermSize设置非堆内存(永久代/方法区)初始值,默认是物理内存的1/64;由XX:MaxPermSize设置最大非堆内存的大小,默认是物理内存的1/4。

JDK1.8版本开始 ,元空间 MetaSpace 取代了永久代。元数据和永久代本质上都是方法区的实现。元空间并不在 JVM中,而是使用本地内存。元空间两个参数:

  • MetaSpaceSize:初始化元空间大小,控制发生GC阈。
  • MaxMetaspaceSize : 限制元空间大小上限,防止异常占用过多物理内存
// JDK1.8之前永久代参数设置:
-XX:PermSize=128m 
-XX:MaxPermSize=256
// XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space 就是内存益出。
// 内存溢出原因: 
// (1)这一部分内存用于存放Class和Meta的信息,Class在被 Load的时候被放入PermGen space区域,它
// 和存放Instance的Heap区域不同。 
// (2)GC(Garbage Collection)不会在主程序运行期对PermGen space进行清理,所以如果你的APP会
// LOAD很多CLASS 的话,就很可能出现PermGen space错误。
//   这种错误常见在web服务器对JSP进行pre compile的时候。

// JDK1.8元数据参数设置:
-XX:MetaspaceSize=18m
-XX:MaxMetaspaceSize=60m

2.3 JVM内存限制(最大值)

JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然 可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统 下为2G-3G),而64bit以上的处理器就不会有限制了。

3、JAVA内存调优

3.1 JVM调优目标

JVM调优目标是使用较小的内存占用来获得较高的吞吐量或者较低的延迟。

  • 程序在上线前的测试或运行中有时会出现一些大大小小的JVM问题,比如cpu load过高、请求延迟、tps降低等,甚至出现内存泄漏(每次垃圾收集使用的时间越来越长,垃圾收集频率越来越高,每次垃圾收集清理掉的垃圾数据越来越少)、内存溢出导致系统崩溃,因此需要对JVM进行调优,使得程序在正常运行的前提下,获得更高的用户体验和运行效率。

这里有几个比较重要的指标:

  • 内存占用:程序正常运行需要的内存大小。
  • 延迟:由于垃圾收集而引起的程序停顿时间。
  • 吞吐量:用户程序运行时间占用户程序和垃圾收集占用总时间的比值。

当然,和CAP原则一样,同时满足一个程序内存占用小、延迟低、高吞吐量是不可能的,程序的目标不同,调优时所考虑的方向也不同,在调优之前,必须要结合实际场景,有明确的的优化目标,找到性能瓶颈,对瓶颈有针对性的优化,最后进行测试,通过各种监控工具确认调优后的结果是否符合目标。

调优原则:

  1. 多数的Java应用不需要在服务器上进行GC优化;
  2. 多数导致GC问题的Java应用,都不是因为我们参数设置错误,而是代码问题;
  3. 在应用上线之前,先考虑将机器的JVM参数设置到最优(最适合);
  4. 减少创建对象的数量;
  5. 减少使用全局变量和大对象;
  6. GC优化是到最后不得已才采用的手段;
  7. 在实际使用中,分析GC情况优化代码比优化GC参数要多得多;

调优目标:

  1. 将转移到老年代的对象数量降低到最小;
  2. 减少full GC的执行时间;

为达目的需做的事情:

  1. 减少使用全局变量和大对象;
  2. 调整新生代的大小到最合适;
  3. 设置老年代的大小为最合适;
  4. 选择合适的GC收集器;

3.2 JVM调优工具

调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。

  1. 系统运行日志:系统运行日志就是在程序代码中打印出的日志,描述了代码级别的系统运行轨迹(执行的方法、入参、返回值等),一般系统出现问题,系统运行日志是首先要查看的日志。
  2. 堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。
  3. GC日志:程序启动时用 -XX:+PrintGCDetails 和 -Xloggc:/data/jvm/gc.log 可以在程序运行时把gc的详细过程记录下来,或者直接配置“-verbose:gc”参数把gc日志打印到控制台,通过记录的gc日志可以分析每块内存区域gc的频率、时间等,从而发现问题,进行有针对性的优化。
  4. 线程快照:顾名思义,根据线程快照可以看到线程在某一时刻的状态,当系统中可能存在请求超时、死循环、死锁等情况是,可以根据线程快照来进一步确定问题。通过执行虚拟机自带的“jstack pid”命令,可以dump出当前进程中线程的快照信息。
  5. 堆转储快照:程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。
  6. 用 jps(JVM process Status)可以查看虚拟机启动的所有进程、执行主类的全名、JVM启动参数,比如当执行了JPSTest类中的main方法后(main方法持续执行),执行 jps -l可看到下面的JPSTest类的pid为31354,加上-v参数还可以看到JVM启动参数。
  7. 用jstat(JVM Statistics Monitoring Tool)监视虚拟机信息。 jstat -gc pid 500 10 :每500毫秒打印一次Java堆状况(各个区的容量、使用容量、gc时间等信息),打印10次。jstat还可以以其他角度监视各区内存大小、监视类装载信息等。
  8. 用jmap(Memory Map for Java)查看堆内存信息。执行jmap -histo pid可以打印出当前堆中所有每个类的实例数量和内存占用,如下,class name是每个类的类名([B是byte类型,[C是char类型,[I是int类型),bytes是这个类的所有示例占用内存大小,instances是这个类的实例数量。执行 jmap -dump 可以转储堆内存快照到指定文件,比如执行 jmap -dump:format=b,file=/data/jvm/dumpfile_jmap.hprof 3361 可以把当前堆内存的快照转储到dumpfile_jmap.hprof文件中,然后可以对内存快照进行分析。
  9. 利用jconsole、jvisualvm分析内存信息(各个区如Eden、Survivor、Old等内存变化情况),如果查看的是远程服务器的JVM,程序启动需要加上远程连接配置参数。概览选项可以观测堆内存使用量、线程数、类加载数和CPU占用率;内存选项可以查看堆中各个区域的内存使用量和左下角的详细描述(内存大小、GC情况等);线程选项可以查看当前JVM加载的线程,查看每个线程的堆栈信息,还可以检测死锁;VM概要描述了虚拟机的各种详细参数。

3.3 JVM监控和调优步骤

3.3.1 监控GC的状态

使用各种JVM工具,查看当前日志,分析当前JVM参数设置,并且分析当前堆内存快照和gc日志,根据实际的各区域内存划分和GC执行时间,觉得是否进行优化;

3.3.2 分析结果,判断是否需要优化

如果各项参数设置合理,系统没有超时日志出现,GC频率不高,GC耗时不高,那么没有必要进行GC优化;如果GC时间超过1-3秒,或者频繁GC,则必须优化;

注:如果满足下面的指标,则一般不需要进行GC:

  • Minor GC执行时间不到50ms;
  • Minor GC执行不频繁,约10秒一次;
  • Full GC执行时间不到1s;
  • Full GC执行频率不算频繁,不低于10分钟1次;

3.3.3 调整GC类型和内存分配

如果内存分配过大或过小,或者采用的GC收集器比较慢,则应该优先调整这些参数,并且先找1台或几台机器进行beta,然后比较优化过的机器和没有优化的机器的性能对比,并有针对性的做出最后选择;

3.3.4 不断的分析和调整

通过不断的试验和试错,分析并找到最合适的参数

3.3.5 全面应用参数

如果找到了最合适的参数,则将这些参数应用到所有服务器,并进行后续跟踪。

4、JVM内存调优参数

JVM:JAVA_OPTS="-server -Xms2048m -Xmx2048m -Xss512k", JAVA_OPTS ,顾名思义,是用来设置JVM相关运行参数的变量。

-server:一定要作为第一个参数,在多个CPU时性能佳

-Xms:设置初始分配大小,默认是物理内存的1/64。实例-Xms512M

-Xmx:最大分配内存,默认是物理内存的1/4。实例-Xmx2G

-Xmn:设定新生代大小,新生代不宜太小,否则会有大量对象涌入老年代,官方推荐为整个堆的3/8。实例-Xmn512M

-Xss:线程堆栈大小,jdk1.5及之后默认1M,之前默认256k。实例-Xss512k

-XX:PermSize: 设定内存永久保存区域初始大小(JVM初始分配的非堆内存),默认是物理内存的1/64,JDK1.8之前。实例-XX:PermSize=128M

-XX:MaxPermSize: 设定内存永久保存区域的最大值(JVM分配的最大非堆内存),默认是物理内存的1/4,JDK1.8之前。实例-XX:MaxPermSize=256M

-XX:MetaspaceSize: 设定元数据初始值,JDK1.8及1.8之后。

-XX:MaxMetaspaceSize: 设定元数据最大值,JDK1.8及1.8之后。

-XX:NewRatio:新生代和老年代的占比。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4。实例-XX:NewRatio=3

-XX:NewSize:新生代空间。

-XX:SurvivorRatio:年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:8,表示Eden:Survivor=8:1:1,一个Survivor区占整个年轻代的1/8。实例-XX:SurvivorRatio=8

-XX:MaxTenuringThreshold:对象进入老年代的年龄阈值。

-XX:+PrintGC:打印GC日志,内容简单

-XX:+PrintGCDetails:输出详细的GC处理日志

-XX:+PrintGCDateStamps:输出GC的时间戳信息

-Xloggc:filename:指定gc日志路径。实例-Xloggc:/data/jvm/gc.log

-XX:+PrintHeapAtGC:在GC进行处理的前后打印堆内存信息。

-XX:+HeapDumpOnOutOfMemoryError: 表示当JVM发生OOM时,自动生成DUMP文件。

-XX:HeapDumpPath: 与HeapDumpOnOutOfMemoryError参数匹配使用,-XX:HeapDumpPath=${目录}参数表示生成DUMP文件的路径,也可以指定文件名称,例如:-XX:HeapDumpPath=${目录}/java_heapdump.hprof。如果不指定文件名,默认为:java_<pid>_<date>_<time>_heapDump.hprof。

-Xloggc:(SavePath):设置日志信息保存文件 在堆内存的调优策略中。

-verbose:class:在控制台打印类加载信息

-verbose:gc:在控制台打印垃圾回收日志

-XX:+UseSerialGC:年轻代设置串行收集器Serial

-XX:+UseParallelGC:年轻代设置并行收集器Parallel Scavenge	 

-XX:ParallelGCThreads=n:设置Parallel Scavenge收集时使用的CPU数。并行收集线程数。实例-XX:ParallelGCThreads=4

-XX:MaxGCPauseMillis=n:设置Parallel Scavenge回收的最大时间(毫秒)。实例-XX:MaxGCPauseMillis=100

-XX:GCTimeRatio=n:设置Parallel Scavenge垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)。实例-XX:GCTimeRatio=19

-XX:+UseParallelOldGC:设置老年代为并行收集器ParallelOld收集器	 

-XX:+UseConcMarkSweepGC:设置老年代并发收集器CMS	 

-XX:+CMSIncrementalMode:设置CMS收集器为增量模式,适用于单CPU情况。

5、tomcat调优参数

tomcat调优参数是修改安装目录下的\bin\catalina.bat  (linux修改的是catalina.sh)文件。在catalina.bat/catalina.sh文件中, 找到“echo “Using CATALINA_BASE: $CATALINA_BASE”,在上面加入调优参数配置。如下:

set "JAVA_OPTS=%JAVA_OPTS% %JSSE_OPTS% -server -XX:MetaspaceSize=500m -XX:MaxMetaspaceSize=756m -Xms4096m -Xmx4096m -Xmn2048m -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:../logs/tomcat_gc.log -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=../logs/heapdump.dump"
  • 其中:-XX:+PrintGCDetails表示打印GC详细信息;
  • XX:+PrintGCTimeStamps;-XX:+PrintGCDetails 表示打印CG发生的时间戳;
  • -Xloggc:表示GC日志打印的文件路径;
  • -XX:+HeapDumpOnOutOfMemoryError参数表示当JVM发生OOM时,自动生成DUMP文件;
  • -XX:HeapDumpPath=参数表示当JVM发生OOM时,生成DUMP文件的路径,也可以指定文件名称,例如:-XX:HeapDumpPath=${目录}/java_heapdump.hprof。如果不指定文件名,默认为:java_<pid>_<date>_<time>_heapDump.hprof

5.1 java.lang.OutOfMemoryError: PermGen space

错误信息中的PermGen space的全称是Permanent Generation space,是指内存的永久保存区域
XX:MaxPermSize设置过小会导致java.lang.OutOfMemoryError: PermGen space。

  • 所以如果你的应用中有很多CLASS或用了大量的第三方jar, 其大小超过了jvm默认的大小(4M),就很可能出现PermGen space错误, 这种错误常见在web服务器对JSP进行pre compile的时候。
// JDK1.8之前
JAVA_OPTS="-server -XX:PermSize=500M -XX:MaxPermSize=756m"

// JDK1.8及之后
JAVA_OPTS="-server --XX:MetaspaceSize=500m -XX:MaxMetaspaceSize=756m"

建议 :将相同的第三方jar文件移置到tomcat/shared/lib目录下,这样可以减少jar文档重复占用内存。

5.2 java.lang.OutOfMemoryError: Java heap space

Heap size 设置 JVM堆的设置是指java程序运行过程中JVM可以调配使用的内存空间的设置。JVM在启动的时候会自动设置Heap size的值,其初始空间(即-Xms)是物理内存的1/64,最大空间(-Xmx)是物理内存的1/4。

  • 可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置。Heap size 的大小是Young Generation 和Tenured Generaion 之和。

提示:

  1. 在JVM中如果98%的时间是用于GC且可用的Heap size 不足2%的时候将抛出此异常信息。
  2. Heap Size 最大不要超过可用物理内存的80%,一般的要将-Xms和-Xmx选项设置为相同,而-Xmn为1/4的-Xmx值。
JAVA_OPTS=”-server -Xms4096m -Xmx4096m -Xmn2048m”

5.3 java.lang.OutOfMemoryError: GC overhead limit exceeded

这个异常代表:GC为了释放很小的空间却耗费了太多的时间,其原因一般有两个:1,堆太小,2,有死循环或大对象;

  • 首先排除了第2个原因,因为这个应用同时是在线上运行的,如果有问题,早就挂了。所以怀疑是这台机器中堆设置太小;

使用ps -ef |grep "java"查看,发现:

image-1649319323980

该应用的堆区设置只有768m,而机器内存有2g,机器上只跑这一个java应用,没有其他需要占用内存的地方。另外,这个应用比较大,需要占用的内存也比较多;

通过上面的情况判断,只需要改变堆中各区域的大小设置即可,于是改成下面的情况:

image-1649319343813

跟踪运行情况发现,相关异常没有再出现;

5.4 服务经常卡顿

一个服务系统,经常出现卡顿,分析原因,发现Full GC时间太长:

jstat -gcutil:
S0   S1   E   O   P   YGC YGCT FGC FGCT   GCT
12.16 0.00 5.18 63.78 20.32 54 2.047 5   6.946  8.993

分析上面的数据,发现Young GC执行了54次,耗时2.047秒,每次Young GC耗时37ms,在正常范围,而Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的是指发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的原因:

  • 1,新生代太小,导致对象提前进入老年代,触发老年代发生Full GC;
  • 2,老年代较大,进行Full GC时耗时较大;

优化的方法是调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种做法对一些应用是很有用的,但并不是对所有应用都要这么做)

5.5 内存占用高,Full GC频繁

一应用在性能测试过程中,发现内存占用率很高,Full GC频繁,使用sudo -u admin -H  jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat差距进行分析,发现:

image-1649319500361

从图中可以看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,导致整个线程占用内存高达378m,此时通知开发人员进行代码优化,将相关对象释放掉即可。

6、垃圾回收日志分析

image-1649319527478

让我们来挑几条典型的日志进行分析:

第一条:63.971: [GC (Allocation Failure) [PSYoungGen: 31073K->4210K(38400K)] 31073K->4234K(125952K), 0.0049946 secs] [Times: user=0.05 sys=0.02, real=0.01 secs]

  1. 63.971:gc发生时,虚拟机运行了多少秒。
  2. GC (Allocation Failure) : 发生了一次垃圾回收,这是一次Minor GC 。注意它不表示只GC新生代,括号里的内容是gc发生的原因,这里的Allocation Failure的原因是年轻代中没有足够区域能够存放需要分配的数据而失败。
  3. PSYoungGen: 使用的垃圾收集器的名字。
  4. 31073K->4210K(38400K)指的是垃圾收集前->垃圾收集后(年轻代堆总大小)
  5. 31073K->4234K(125952K),指的是垃圾收集前后,Java堆的大小(总堆125952K,堆大小包括新生代和年老代),因此可以计算出年老代占用空间为125952k-38400k = 87552k
  6. 0.0049946 secs:整个GC过程持续时间
  7. [Times: user=0.05 sys=0.02, real=0.01 secs]分别表示用户态耗时,内核态耗时和总耗时。也是对gc耗时的一个记录。

第二条:106.840: [GC (System.gc()) [PSYoungGen: 130695K->5216K(472576K)] 228903K->121835K(644608K), 0.0342911 secs] [Times: user=0.13 sys=0.00, real=0.03 secs]

  • 先对于第一条的(Allocation Failure) 变成了(System.gc()),说明这是一次成功的垃圾回收。

第三条:83.783: [Full GC (System.gc()) [PSYoungGen: 15361K->0K(372224K)] [ParOldGen: 83468K->98200K(172032K)] 98829K->98200K(544256K), [Metaspace: 9989K->9989K(1058816K)], 0.3036213 secs] [Times: user=1.03 sys=0.00, real=0.30 secs]

对于Full GC:

  1. [PSYoungGen: 15361K->0K(372224K)] :年轻代:垃圾收集前->垃圾收集后(年轻代堆总大小)
  2. [ParOldGen: 83468K->98200K(172032K)]  :年老代:垃圾收集前->垃圾收集后(年老代堆总大小)
  3. 98829K->98200K(544256K), :垃圾收集前->垃圾收集后(总堆大小)
  4. [[Metaspace: 9989K->9989K(1058816K)], Metaspace空间信息,同上
  5. 0.3036213 secs:整个GC过程持续时间
  6. [Times: user=1.03 sys=0.00, real=0.30 secs] 分别表示用户态耗时,内核态耗时和总耗时。也是对gc耗时的一个记录。

第四条:71.601: [Full GC (Ergonomics) [PSYoungGen: 20555K->0K(367616K)] [ParOldGen: 104080K->83460K(172032K)] 124636K->83460K(539648K), [Metaspace: 9959K->9959K(1058816K)], 0.2146493 secs] [Times: user=0.64 sys=0.00, real=0.21 secs]

  • 这里可以看到full gc的reason是Ergonomics,是因为开启了UseAdaptiveSizePolicy,jvm自己进行自适应调整引发的full gc。

image-1649319910577

  1. 对于底下Heap的输出情况,和上面是完全一致的。只是增加了堆中每个部分total总大小,used使用情况。
  2. PSYoungGen分为eden space 530336K, 3% used , from space 48128K, 0% used , to   space 47104K, 0% used三个部分,分别显示了它们的大小和used比例。
  3. ParOldGen分为object space246784K, 1% used,显示了其大小和used比例。
  4. Metaspace中used大小为10174k。

解决方式:直接修改配置参数 -Xmn2g (这里需要根据需求来设置)

7、JVM问题排查记录案例

JVM服务问题排查

一次让人难以忘怀的排查频繁Full GC过程

线上FullGC频繁的排查

【JVM】线上应用故障排查

一次JVM中FullGC问题排查过程

JVM内存溢出导致的CPU过高问题排查案例

一个java内存泄漏的排查案例

1

评论区