JVM 命令工具箱
在 Ubuntu 18.04 上验证
切换 Java 版本
$ update-alternatives --config java
JDK 命令无法执行
报错:
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process: ptrace(PTRACE_ATTACH, ..) failed for 34131: Operation not permitted
解决方法:
# 把这个文件里面的值从 1 改为 0 即可
echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
报错:
Exception in thread "main" java.lang.reflect.InvocationTargetException
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.jinfo.JInfo.runTool(JInfo.java:108)
at sun.tools.jinfo.JInfo.main(JInfo.java:76)
Caused by: java.lang.InternalError: Metadata does not appear to be polymorphic
at sun.jvm.hotspot.types.basic.BasicTypeDataBase.findDynamicTypeForAddress(BasicTypeDataBase.java:278)
at sun.jvm.hotspot.runtime.VirtualBaseConstructor.instantiateWrapperFor(VirtualBaseConstructor.java:102)
at sun.jvm.hotspot.oops.Metadata.instantiateWrapperFor(Metadata.java:68)
at sun.jvm.hotspot.memory.SystemDictionary.getSystemKlass(SystemDictionary.java:127)
at sun.jvm.hotspot.runtime.VM.readSystemProperties(VM.java:879)
或
Heap Usage:
Exception in thread "main" java.lang.reflect.InvocationTargetException
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: unknown CollectedHeap type : class sun.jvm.hotspot.gc_interface.CollectedHeap
at sun.jvm.hotspot.tools.HeapSummary.run(HeapSummary.java:144)
at sun.jvm.hotspot.tools.Tool.startInternal(Tool.java:260)
at sun.jvm.hotspot.tools.Tool.start(Tool.java:223)
at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118)
解决方法,应该安装对应的 Java 版本的 debug 包:
sudo apt install openjdk-8-dbg
查看进程基础信息
命令 | 作用 |
---|---|
jps -l |
列举全部 JVM 的进程 |
jps -l -m |
列举全部 JVM 的进程, -m 列举传递给 main() 方法的参数 |
java -version |
查看 JVM 工作模式 |
jinfo <pid> |
查看配置信息: System properties、JVM 命令行参数 |
jinfo -flag OldSize <pid> |
查看老年代内存大小 |
java -XX:+PrintCommandLineFlags -version |
让 JVM 打印出那些已经被用户或者JVM设置过的详细的XX参数的名称和值 |
java -XX:+PrintFlagsInitial -version |
看下所有 XX 参数的初始值 |
java -XX:+PrintFlagsFinal -version |
看下所有 XX 参数的最终值,这个就是程序最终使用的堆的配置 |
查看进程系统信息
命令 | 作用 |
---|---|
uname -a |
操作系统信息 |
cat /proc/<pid>/status |
VmRSS 的值是当前占用的物理内存 |
ps -p 51748 -o rss, vsz, sz |
RSS 是实际使用的内存,VSZ 是虚拟内存,单位都是 KB |
JVM 参数 -XX:NativeMemoryTracking=detail |
追踪 JVM 的内部内存使用,生产环节勿用 |
jcmd <pid> VM.native_memory scale=MB |
获取内存信息 |
查看 GC 情况
命令 | 作用 |
---|---|
jstat -gc <pid> 1000 5 |
堆各个区占用情况、垃圾回收次数,每 1000 毫秒输出一次,共输出 5 次 |
jstat -gccapacity <pid> 1000 5 |
比上面命令多输出: 各区域最大最小空间 |
jstat -gcutil <pid> 1000 5 |
各区域所占的百分比 |
jstat -gccause <pid> 1000 5 |
附加最近两次垃圾回收事件的原因 |
查看堆情况
命令 | 作用 |
---|---|
jmap -heap <pid> |
查看当前堆内存配置信息和使用情况 |
jmap -histo <pid> | sort -n -r -k 3 | head -20 |
堆每种类型实例的内存 bytes 倒序 |
jmap -histo <pid> | sort -n -r -k 2 | head -20 |
堆每种类型实例的 instances 倒序 |
jmap -histo:live <pid> | sort -n -r -k 2 | head -20 |
堆每种类型实例的 instances 倒序,加上了 :live 会强制 Full GC 一次,以便只统计存活的对象 |
jmap -dump:live,format=b,file=/home/myheapdump.hprof <pid> |
Dump 堆内存 |
jhat /home/myheapdump.hprof |
本地启动 HTTP Server 分析堆内存 |
jstat -class <pid> |
查看类加载情况 |
jmap -clstats <pid> |
查看类加载情况 |
查看栈信息
命令 | 作用 |
---|---|
jstack -l <pid> |
查看当前栈信息 |
JVM 参数解释
命令 | 作用 |
---|---|
-XX:InitialHeapSize=96468992 或 -Xms |
JVM 初始堆内存,单位 Byte |
-XX:MaxHeapSize=1541406720 或 -Xmx |
JVM 最大堆内存 |
-XX:MaxNewSize=513802240 |
JVM 新生代最大空间 |
-XX:NewSize=31981568 |
JVM 新生代初始大小 |
-XX:OldSize=64487424 |
老年代的默认大小,参数不一定生效 |
-XX:NewRatio |
老年代与新生代的比例 |
-XX:SurvivorRatio |
Eden 与 Survivor 幸存区 To/From 的比例 |
-XX:+HeapDumpBeforeFullGC 和 -XX:+HeapDumpAfterFullGC |
FullGC 前后分别对内存做 Heap Dump |
-XX:+UnlockDiagnosticVMOptions -XX:+PrintNMTStatistics |
JVM shutdown 的时候输出整体的 native memory 统计 |
各命令含义具体解释
class name
jmap -histo
列举的最后一列的 class name
的含义:
B
: byteC
: charS
: shortD
: doubleF
: floatI
: intJ
: longZ
: boolean[
: 数组,例如[i
表示int[]
[L 类名
: 表示某个类的数组
XX 参数语法
所有的XX参数都以”-XX:”开始,但是随后的语法不同,取决于参数的类型。
- 对于布尔类型的参数,我们有”+”或”-“,然后才设置JVM选项的实际名称。例如,
-XX:+<name>
用于激活<name>
选项,而-XX:-<name>
用于注销选项。 - 对于需要非布尔值的参数,如string或者integer,我们先写参数的名称,后面加上”=”,最后赋值。例如,
-XX:<name>=<value>
给<name>
赋值<value>
MaxMetaspaceSize 值过大
用 jmap -heap <pid>
显示的 MaxMetaspaceSize
值特别大,看起来就和没有限制一样。JDK 1.8 之后应该使用 -XX:MetaspaceSize=64m
和 -XX:MaxMetaspaceSize=128m
这两个参数限制元空间的大小。
默认堆大小
默认最多使用多大内存的堆空间是根据每台机器的配置、物理内存决定的。可以使用命令
java -XX:+PrintFlagsFinal -version | grep HeapSize
来查看初始堆内存和最大堆内存大小。对于 JDK 8 而言,一般最小和最大堆内存位于机器物理内存的 1/64 ~ 1/4 之间,其中物理内存从 cat /proc/meminfo
信息中获取。从如下参数也可以看到使用 RAM 的百分比:
java -XX:+PrintFlagsFinal -version | grep RAM
对于容器而言,Docker 容器本质是是宿主机上的一个进程,它与宿主机共享一个 /proc
目录,也就是说我们在容器内看到的 /proc/meminfo
,/proc/cpuinfo
与直接在宿主机上看到的一致,如果不加限制,可能存在 JVM 超额使用内存被 OS Kill 掉的风险。
对于 Java SE 8u131 之前的 JDK 版本,需要将 -Xmx
参数定义为与容器限制的资源一样的参数。
对于 Java SE 8u131 之后和 JDK 9 的版本,如果 JVM 是运行在容器中,那么需要正确的配置 JVM 参数以便感知到容器对于 JVM 最大可用内存的限制。当配置上 UseCGroupMemoryLimitForHeap
参数后,JVM 会根据 Linux cgroup 中的配置来决定最大内存,如下参数可以配置在 Tomcat 的 JAVA_OPTS
参数中:
-XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap
在此多少一句,对于 -XX:ParalllelGCThreads
、-XX:CICompilerCount
未设置的情况下,JVM Java SE 8u131 和 JDK 9 版本能够自动透明自动感知到 Docker 对于 CPU 的如个数等资源的限制。
对于 JDK8 的版本高于 191 以及 JDK 10,XXFraction
被标记为 deprecated,可以使用 Percentage
来更为灵活的定义堆空间可用比例:
-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=75.0 -XX:MinRAMPercentage=75.0
NativeMemoryTracking
- reserved 表示应用可用的内存大小,是 JVM 通过 mmaped 申请的虚拟地址空间,权限是
PROT_NONE
,在页表中已经存在了记录(entries),保证了其他进程不会被占用,但是 JVM 还不能直接使用这块内存。 - committed 表示应用正在使用的内存大小,是 JVM 向操作系统实际分配的内存(malloc/mmap),mmaped 权限是
PROT_READ | PROT_WRITE
,这块内存可以被直接使用。
mmap
函数可以把一个文件的内容在内存里面做一个映像。映射成功后,用户对这段内存区域的修改可以直接反映到内核空间,同样,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间<–>用户空间两者之间需要大量数据传输等操作的话效率是非常高的。
void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset);
mmap
函数里面的 prot
参数有如下取值:
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
GC 后内存何时释放给 OS
JVM GC 是把占用的垃圾内存回收到 JVM,而不是把 JVM 的内存回收给操作系统。可是 JVM 在申请内存的时候却不使用回收来的内存,而是继续去未开发的内存里去申请,直到申请到定义的 max
的内存。
堆收缩能力取决于 JVM 和 GC 垃圾收集器的版本:
- JDK 1.8 中默认的 GC 收集器为 Parallel GC,这个 GC 收集器在 GC 之后不会将内存返还给 OS,所以 GC 之后进程内存占用不会降低。参考
- JDK 8 以及更早版本,没有明确的选项一定会让内存回收给操作系统,但是通过配置如下这些参数
-XX:GCTimeRatio=19
、-XX:MinHeapFreeRatio=20
、-XX:MaxHeapFreeRatio=30
可以让 GC 更具 aggresive,花费更多的时间在 CPU 上,从而在 GC 之后约束分配但未使用的堆内存的总量。 - 对于并发收集器,可以设置
-XX:InitiatingHeapOccupancyPercent=N
参数为比较低的值,可以让 GC 几乎连续不停地收集垃圾,虽然会耗费更多的 CPU 但会更快的收缩堆。 -XX:+UseAdaptiveSizePolicy
可以自动调整堆大小。- G1 收集器,并且程序里面时不时的调用
System.gc()
,Java 将会释放内存给 OS。从 JDK 12 开始,空闲的时候 G1 收集器可以自动释放内存给 OS。 - ZGC 会自动释放内存给 OS。
JDK 1.8 内存占用
JVM 运行在 Pod 中
- Docker 汇报的内存 = file cache +
top
命令汇报的RSS
RSS
=Heap
大小 +MetaSpace
+OffHeap
OffHeap
=threads stack
+direct buffer
(NIO) +mapped files
(libraries/jars) +JVM code
使用 JVisualVM 可以看到 JVM 总共有多少个线程,每个线程占用的大小可以用 java -XX:+PrintFlagsFinal -version |grep ThreadStackSize
命令看到,一般是 1MB。direct buffer
的大小可以用 JMX 工具看到。
从 JDK 1.8.40 可以看到,可以使用 Native Memory Tracker 来追踪 JVM 详细的内存开销,NMT 展示的是 committed
内存,而非 ps
命令汇报的 resident
内存,所以 NMT 展示的非堆内存可能高于 ps
命令汇报的 resident
内存。
参考
- JVM实用参数(三)打印所有XX参数及值
- JVM实用参数(五)新生代垃圾回收
- Oracle GC Tuning Guide
- Java虚拟机中 -XX:+PrintFlagsFinal与XX:+PrintCommandLineFlags 中MaxHeapSize的值不同的原因
- 一次完整的JVM堆外内存泄漏故障排查记录
- 聊聊HotSpot VM的Native Memory Tracking
- 请教为何JVM不把空闲内存归还给操作系统
- Java内存占用分析
- Does GC release back memory to OS?
- Linux 内存映射函数 mmap()函数详解
- How is the default max Java heap size determined?
- Java SE support for Docker CPU and memory limits
- Kubernetes_pod_javajdk_动态JVM堆内存大小限制
- 关于Docker中设置Java应用的JVM
- Analyzing java memory usage in a Docker container
- JVM调优之探索CMS和G1的物理内存归还机制