JAVA问题定位只用看jvm的三个点——GC、theadump、heapdump

88 阅读12分钟

定位问题前的思路梳理

developer.aliyun.com/article/767…

oracle官方资料

jdk8 jvm参数: docs.oracle.com/javase/8/do…

一、 jvm内存空间介绍

image.png

  • 程序计数器:当前线程所执行的字节码的信号指示器。程序控制流的指示器,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器。

  • Java虚拟机栈:与程序计数器一样,Java虚拟机栈(Java Virtual Machine Stack)也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都 会同步创建一个栈帧(Stack Frame)用于存储局部变量表(存储基本数据类型 、对象引用)、操作数栈、动态连接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。

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

  • Java堆

        对于Java应用程序来说,Java堆(Java Heap)是虚拟机所管理的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,Java里“几乎”所有的对象实例都在这里分配内存。Java堆是垃圾收集器管理的内存区域,因此一些资料中它也被称作“GC堆”(Garbage Collected Heap)。

        如果从分配内存的角度看,所有线程共享的Java堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配时的效率。不过无论从什么角度,无论如何划分,都不会改变Java堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。

        根据《Java虚拟机规范》的规定,Java堆可以处于物理上不连续的内存空间中,但在逻辑上它应该 被视为连续的,这点就像我们用磁盘空间去存储文件一样,并不要求每个文件都连续存放。但对于大对象(典型的如数组对象),多数虚拟机实现出于实现简单、存储高效的考虑,很可能会要求连续的内存空间。

  • 方法区:方法区(Method Area)与Java堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。虽然《Java虚拟机规范》中把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫作“非堆”(Non-Heap),目的是与Java堆区分开来。方法区不等同于永久代(Permanent Generation),只是HotSpot虚拟机设计团队把收集器的分代设计扩展至方法区。而且有-XX:MaxPermSize的上限,如果不满足内存分配的要求会抛出OutOfMemoryError异常。JDK8废弃永久代概念,在本地内存中实现的元空间(Meta-space)来代替。这个空间的回收条件很苛刻,一般是一些类型的卸载等。

  • 运行时常量区:运行时常量池(Runtime Constant Pool)是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量与符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。

        String::intern()方法不停的往字符串常量池新增,jdk6时,会受到-XX:MaxPermSize影响而发生OOM,而JDK7 和 JDK8不会,原因见上图

  • 直接内存:直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。

        在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区 (Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆里面的 DirectByteBuffer对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了 在Java堆和Native堆中来回复制数据。

        显然,本机直接内存的分配不会受到Java堆大小的限制,但是,既然是内存,则肯定还是会受到 本机总内存(包括物理内存、SWAP分区或者分页文件)大小以及处理器寻址空间的限制,一般服务器管理员配置虚拟机参数时,会根据实际内存去设置-Xmx等参数信息,但经常忽略掉直接内存,使得 各个内存区域总和大于物理内存限制(包括物理的和操作系统级的限制),从而导致动态扩展时出现 OutOfMemoryError异常。

-XX:MaxPermSize(jdk7)永久代(Permanent Generation)大小
-XX:MaxMeta-spaceSize (jdk8)元空间大小,默认-1,只受限于本地内存大小
XX:MetaspaceSize(jdk8)指定元空间触发Fullgc的初始阈值(元空间无固定初始大小), 以字节为单位,默认是21M左右,达到该值就会触发full gc进行类型卸载, 同时收集器会对该值进行调整: 如果释放了大量的空间, 就适当降低该值; 如果释放了很少的空间, 那么在不超过-XX:MaxMetaspaceSize(如果设置了的话) 的情况下, 适当提高该值。这个跟早期jdk版本的-XX:PermSize参数意思不一样,-XX:PermSize代表永久代的初始容量。由于调整元空间的大小需要Full GC,这是非常昂贵的操作,如果应用在启动的时候发生大量Full GC,通常都是由于永久代或元空间发生了大小调整,基于这种情况,一般建议在JVM参数中将MetaspaceSize和MaxMetaspaceSize设置成一样的值,并设置得比初始值要大,对于8G物理内存的机器来说,一般都会将这两个值都设置为256M。
-XX:+/-UseTLAB虚拟机是否使用TLAB
-XX:MaxDirectMemorySize直接内存大小,默认-Xmx一样
-Xnoclassgc是否要对类型进行回收(jdk11 zgc不支持类卸载)
-verbose:class-XX:+TraceClass-Loading-XX:+TraceClassUnLoading查看类加载和卸载信息

二、如何查看现有java程序的jvm参数

2.1 查看jvm的运行参数

java -XX:+PrintFlagsFinal -version

结果:

image.png 由上述的信息可以看出,参数有boolean类型和数字类型,值的操作符是=或:=,分别代表默认值和被修改的值

2.1.1 查看堆内存配置

# 查看java的实际堆内存配置 
jmap -heap pid 

2.1.2 项目启动查看加载的class类有哪些

# linux系统上可以输出到具体的文件里面 
java -verbose:class 程序名 > log.log

2.2 用jinfo查看jvm内存参数

步骤一:先获取java pid

jps -l
#或者
ps -ef | grep java

步骤二:查看jvm内存设置

jinfo -flags  23832

结果:

image.png

2.2.1 查看某一参数的值,用法

jinfo ‐flag <参数名> <进程id>

E:\jvm>jinfo -flag MaxHeapSize  23832
-XX:MaxHeapSize=4263510016

2.2.2 查看启动jar包的jdk版本

ps -ef | grep java
可以看到启动的文件名路径 找到java的文件名 把全路径 -version就可以查出来具体的java版本

三、常见jvm参数调优

参考:www.baeldung.com/jvm-paramet…

3.1 打开GC日志

命令作用举例
-XX:+UseGCLogFileRotationUseGCLogFileRotation 指定日志文件滚动策略,很像 log4j、s4lj 等-XX:+UseGCLogFileRotation
-XX:NumberOfGCLogFiles=< number of log files >NumberOfGCLogFiles 表示单个应用程序生命周期可以写入的日志文件的最大数量。-XX:+UseGCLogFileRotation
-XX:GCLogFileSize=< file size >[ unit ]NumberOfGCLogFiles 表示单个应用程序生命周期可以写入的日志文件的最大数量-XX:GCLogFileSize=50M
-Xloggc:/path/to/gc.log 或者 gc-%t.log以日志维度生成多个文件loggc 表示它的位置-Xloggc:/path/to/gc-%t.log

这里需要注意的一点是,还有两个 JVM 参数可用(-XX:+PrintGCTimeStamps-XX:+PrintGCDateStamps),我们可以使用它们在 GC 日志中打印日期时间戳。

然而,问题是总是使用一个额外的守护线程来在后台监视系统时间。这种行为可能会造成一些性能瓶颈,这就是为什么最好不要在生产中使用此参数。

最佳实践参考:blog.51cto.com/u_1472521/3…

-XX:+PrintGcDetails
-XX:+PrintGcDatestamps
-XX:+PrintGcApplicationConcurrentTime
-XX:+PrintHeapAtGC
-Xloggc:/home/GCEASY/gc-%t.1og

详细可调整参数

-XX:+PrintGC

-XX:+PrintGCTimeStamps(打印GC发生时的时间戳)

-XX:+PrintGCDetails(打印更加详细的GC信息,包括回收后堆空间的详细占用情况)

-XX:+PrintHeapAtGC(打印回收前,回收后的堆空间占用详细情况)

-XX:+PrintGCApplicationConcurrentTime(打印回收期间应用程序的执行时间)

-XX:+PrintGCApplicationStoppedTime(打印由于GC而产生的应用程序停顿时间)

-XX:+PrintReferenceGC(跟踪系统内软引用、弱引用、虚引用和Finalize队列)

-Xloggc:gc.log(将日志输出到文本文件)

3.2 内存溢出的处理

命令作用
-XX:+HeapDumpOnOutOfMemoryError指示 JVM 在发生 OutOfMemoryError 时将堆转储到物理文件中
-XX:HeapDumpPath=./java_pid.hprof 一般用法是:-XX:HeapDumpPath=/path/dumpHeapDumpPath 表示文件将被写入的路径。可以给出任何文件名;但是,如果 JVM 在名称中发现 标记,则导致内存不足错误的当前进程的进程 ID 将以 .hprof 格式附加到文件名中。
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >"OnOutOfMemoryError 用于发出紧急命令,在内存不足错误时执行这些命令。我们应该在 cmd args 空间中使用正确的命令。例如,如果我们想在内存不足时立即重新启动服务器,我们可以设置参数:-XX:OnOutOfMemoryError="shutdown -r"(一般用不到)
-XX:+UseGCOverheadLimitUseGCOverheadLimit 是一项策略,用于限制抛出 OutOfMemory 错误之前 VM 在 GC 上花费的时间比例。(默认是开启的不用调整)
-   `-XX:+HeapDumpOnOutOfMemoryError` 当OutOfMemoryError发生时自动生成 Heap Dump 文件
-   `-XX:+HeapDumpBeforeFullGC` 当 JVM 执行 FullGC 前执行 dump
-   `-XX:+HeapDumpAfterFullGC` 当 JVM 执行 FullGC 后执行 dump
-   `-XX:+HeapDumpOnCtrlBreak` 交互式获取dump。在控制台按下快捷键Ctrl + Break时,JVM就会转存一下堆快照。
-   `-XX:HeapDumpPath=d:\test.hprof` 指定 dump 文件存储路径。    

3.2.1 频繁FullGC怎么查

步骤一:

# 查看当前的java pid
jps -l

步骤二:

# 假设5940是pid
#jinfo -flag +HeapDumpBeforeFullGC 5940 
#jinfo -flag +HeapDumpAfterFullGC 5940 
使用 #jinfo -flags pid 检查有没有生效

步骤三:

# 等发生fullgc后去dump文件目录下通过MAT等工具分析dump文件
#jinfo -flag -HeapDumpBeforeFullGC 5940
#jinfo -flag -HeapDumpAfterFullGC 5940 
使用 #jinfo -flags pid 检查有没有生效    

3.2.2 heap文件打印

# 1.可以打印当前对象的个数和大小
jmap -histo <java pid> > objhist.log
# 2.如果系统已经出现OOM并停止工作可以通过以下命令获取内存信息
jmap -heap: format=b <java pid> 
# 3.在启动期间添加
-XX:+HeapDumpOnOutOfMemoryError
-XX:+HeapDumpPath="具体的路径"
# 一旦发生OOM就会将内存信息和堆信息收集到具体的路径下面

docs.oracle.com/javase/8/do…

3.3 线程堆栈

线程堆栈排查oracle官方文档:docs.oracle.com/cd/E13150_0…

3.3.1 线程堆栈中需要关注的状态

  • runnable,线程处于执行中

  • deadlock,死锁(重点关注)

  • blocked,线程被阻塞 (重点关注)

  • Parked,停止

  • locked,对象加锁

  • waiting,线程正在等待

  • waiting to lock 等待上锁

  • Object.wait(),对象等待中

  • waiting for monitor entry 等待获取监视器(重点关注)

  • Waiting on condition,等待资源(重点关注),最常见的情况是线程在等待网络的读写

3.3.2 jstack命令

# 查看命令
jstack -help

选项是相互排斥的。选项(如果使用)应紧跟在命令名称之后。请参阅选项。

选项作用
-F当正常输出的请求不被响应时,强制输出线程堆栈
-m如果调用到本地方法的话,可以显示C/C++的堆栈
-l除堆栈外,显示关于锁的附加信息,在发生死锁时可以用jstack -l pid来观察锁持有情况

可以通过

jstack [options] pid >> /xxx/xx/x/dump.log
# 举例
jstack -l 580 >> /threadDump.log

命令,将堆栈信息输出到dump.log文件后,然后下载到本地排查文件。

3.3.3 如果进行线程堆栈分析

juejin.cn/post/733396…

juejin.cn/post/733396…

3.3.4 其他重要jvm参数

jvm参数作用
-XX:ErrorFile=targetDir/hs_err_pid_%p.logLocation of Fatal Error Log(docs.oracle.com/javase/8/do…
-XX:LargePageSizeInBytes=size在 Solaris 上,设置用于 Java 堆的大页面的最大大小(以字节为单位)。大小参数必须是 2 的幂 (2, 4, 8, 16, ...)。附加字母 k 或 K 表示千字节, m 或 M 表示兆字节, g 或 < b5> 表示千兆字节。默认情况下,大小设置为 0,这意味着 JVM 自动选择大页面的大小。
-XX:MaxDirectMemorySize=size设置新 I/O( java.nio 包)直接缓冲区分配的最大总大小(以字节为单位)。附加字母 k 或 K 表示千字节, m 或 M 表示兆字节, g 或 < b6> 表示千兆字节。默认情况下,大小设置为 0,这意味着 JVM 自动选择 NIO 直接缓冲区分配的大小。
-XX:NativeMemoryTracking=mode
-XX:ObjectAlignmentInBytes=alignment
-XX:OnError=string
-XX:+UseLargePages

-XX:FlightRecorderOptions=parameter=value
设置控制 JFR 行为的参数。这是一项与 -XX:+UnlockCommercialFeatures 选项结合使用的商业功能。仅当启用 JFR(即指定 -XX:+FlightRecorder 选项)时才能使用此选项。