定位问题前的思路梳理
developer.aliyun.com/article/767…
oracle官方资料
jdk8 jvm参数: docs.oracle.com/javase/8/do…
一、 jvm内存空间介绍
-
程序计数器:当前线程所执行的字节码的信号指示器。程序控制流的指示器,分支、循环、跳转、异常处理、线程回复等基础功能都需要依赖这个计数器。
-
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
结果:
由上述的信息可以看出,参数有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
结果:
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:+UseGCLogFileRotation | UseGCLogFileRotation 指定日志文件滚动策略,很像 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/dump | HeapDumpPath 表示文件将被写入的路径。可以给出任何文件名;但是,如果 JVM 在名称中发现 标记,则导致内存不足错误的当前进程的进程 ID 将以 .hprof 格式附加到文件名中。 |
-XX:OnOutOfMemoryError="< cmd args >;< cmd args >" | OnOutOfMemoryError 用于发出紧急命令,在内存不足错误时执行这些命令。我们应该在 cmd args 空间中使用正确的命令。例如,如果我们想在内存不足时立即重新启动服务器,我们可以设置参数:-XX:OnOutOfMemoryError="shutdown -r"(一般用不到) |
-XX:+UseGCOverheadLimit | UseGCOverheadLimit 是一项策略,用于限制抛出 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就会将内存信息和堆信息收集到具体的路径下面
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 如果进行线程堆栈分析
3.3.4 其他重要jvm参数
jvm参数 | 作用 |
---|---|
-XX:ErrorFile=targetDir/hs_err_pid_%p.log | Location 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 选项)时才能使用此选项。