1.概述
JVM作为是JAVA中重要的基石,是java编程人员进阶路上的必需了解内容。为了帮助大家快速了解一些JVM的相关知识,本文将基于一个JVM案例(内存占比较高,调用垃圾回收方法后,内存占比仍然很高),来分析类似问题的解决方案以及排查思路。
2.JVM高内存占用案例
首先大概讲一下这个案例的基础现象:有一个JAVA应用程序,在经过多次垃圾回收之后,内存占用仍然很高。
针对上述案例,提供一种排查思路,具体如下(本文演示环境:idea,安装环境:jdk 1.8):
2.1 利用jps查看进程
jps(Java Virtual Machine Process Status Tool)是JDK提供的一个可以列出正在运行的Java虚拟机的进程信息的命令行工具,它可以显示JAVA虚拟机进程的执行主类(Main Class,main()函数所在的类)名称、本地虚拟机唯一ID(LVMID,Local Virtual Machine Identifier)等信息。注意,jps命令只显示它有访问权限的JAVA进程的信息。
jps一些指令信息:
指令 | 作用 |
---|---|
-q | 不显示主类名称、JAR文件名和传递给主方法的参数,只显示本地虚拟机唯一Id |
-m | 显示Java虚拟机启动时传递给main()方法的参数 |
-l | 显示主类的完整包名,如果进程执行的是JAR文件,也会显示JAR文件的完整路径 |
-v | 显示Java虚拟机启动时传递的JVM参数 |
-V | 显示主类名称和本地虚拟机唯一Id,不显示JAR文件名和传递给主方法的参数 |
hostid | 指定的远程主机,可以是ip地址和域名, 也可以指定具体协议,端口。如果不指定,则显示本机的Java虚拟机的进程信息 |
-help | 显示jps命令的帮助信息 |
jps -[q] -[mlvV] -[hostid]
jps -[help]
注意:在没有指定任何参数的情况下,jps命令会显示每个Java虚拟机进程的本地虚拟机唯一ID,后面跟着主类名称或JAR文件名的简短形式。同时,指令中的m、l、v、V可以任意组合。
利用jps指令可以查看当前JAVA虚拟机正在运行的进程,对于本地虚拟机来说,本地虚拟机唯一ID和操作系统的进程ID(PID,Process Identifier)是一致的,如果同时启动多个Java虚拟机进程,无法根据进程名称确定某个进程,我们就是使用jps命令显示主类名称的功能区分出来。
由于本文的测试环境是本地环境,因此可以先用jps指令查看一下虚拟机当前运行的JAVA进程,如下:
本文的测试代码写在JvisiualTest类中,因此需要重点排查该类。如果在centOS环境下遇到内存大量占用的情况,可以先用top指令查看进程id和内存占用情况。
2.2 利用jmap指令查看堆栈信息
jmap(Java Memory Map)是jdk安装后自带的一些小工具,主要用于打印指定JAVA进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。
利用如下指令展示pid的整体堆信息:
jmap -heap pid
下面仔细分析一下每个显示属性的具体意义:
Heap Configuration: //堆内存初始化配置
MinHeapFreeRatio = 0 //设置JVM堆最小空闲比率(默认40),对应jvm启动参数-XX:MinHeapFreeRatio
MaxHeapFreeRatio = 100 //设置JVM堆最大空闲比率(默认70),对应jvm启动参数-XX:MaxHeapFreeRatio
MaxHeapSize = 1864368128 (1778.0MB) //设置JVM堆的最大值,对应jvm启动参数-XX:MaxHeapSize=
NewSize = 38797312 (37.0MB) //设置JVM堆的“新生代”的默认大小,对应jvm启动参数-XX:NewSize=
MaxNewSize = 621281280 (592.5MB) //设置JVM堆的‘新生代’的最大值,对应jvm启动参数-XX:MaxNewSize=
OldSize = 78643200 (75.0MB) //设置JVM堆的“老生代”的大小,对应jvm启动参数-XX:OldSize=
NewRatio = 2 //“新生代”和“老生代”的大小比率,对应jvm启动参数-XX:NewRatio=
SurvivorRatio = 8 //设置年轻代中Eden区与Survivor区的大小比值,对应jvm启动参数-XX:SurvivorRatio=
MetaspaceSize = 21807104 (20.796875MB) //Metaspace扩容时触发FullGC的初始化阈值,也是最小的阈值,对应jvm启动参数-XX:MetaspaceSize
CompressedClassSpaceSize = 1073741824 (1024.0MB) //Java8在UseCompressedOops之外,额外增加了一个新选项叫做UseCompressedClassPointer。这个选项打开后,class信息中的指针也用32bit的Compressed版本。而这些指针指向的空间被称作“Compressed Class Space”。默认大小是1G,但可以通过“CompressedClassSpaceSize”调整
MaxMetaspaceSize = 17592186044415 MB //设置元空间Metaspace最大值,对应jvm启动参数-XX:MaxMetaspaceSize
G1HeapRegionSize = 0 (0.0MB) // 使用G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个Region块大小根据堆空间的实际大小而定,整体被控制在1MB到32MB之间,且为2的N次幂,即1MB,2MB, 4MB, 8MB, 1 6MB, 32MB。可以通过-XX :G1HeapRegionSize设定。所有的Region大小相同,且在JVM生命周期内不会被改变
Heap Usage: //堆内存使用情况
PS Young Generation
Eden Space: //Eden区内存分布
capacity = 58720256 (56.0MB) //Eden区总容量
used = 45173512 (43.08081817626953MB) //Eden区已使用
free = 13546744 (12.919181823730469MB) //Eden区剩余容量
76.93003245762416% used //Eden区使用比率
From Space: //其中一个Survivor区的内存分布
capacity = 4718592 (4.5MB)
used = 4424216 (4.219261169433594MB)
free = 294376 (0.28073883056640625MB)
93.76135932074652% used
To Space: //另一个Survivor区的内存分布
capacity = 26214400 (25.0MB)
used = 0 (0.0MB)
free = 26214400 (25.0MB)
0.0% used
PS Old Generation //当前的Old区内存分布
capacity = 254279680 (242.5MB) //总容量
used = 161013136 (153.55409240722656MB) //已使用
free = 93266544 (88.94590759277344MB) //剩余容量
63.32127521947487% used //使用率
2.3 利用jconsole查看堆内存信息
jconsole是jdk自带的监控工具,它用于连接正在运行的本地或者远程的JVM,对运行在java应用程序的资源消耗和性能进行监控,并利用可视化图表的形式提供监控界面,方便实时监控内存、堆栈信息等,同时该指令占用服务器的内存很小。
在idea中执行如下指令:
会得到如下结果:
执行GC后出现以下结果:
由上图可知,执行GC命令后,内存被回收了一部分,但是回收部分很小,内存并没有太大变化,说明大部分对象可能存在于老年代或永久代中,可以利用jvisualvm可视化方式来查看具体堆栈信息。
2.4 利用jvisualvm查看堆栈信息
jvisualvm是可以监控java运行内存的可视化工具,能够直观查看正在运行的JAVA服务内存信息、堆栈信息等,下面将利用该指令来查看一些关键信息。
执行上述命令会得到如下结果:
连接到异常的线程,可以得到如下堆内存信息:
抓取堆内存当前快照信息,得到结果如下:
得到当前堆内存快照信息如下:
由上图可知,其中占用内存最大的是一个ArrayList,内存大概209M,具体如下:
查看该数组可以发现,该ArrayList内部包含了200个Product对象,product对象内部有一个1048字节的对象,这些应该就是内存占用的原因:
2.5 源码分析
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class JvisiualTest {
public static void main(String[] args) throws InterruptedException {
List<Product> lists = new ArrayList<>();
for (int i = 0; i < 200; i++) {
lists.add(new Product());
}
TimeUnit.SECONDS.sleep(10000000);
}
}
class Product {
private byte[] big = new byte[1024 * 1024];
}
由上述代码可知,Product对象内部只有一个1M的对象,测试代码启动时,会生成200个Product对象,并放入ArrayList数组中,然后线程进入休眠状态,此时数组中的Product对象一直未被销毁,一直存在于老年代中,导致内存一直处于高占用状态。
3.小结
1.jps是一个实用指令,日常开发中可以查看java运行进程信息;
2.jmap可以打印指定JAVA进程的堆内存信息;
3.jconsole和jvisualvm是利用可视化的方式来查看堆内存信息及一些进程信息。
4.参考文献
1.https://www.jianshu.com/p/c52ffaca40a5
2.https://www.jianshu.com/p/b448c21d2e71
3.https://www.jianshu.com/p/5ee71f1724cd