目录
OOM原因分析:
- 一次性申请对象数量过多
- 使用资源未释放
- 为JVM分配的空间不足
解决:
- 采用分页等,减少一次返回数据量的大小
- 检查代码中是否有未关闭的资源
- 修改JVM参数,合理设置堆大小
利用hprof文件分析的步骤,结合jvisualvm工具
- 程序启动时,提前加上参数如下参数,会在OOM发生时,导出堆文件,直接指定至目录即可,最终会生成后缀为hprof的文件
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/aaa/bbb/logs
例如:
java -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/opt/aaa/bbb/logs aaa-web-0.0.1.jar
- 程序运行过程中,可到出当前堆信息,生成aaa.hprof文件
jmap -dump:live,format=b,file=aaa.hprof pid
- 使用JDK自带的jvisualvm工具,装入aaa.hprof进行分析
- 点击【文件】->【装入】,选择aaa.hprof,点击【类】,找到实例数较多的类信息,并且是业务代码所定义的,点击进入该类,找到一个实例,查找GCRoot
- GCRoot对象在左上角会有蓝色三角形,找到后,右键【在线程中显示】,定位至具体业务代码,参考下面3张图
jvisualvm实时监控本地jvm
- java程序如果是本地运行的话,打开jvisualvm就会出现当前的进程信息,如下图
- 查看CPU、内存、类、线程等信息
- 查看线程信息。可以分析出是否创建线程过多,修改线程创建方式,采用线程池等
- 查看GC信息,需要装Visual GC插件,在【工具】->【插件】中可下载。或者访问该链接https://visualvm.github.io/pluginscenters.html,找到对应JDK版本的插件,导入后重启即可使用。
jvisualvm实时监控远程jvm
- 进入远程JDK路径:
cd /opt/aaa/jdk-11.0.8/bin
- 创建文件
touch jstatd-all.policy
- 编辑文件
vi jstatd-all.policy
- 文件内容:
grant codebase "file:/opt/aaa/jdk-11.0.8/lib/tools.jar" {
permission java.security.AllPermission;
};
- 修改文件权限
chmod 777 jstatd-all.policy
- 执行命令
./jstatd -J-Djava.security.policy=jstatd-all.policy -J-Djava.rmi.server.hostname=172.18.98.111
执行后,可能会报下面的错误
Could not create remote object
java.security.AccessControlException: access denied ("java.util.PropertyPermission" "java.rmi.server.ignoreSubClasses" "write")
at java.base/java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
at java.base/java.security.AccessController.checkPermission(AccessController.java:897)
at java.base/java.lang.SecurityManager.checkPermission(SecurityManager.java:322)
at java.base/java.lang.System.setProperty(System.java:894)
at jdk.jstatd/sun.tools.jstatd.Jstatd.main(Jstatd.java:140)
解决方法如下:
cd /opt/aaa/jdk-11.0.8/conf/security
vi java.policy
添加 permission java.security.AllPermission;
再回到JDK的bin目录,执行
./jstatd -J-Djava.security.policy=jstatd-all.policy -J-Djava.rmi.server.hostname=172.18.98.111
- 打开jvisualvm,右键左侧树结构中的【远程】->【添加远程主机】
- 右键已添加的主机,点击【添加jstatd连接】,不用修改,直接【确认】后,展示远程服务器上所有运行的java进程。
对于JVM的GC理解
新创建的对象被放于Eden区,满了之后触发MinorGC,老年代满了之后Full GC。
考虑并发量较高的业务,一次请求中包含多少对象数据,大概能占用多大的内存空间,一般不要超过S0、S1区的一半,否则对象直接被存入老年代,频繁导致Full GC,影响业务流程。每次Full GC都会停掉业务线程,所以尽量减少Full GC的发生。
Minor GC
由于Eden去残存的为朝生夕死的对象,大部分都是要清除的,残存对象较少,一般采用通过复制算法。
在Eden区和S0区标记找出所有GCRoot,然后清除所有未标记对象,并把剩余对象移至S1区,等待下一次Eden区满了之后,再次通过标记清除法,将剩余对象移至S0区,S0和S1每次只会使用一个。
对象从Eden区到S0区,以及S0和S1区的交换,累计经过15次之后,将次对象放入老年代,该阈值可配置。
Full GC
老年代一般情况需要清理的的对象比较少,都是存活率较高的,所以一般采用标记整理算法或者标记清除算法。
标记清除会产生碎片化的内存,存在缺陷,而标记整理会将剩余的对象移至一端,效率没有标记清除高。
每次Full GC都会停掉业务线程,所以尽量减少Full GC的发生。
常用JVM参数配置
-Xms:2g 初始化堆大小,等价于-XX:InitialHeapSize=2g,默认初始化堆大小为物理内存的1/64
-Xmx:2g 最大堆大小,等价于-XX:MaxHeapSize=2g,最大堆内存为物理内存的1/4
-Xmn:1g 新生代大小,等价于-XX:NewSize=1g
-XX:NewRatio=2 老年代与新生代比例为2:1,默认老年代与新生代比例为2:1
-XX:SurvivorRatio=8 Eden:S0:S1为2:1:1,Eden:S0:S1为2:1:1
-XX:MaxTenuringThreshold=15 被放入老年代的年龄阈值,15次
查看JVM堆信息
查看堆信息,使用jmap -heap 7118,会出现如下提示信息,需要用jhsdb jmap
Error: -heap option used
Cannot connect to core dump or remote debug server. Use jhsdb jmap instead
jhsdb jmap --pid 7118 --heap
Attaching to process ID 7118, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 11.0.8+10
using thread-local object allocation.
Garbage-First (G1) GC with 13 thread(s)
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 8392802304 (8004.0MB)
NewSize = 1363144 (1.2999954223632812MB)
MaxNewSize = 5035261952 (4802.0MB)
OldSize = 5452592 (5.1999969482421875MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 2097152 (2.0MB)
Heap Usage:
G1 Heap:
regions = 4002
capacity = 8392802304 (8004.0MB)
used = 950722080 (906.6792297363281MB)
free = 7442080224 (7097.320770263672MB)
11.327826458474863% used
G1 Young Generation:
Eden Space:
regions = 235
capacity = 1874853888 (1788.0MB)
used = 492830720 (470.0MB)
free = 1382023168 (1318.0MB)
26.286353467561522% used
Survivor Space:
regions = 7
capacity = 14680064 (14.0MB)
used = 14680064 (14.0MB)
free = 0 (0.0MB)
100.0% used
G1 Old Generation:
regions = 215
capacity = 1109393408 (1058.0MB)
used = 445308448 (424.6792297363281MB)
free = 664084960 (633.3207702636719MB)
40.1398137747002% used
一些问题的解决
执行jdk命令时,有可能会出现如下报错信息
Attaching to process ID 7118, please wait...
Error attaching to process: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version
sun.jvm.hotspot.debugger.DebuggerException: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version
at sun.jvm.hotspot.HotSpotAgent.setupVM(HotSpotAgent.java:435)
at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:305)
at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140)
at sun.jvm.hotspot.tools.Tool.start(Tool.java:185)
at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118)
at sun.jvm.hotspot.tools.HeapSummary.main(HeapSummary.java:49)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at sun.tools.jmap.JMap.runTool(JMap.java:201)
at sun.tools.jmap.JMap.main(JMap.java:130)
Caused by: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version
at sun.jvm.hotspot.runtime.VM.<init>(VM.java:291)
at sun.jvm.hotspot.runtime.VM.initialize(VM.java:370)
at sun.jvm.hotspot.HotSpotAgent.setupVM(HotSpotAgent.java:431)
... 11 more
原因是操作系统默认的JDK版本和程序使用的JDK版本不一致,程序使用的JDK11
查看系统JDK版本:java -version
java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)
可以在执行命令前,加上JDK11的路径,指定JDK为11
/opt/java/jdk-11.0.8/bin/jhsdb jmap --pid 7118 --heap
JVM调查命令总结
jstack
jstack pid
命令用于生成虚拟机当前时刻的线程快照。线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等都是导致线程长时间停顿的常见原因。
jinfo
jinfo -flags pid
查看进程的JVM配置信息
jmap
jmap -dump:live,format=b,file=heap.hprof pid
导出堆文件,并通过jvisualvm、MAT等工具进行分析
jhsdb jmap --pid pid --heap
打印heap的概要信息
jmap -histo pid
查看系统内存使用情况,可以找到跟业务代码相关的类信息
使用到的Linux命令总结
nohup
nohup /opt/cpms-server/bin/cpms-server.sh > /dev/null > /opt/cpms-server/logs/cpms-stdout.log 2>&1 &
拆解分析:
- nohup(no hang up
不挂起),在系统后台不挂断的运行命令,退出终端不影响程序运行,如果不加&,虽然不会在控制台输出log,但是ctrl+C会中断程序运行;加了&后,退出终端命令依旧执行 - /dev/null 指向一个不存在的文件,/dev/null 是一个黑洞,重定向到它的数据都会被扔掉
- 默认 >、>> 为标准输出(stdout),即:1>、1>>
- 标准输入0 stdin、标准输出1 stdout、标准错误2 stderr
- 覆盖>;>>追加 2>&1,&1为取stdout的重定向输出流地址,2>将stderr输出到stdout定向文件的地址
查看防火墙命令
systemctl status firewalld
查看进程信息
ps -aux|grep java 或者 jps
查看内存使用情况
free -h
查看硬盘使用情况
df -hT
top
top -p pid
RES使用的物理内存大小,%MEM使用的物理内存占实际内存的百分比
该页面输入H后,可显示进程下的线程信息
查找文件
find / -name fileName
添加环境变量
export JAVA_HOME=/opt/founder/jdk-11.0.8
export PATH=$PATH:$JAVA_HOME/bin
使用java -version命令,提示未找到命令,原因:未设环境变量
切换用户
su username