JVM BOX

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: byte
  • C: char
  • S: short
  • D: double
  • F: float
  • I: int
  • J: long
  • Z: 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 命令看到,一般是 1MBdirect buffer 的大小可以用 JMX 工具看到。

从 JDK 1.8.40 可以看到,可以使用 Native Memory Tracker 来追踪 JVM 详细的内存开销,NMT 展示的是 committed 内存,而非 ps 命令汇报的 resident 内存,所以 NMT 展示的非堆内存可能高于 ps 命令汇报的 resident 内存。

参考