JAVA从入门到放弃之JVM内存高占用问题排查

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进程,如下:
JAVA从入门到放弃之JVM内存高占用问题排查_第1张图片

本文的测试代码写在JvisiualTest类中,因此需要重点排查该类。如果在centOS环境下遇到内存大量占用的情况,可以先用top指令查看进程id和内存占用情况。

2.2 利用jmap指令查看堆栈信息

jmap(Java Memory Map)是jdk安装后自带的一些小工具,主要用于打印指定JAVA进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。
利用如下指令展示pid的整体堆信息:

jmap -heap pid

JAVA从入门到放弃之JVM内存高占用问题排查_第2张图片JAVA从入门到放弃之JVM内存高占用问题排查_第3张图片
下面仔细分析一下每个显示属性的具体意义:

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中执行如下指令:
在这里插入图片描述
会得到如下结果:
JAVA从入门到放弃之JVM内存高占用问题排查_第4张图片
执行GC后出现以下结果:
JAVA从入门到放弃之JVM内存高占用问题排查_第5张图片
由上图可知,执行GC命令后,内存被回收了一部分,但是回收部分很小,内存并没有太大变化,说明大部分对象可能存在于老年代或永久代中,可以利用jvisualvm可视化方式来查看具体堆栈信息。

2.4 利用jvisualvm查看堆栈信息

jvisualvm是可以监控java运行内存的可视化工具,能够直观查看正在运行的JAVA服务内存信息、堆栈信息等,下面将利用该指令来查看一些关键信息。
JAVA从入门到放弃之JVM内存高占用问题排查_第6张图片
执行上述命令会得到如下结果:

JAVA从入门到放弃之JVM内存高占用问题排查_第7张图片
连接到异常的线程,可以得到如下堆内存信息:
JAVA从入门到放弃之JVM内存高占用问题排查_第8张图片
抓取堆内存当前快照信息,得到结果如下:
JAVA从入门到放弃之JVM内存高占用问题排查_第9张图片
JAVA从入门到放弃之JVM内存高占用问题排查_第10张图片
得到当前堆内存快照信息如下:
JAVA从入门到放弃之JVM内存高占用问题排查_第11张图片
由上图可知,其中占用内存最大的是一个ArrayList,内存大概209M,具体如下:
JAVA从入门到放弃之JVM内存高占用问题排查_第12张图片
查看该数组可以发现,该ArrayList内部包含了200个Product对象,product对象内部有一个1048字节的对象,这些应该就是内存占用的原因:
JAVA从入门到放弃之JVM内存高占用问题排查_第13张图片
JAVA从入门到放弃之JVM内存高占用问题排查_第14张图片

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

你可能感兴趣的:(java,开发语言)